pi-app-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/dist/command-classification.d.ts +59 -0
- package/dist/command-classification.d.ts.map +1 -0
- package/dist/command-classification.js +78 -0
- package/dist/command-classification.js.map +7 -0
- package/dist/command-execution-engine.d.ts +118 -0
- package/dist/command-execution-engine.d.ts.map +1 -0
- package/dist/command-execution-engine.js +259 -0
- package/dist/command-execution-engine.js.map +7 -0
- package/dist/command-replay-store.d.ts +241 -0
- package/dist/command-replay-store.d.ts.map +1 -0
- package/dist/command-replay-store.js +306 -0
- package/dist/command-replay-store.js.map +7 -0
- package/dist/command-router.d.ts +25 -0
- package/dist/command-router.d.ts.map +1 -0
- package/dist/command-router.js +353 -0
- package/dist/command-router.js.map +7 -0
- package/dist/extension-ui.d.ts +139 -0
- package/dist/extension-ui.d.ts.map +1 -0
- package/dist/extension-ui.js +189 -0
- package/dist/extension-ui.js.map +7 -0
- package/dist/resource-governor.d.ts +254 -0
- package/dist/resource-governor.d.ts.map +1 -0
- package/dist/resource-governor.js +603 -0
- package/dist/resource-governor.js.map +7 -0
- package/dist/server-command-handlers.d.ts +120 -0
- package/dist/server-command-handlers.d.ts.map +1 -0
- package/dist/server-command-handlers.js +234 -0
- package/dist/server-command-handlers.js.map +7 -0
- package/dist/server-ui-context.d.ts +22 -0
- package/dist/server-ui-context.d.ts.map +1 -0
- package/dist/server-ui-context.js +221 -0
- package/dist/server-ui-context.js.map +7 -0
- package/dist/server.d.ts +82 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +561 -0
- package/dist/server.js.map +7 -0
- package/dist/session-lock-manager.d.ts +100 -0
- package/dist/session-lock-manager.d.ts.map +1 -0
- package/dist/session-lock-manager.js +199 -0
- package/dist/session-lock-manager.js.map +7 -0
- package/dist/session-manager.d.ts +196 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +1010 -0
- package/dist/session-manager.js.map +7 -0
- package/dist/session-store.d.ts +190 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/session-store.js +446 -0
- package/dist/session-store.js.map +7 -0
- package/dist/session-version-store.d.ts +83 -0
- package/dist/session-version-store.d.ts.map +1 -0
- package/dist/session-version-store.js +117 -0
- package/dist/session-version-store.js.map +7 -0
- package/dist/type-guards.d.ts +59 -0
- package/dist/type-guards.d.ts.map +1 -0
- package/dist/type-guards.js +40 -0
- package/dist/type-guards.js.map +7 -0
- package/dist/types.d.ts +621 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +23 -0
- package/dist/types.js.map +7 -0
- package/dist/validation.d.ts +22 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +323 -0
- package/dist/validation.js.map +7 -0
- package/package.json +135 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Command Handlers - extensible server command dispatch via handler map.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the command-router.ts pattern for consistency.
|
|
5
|
+
* Each handler is a self-contained function that executes against
|
|
6
|
+
* the session manager context and returns a response.
|
|
7
|
+
*
|
|
8
|
+
* This extraction from session-manager.ts enables:
|
|
9
|
+
* - O(1) dispatch (handler map vs switch)
|
|
10
|
+
* - Isolated testing of server commands
|
|
11
|
+
* - Easy addition of new server commands
|
|
12
|
+
* - Smaller session-manager.ts (orchestration only)
|
|
13
|
+
*/
|
|
14
|
+
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
15
|
+
import type { RpcResponse, SessionInfo, StoredSessionInfo } from "./types.js";
|
|
16
|
+
/**
|
|
17
|
+
* Context provided to server command handlers.
|
|
18
|
+
* Contains everything handlers need without direct SessionManager coupling.
|
|
19
|
+
*/
|
|
20
|
+
export interface ServerCommandContext {
|
|
21
|
+
/** Resolve sessions by ID */
|
|
22
|
+
getSession: (sessionId: string) => AgentSession | undefined;
|
|
23
|
+
/** Get session info (includes metadata) */
|
|
24
|
+
getSessionInfo: (sessionId: string) => SessionInfo | undefined;
|
|
25
|
+
/** List all active sessions */
|
|
26
|
+
listSessions: () => SessionInfo[];
|
|
27
|
+
/** Create a new session */
|
|
28
|
+
createSession: (sessionId: string, cwd?: string) => Promise<SessionInfo>;
|
|
29
|
+
/** Delete a session */
|
|
30
|
+
deleteSession: (sessionId: string) => Promise<void>;
|
|
31
|
+
/** Load a session from a stored file */
|
|
32
|
+
loadSession: (sessionId: string, sessionPath: string) => Promise<SessionInfo>;
|
|
33
|
+
/** List stored sessions that can be loaded */
|
|
34
|
+
listStoredSessions: () => Promise<StoredSessionInfo[]>;
|
|
35
|
+
/** Get metrics from governor and stores */
|
|
36
|
+
getMetrics: () => RpcResponse;
|
|
37
|
+
/** Get memory sink metrics (optional, for ADR-0016 metrics system) */
|
|
38
|
+
getMemoryMetrics?: () => Record<string, unknown> | undefined;
|
|
39
|
+
/** Health check */
|
|
40
|
+
getHealth: () => RpcResponse;
|
|
41
|
+
/** Handle extension UI response */
|
|
42
|
+
handleUIResponse: (command: {
|
|
43
|
+
id?: string;
|
|
44
|
+
sessionId: string;
|
|
45
|
+
type: "extension_ui_response";
|
|
46
|
+
requestId: string;
|
|
47
|
+
response: any;
|
|
48
|
+
}) => {
|
|
49
|
+
success: boolean;
|
|
50
|
+
error?: string;
|
|
51
|
+
};
|
|
52
|
+
/** Route session command to appropriate handler */
|
|
53
|
+
routeSessionCommand: (session: AgentSession, command: any, getSessionInfo: (sessionId: string) => SessionInfo | undefined) => Promise<RpcResponse> | RpcResponse | undefined;
|
|
54
|
+
/** Generate a unique session ID */
|
|
55
|
+
generateSessionId: () => string;
|
|
56
|
+
/** Record heartbeat for session activity */
|
|
57
|
+
recordHeartbeat: (sessionId: string) => void;
|
|
58
|
+
/** Get circuit breaker manager */
|
|
59
|
+
getCircuitBreakers: () => {
|
|
60
|
+
hasOpenCircuit: () => boolean;
|
|
61
|
+
getBreaker: (provider: string) => {
|
|
62
|
+
canExecute: () => {
|
|
63
|
+
allowed: boolean;
|
|
64
|
+
reason?: string;
|
|
65
|
+
};
|
|
66
|
+
recordSuccess: (elapsedMs: number) => void;
|
|
67
|
+
recordFailure: (type: "timeout" | "error") => void;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
/** Get bash circuit breaker */
|
|
71
|
+
getBashCircuitBreaker: () => {
|
|
72
|
+
canExecute: (sessionId: string) => {
|
|
73
|
+
allowed: boolean;
|
|
74
|
+
reason?: string;
|
|
75
|
+
};
|
|
76
|
+
recordSuccess: (sessionId: string) => void;
|
|
77
|
+
recordTimeout: (sessionId: string) => void;
|
|
78
|
+
recordSpawnError: (sessionId: string) => void;
|
|
79
|
+
hasOpenCircuit: () => boolean;
|
|
80
|
+
getMetrics: () => {
|
|
81
|
+
enabled: boolean;
|
|
82
|
+
globalState: string;
|
|
83
|
+
sessionCount: number;
|
|
84
|
+
openSessionCount: number;
|
|
85
|
+
totalCalls: number;
|
|
86
|
+
totalTimeouts: number;
|
|
87
|
+
totalRejected: number;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
/** Get default command timeout in ms */
|
|
91
|
+
getDefaultCommandTimeoutMs: () => number;
|
|
92
|
+
}
|
|
93
|
+
export type ServerCommandHandler = (command: any, context: ServerCommandContext) => Promise<RpcResponse> | RpcResponse;
|
|
94
|
+
/**
|
|
95
|
+
* Execute an LLM command with circuit breaker protection.
|
|
96
|
+
* Returns undefined if not an LLM command (caller should route normally).
|
|
97
|
+
*/
|
|
98
|
+
export declare function executeLLMCommand(command: any, session: AgentSession, context: ServerCommandContext): Promise<RpcResponse | undefined>;
|
|
99
|
+
/**
|
|
100
|
+
* Execute a bash command with circuit breaker protection.
|
|
101
|
+
* Returns undefined if not a bash command (caller should route normally).
|
|
102
|
+
*
|
|
103
|
+
* Design: docs/design-bash-circuit-breaker.md
|
|
104
|
+
*
|
|
105
|
+
* Key difference from LLM circuit breaker:
|
|
106
|
+
* - Only TIMEOUT counts as failure (non-zero exit codes are often legitimate)
|
|
107
|
+
* - Hybrid protection: per-session + global circuit breakers
|
|
108
|
+
*/
|
|
109
|
+
export declare function executeBashCommand(command: any, session: AgentSession, context: ServerCommandContext): Promise<RpcResponse | undefined>;
|
|
110
|
+
export declare const serverCommandHandlers: Record<string, ServerCommandHandler>;
|
|
111
|
+
/**
|
|
112
|
+
* Route a server command to the appropriate handler.
|
|
113
|
+
* Returns a response or undefined if no handler exists (unknown command).
|
|
114
|
+
*/
|
|
115
|
+
export declare function routeServerCommand(command: any, context: ServerCommandContext): Promise<RpcResponse> | RpcResponse | undefined;
|
|
116
|
+
/**
|
|
117
|
+
* Get list of supported server command types.
|
|
118
|
+
*/
|
|
119
|
+
export declare function getSupportedServerCommands(): string[];
|
|
120
|
+
//# sourceMappingURL=server-command-handlers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-command-handlers.d.ts","sourceRoot":"","sources":["../src/server-command-handlers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
const handleListSessions = async (_command, context) => {
|
|
2
|
+
return {
|
|
3
|
+
type: "response",
|
|
4
|
+
command: "list_sessions",
|
|
5
|
+
success: true,
|
|
6
|
+
data: { sessions: context.listSessions() }
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
const handleCreateSession = async (command, context) => {
|
|
10
|
+
const sessionId = command.sessionId ?? context.generateSessionId();
|
|
11
|
+
const sessionInfo = await context.createSession(sessionId, command.cwd);
|
|
12
|
+
return {
|
|
13
|
+
type: "response",
|
|
14
|
+
command: "create_session",
|
|
15
|
+
success: true,
|
|
16
|
+
data: { sessionId, sessionInfo }
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
const handleDeleteSession = async (command, context) => {
|
|
20
|
+
await context.deleteSession(command.sessionId);
|
|
21
|
+
return {
|
|
22
|
+
type: "response",
|
|
23
|
+
command: "delete_session",
|
|
24
|
+
success: true,
|
|
25
|
+
data: { deleted: true }
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
const handleSwitchSession = (command, context) => {
|
|
29
|
+
const sessionInfo = context.getSessionInfo(command.sessionId);
|
|
30
|
+
if (!sessionInfo) {
|
|
31
|
+
return {
|
|
32
|
+
type: "response",
|
|
33
|
+
command: "switch_session",
|
|
34
|
+
success: false,
|
|
35
|
+
error: `Session ${command.sessionId} not found`
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
type: "response",
|
|
40
|
+
command: "switch_session",
|
|
41
|
+
success: true,
|
|
42
|
+
data: { sessionInfo }
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
const handleListStoredSessions = async (_command, context) => {
|
|
46
|
+
const storedSessions = await context.listStoredSessions();
|
|
47
|
+
return {
|
|
48
|
+
type: "response",
|
|
49
|
+
command: "list_stored_sessions",
|
|
50
|
+
success: true,
|
|
51
|
+
data: { sessions: storedSessions }
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
const handleLoadSession = async (command, context) => {
|
|
55
|
+
const sessionId = command.sessionId ?? context.generateSessionId();
|
|
56
|
+
try {
|
|
57
|
+
const sessionInfo = await context.loadSession(sessionId, command.sessionPath);
|
|
58
|
+
return {
|
|
59
|
+
type: "response",
|
|
60
|
+
command: "load_session",
|
|
61
|
+
success: true,
|
|
62
|
+
data: { sessionId, sessionInfo }
|
|
63
|
+
};
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return {
|
|
66
|
+
type: "response",
|
|
67
|
+
command: "load_session",
|
|
68
|
+
success: false,
|
|
69
|
+
error: error instanceof Error ? error.message : String(error)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const handleGetMetrics = (_command, context) => {
|
|
74
|
+
const response = context.getMetrics();
|
|
75
|
+
if (context.getMemoryMetrics && response.success) {
|
|
76
|
+
const memoryMetrics = context.getMemoryMetrics();
|
|
77
|
+
if (memoryMetrics && "data" in response && response.data) {
|
|
78
|
+
response.data.metrics = memoryMetrics;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return response;
|
|
82
|
+
};
|
|
83
|
+
const handleHealthCheck = (_command, context) => {
|
|
84
|
+
return context.getHealth();
|
|
85
|
+
};
|
|
86
|
+
const handleExtensionUIResponse = (command, context) => {
|
|
87
|
+
const result = context.handleUIResponse({
|
|
88
|
+
id: command.id,
|
|
89
|
+
sessionId: command.sessionId,
|
|
90
|
+
type: "extension_ui_response",
|
|
91
|
+
requestId: command.requestId,
|
|
92
|
+
response: command.response
|
|
93
|
+
});
|
|
94
|
+
if (result.success) {
|
|
95
|
+
return {
|
|
96
|
+
id: command.id,
|
|
97
|
+
type: "response",
|
|
98
|
+
command: "extension_ui_response",
|
|
99
|
+
success: true
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
id: command.id,
|
|
104
|
+
type: "response",
|
|
105
|
+
command: "extension_ui_response",
|
|
106
|
+
success: false,
|
|
107
|
+
error: result.error ?? "Unknown error"
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
const LLM_COMMANDS = /* @__PURE__ */ new Set(["prompt", "steer", "follow_up", "compact"]);
|
|
111
|
+
async function executeLLMCommand(command, session, context) {
|
|
112
|
+
const commandType = command.type;
|
|
113
|
+
const provider = session.model?.provider;
|
|
114
|
+
if (!LLM_COMMANDS.has(commandType) || !provider) {
|
|
115
|
+
return void 0;
|
|
116
|
+
}
|
|
117
|
+
const breaker = context.getCircuitBreakers().getBreaker(provider);
|
|
118
|
+
const breakerCheck = breaker.canExecute();
|
|
119
|
+
if (!breakerCheck.allowed) {
|
|
120
|
+
return {
|
|
121
|
+
id: command.id,
|
|
122
|
+
type: "response",
|
|
123
|
+
command: commandType,
|
|
124
|
+
success: false,
|
|
125
|
+
error: breakerCheck.reason ?? "Circuit breaker open"
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const startTime = Date.now();
|
|
129
|
+
try {
|
|
130
|
+
const routed = context.routeSessionCommand(session, command, context.getSessionInfo);
|
|
131
|
+
if (routed === void 0) {
|
|
132
|
+
return void 0;
|
|
133
|
+
}
|
|
134
|
+
const response = await Promise.resolve(routed);
|
|
135
|
+
const elapsedMs = Date.now() - startTime;
|
|
136
|
+
if (response.success) {
|
|
137
|
+
breaker.recordSuccess(elapsedMs);
|
|
138
|
+
} else {
|
|
139
|
+
const errorMsg = response.error?.toLowerCase() ?? "";
|
|
140
|
+
if (errorMsg.includes("timeout") || errorMsg.includes("timed out")) {
|
|
141
|
+
breaker.recordFailure("timeout");
|
|
142
|
+
} else {
|
|
143
|
+
breaker.recordFailure("error");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return response;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
const elapsedMs = Date.now() - startTime;
|
|
149
|
+
const errorMsg = error instanceof Error ? error.message.toLowerCase() : "";
|
|
150
|
+
if (errorMsg.includes("timeout") || elapsedMs >= context.getDefaultCommandTimeoutMs()) {
|
|
151
|
+
breaker.recordFailure("timeout");
|
|
152
|
+
} else {
|
|
153
|
+
breaker.recordFailure("error");
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function executeBashCommand(command, session, context) {
|
|
159
|
+
const commandType = command.type;
|
|
160
|
+
const sessionId = command.sessionId;
|
|
161
|
+
if (commandType !== "bash") {
|
|
162
|
+
return void 0;
|
|
163
|
+
}
|
|
164
|
+
const bashBreaker = context.getBashCircuitBreaker();
|
|
165
|
+
const breakerCheck = bashBreaker.canExecute(sessionId);
|
|
166
|
+
if (!breakerCheck.allowed) {
|
|
167
|
+
return {
|
|
168
|
+
id: command.id,
|
|
169
|
+
type: "response",
|
|
170
|
+
command: commandType,
|
|
171
|
+
success: false,
|
|
172
|
+
error: breakerCheck.reason ?? "Bash circuit breaker open"
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const routed = context.routeSessionCommand(session, command, context.getSessionInfo);
|
|
177
|
+
if (routed === void 0) {
|
|
178
|
+
return void 0;
|
|
179
|
+
}
|
|
180
|
+
const response = await Promise.resolve(routed);
|
|
181
|
+
if (response.success) {
|
|
182
|
+
bashBreaker.recordSuccess(sessionId);
|
|
183
|
+
} else {
|
|
184
|
+
const errorMsg = (response.error ?? "").toLowerCase();
|
|
185
|
+
if (errorMsg.includes("timeout") || errorMsg.includes("timed out")) {
|
|
186
|
+
bashBreaker.recordTimeout(sessionId);
|
|
187
|
+
} else {
|
|
188
|
+
bashBreaker.recordSuccess(sessionId);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return response;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
const errorMsg = error instanceof Error ? error.message.toLowerCase() : "";
|
|
194
|
+
if (errorMsg.includes("timeout") || errorMsg.includes("timed out")) {
|
|
195
|
+
bashBreaker.recordTimeout(sessionId);
|
|
196
|
+
} else {
|
|
197
|
+
bashBreaker.recordSpawnError(sessionId);
|
|
198
|
+
}
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const serverCommandHandlers = {
|
|
203
|
+
// Session lifecycle
|
|
204
|
+
list_sessions: handleListSessions,
|
|
205
|
+
create_session: handleCreateSession,
|
|
206
|
+
delete_session: handleDeleteSession,
|
|
207
|
+
switch_session: handleSwitchSession,
|
|
208
|
+
// Persistence (ADR-0007)
|
|
209
|
+
list_stored_sessions: handleListStoredSessions,
|
|
210
|
+
load_session: handleLoadSession,
|
|
211
|
+
// Metrics & health
|
|
212
|
+
get_metrics: handleGetMetrics,
|
|
213
|
+
health_check: handleHealthCheck,
|
|
214
|
+
// Extension UI
|
|
215
|
+
extension_ui_response: handleExtensionUIResponse
|
|
216
|
+
};
|
|
217
|
+
function routeServerCommand(command, context) {
|
|
218
|
+
const handler = serverCommandHandlers[command.type];
|
|
219
|
+
if (!handler) {
|
|
220
|
+
return void 0;
|
|
221
|
+
}
|
|
222
|
+
return handler(command, context);
|
|
223
|
+
}
|
|
224
|
+
function getSupportedServerCommands() {
|
|
225
|
+
return Object.keys(serverCommandHandlers);
|
|
226
|
+
}
|
|
227
|
+
export {
|
|
228
|
+
executeBashCommand,
|
|
229
|
+
executeLLMCommand,
|
|
230
|
+
getSupportedServerCommands,
|
|
231
|
+
routeServerCommand,
|
|
232
|
+
serverCommandHandlers
|
|
233
|
+
};
|
|
234
|
+
//# sourceMappingURL=server-command-handlers.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/server-command-handlers.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Server Command Handlers - extensible server command dispatch via handler map.\n *\n * Mirrors the command-router.ts pattern for consistency.\n * Each handler is a self-contained function that executes against\n * the session manager context and returns a response.\n *\n * This extraction from session-manager.ts enables:\n * - O(1) dispatch (handler map vs switch)\n * - Isolated testing of server commands\n * - Easy addition of new server commands\n * - Smaller session-manager.ts (orchestration only)\n */\n\nimport type { AgentSession } from \"@mariozechner/pi-coding-agent\";\nimport type { RpcResponse, SessionInfo, StoredSessionInfo } from \"./types.js\";\n\n// =============================================================================\n// HANDLER TYPE\n// =============================================================================\n\n/**\n * Context provided to server command handlers.\n * Contains everything handlers need without direct SessionManager coupling.\n */\nexport interface ServerCommandContext {\n /** Resolve sessions by ID */\n getSession: (sessionId: string) => AgentSession | undefined;\n /** Get session info (includes metadata) */\n getSessionInfo: (sessionId: string) => SessionInfo | undefined;\n /** List all active sessions */\n listSessions: () => SessionInfo[];\n /** Create a new session */\n createSession: (sessionId: string, cwd?: string) => Promise<SessionInfo>;\n /** Delete a session */\n deleteSession: (sessionId: string) => Promise<void>;\n /** Load a session from a stored file */\n loadSession: (sessionId: string, sessionPath: string) => Promise<SessionInfo>;\n /** List stored sessions that can be loaded */\n listStoredSessions: () => Promise<StoredSessionInfo[]>;\n /** Get metrics from governor and stores */\n getMetrics: () => RpcResponse;\n /** Get memory sink metrics (optional, for ADR-0016 metrics system) */\n getMemoryMetrics?: () => Record<string, unknown> | undefined;\n /** Health check */\n getHealth: () => RpcResponse;\n /** Handle extension UI response */\n handleUIResponse: (command: {\n id?: string;\n sessionId: string;\n type: \"extension_ui_response\";\n requestId: string;\n response: any;\n }) => { success: boolean; error?: string };\n /** Route session command to appropriate handler */\n routeSessionCommand: (\n session: AgentSession,\n command: any,\n getSessionInfo: (sessionId: string) => SessionInfo | undefined\n ) => Promise<RpcResponse> | RpcResponse | undefined;\n /** Generate a unique session ID */\n generateSessionId: () => string;\n /** Record heartbeat for session activity */\n recordHeartbeat: (sessionId: string) => void;\n /** Get circuit breaker manager */\n getCircuitBreakers: () => {\n hasOpenCircuit: () => boolean;\n getBreaker: (provider: string) => {\n canExecute: () => { allowed: boolean; reason?: string };\n recordSuccess: (elapsedMs: number) => void;\n recordFailure: (type: \"timeout\" | \"error\") => void;\n };\n };\n /** Get bash circuit breaker */\n getBashCircuitBreaker: () => {\n canExecute: (sessionId: string) => { allowed: boolean; reason?: string };\n recordSuccess: (sessionId: string) => void;\n recordTimeout: (sessionId: string) => void;\n recordSpawnError: (sessionId: string) => void;\n hasOpenCircuit: () => boolean;\n getMetrics: () => {\n enabled: boolean;\n globalState: string;\n sessionCount: number;\n openSessionCount: number;\n totalCalls: number;\n totalTimeouts: number;\n totalRejected: number;\n };\n };\n /** Get default command timeout in ms */\n getDefaultCommandTimeoutMs: () => number;\n}\n\nexport type ServerCommandHandler = (\n command: any,\n context: ServerCommandContext\n) => Promise<RpcResponse> | RpcResponse;\n\n// =============================================================================\n// SESSION LIFECYCLE HANDLERS\n// =============================================================================\n\nconst handleListSessions: ServerCommandHandler = async (_command, context) => {\n return {\n type: \"response\" as const,\n command: \"list_sessions\" as const,\n success: true,\n data: { sessions: context.listSessions() },\n };\n};\n\nconst handleCreateSession: ServerCommandHandler = async (command, context) => {\n const sessionId = command.sessionId ?? context.generateSessionId();\n const sessionInfo = await context.createSession(sessionId, command.cwd);\n return {\n type: \"response\" as const,\n command: \"create_session\" as const,\n success: true,\n data: { sessionId, sessionInfo },\n };\n};\n\nconst handleDeleteSession: ServerCommandHandler = async (command, context) => {\n await context.deleteSession(command.sessionId);\n return {\n type: \"response\" as const,\n command: \"delete_session\" as const,\n success: true,\n data: { deleted: true },\n };\n};\n\nconst handleSwitchSession: ServerCommandHandler = (command, context) => {\n const sessionInfo = context.getSessionInfo(command.sessionId);\n if (!sessionInfo) {\n return {\n type: \"response\" as const,\n command: \"switch_session\" as const,\n success: false,\n error: `Session ${command.sessionId} not found`,\n };\n }\n return {\n type: \"response\" as const,\n command: \"switch_session\" as const,\n success: true,\n data: { sessionInfo },\n };\n};\n\n// =============================================================================\n// PERSISTENCE HANDLERS (ADR-0007)\n// =============================================================================\n\nconst handleListStoredSessions: ServerCommandHandler = async (_command, context) => {\n const storedSessions = await context.listStoredSessions();\n return {\n type: \"response\" as const,\n command: \"list_stored_sessions\" as const,\n success: true,\n data: { sessions: storedSessions },\n };\n};\n\nconst handleLoadSession: ServerCommandHandler = async (command, context) => {\n const sessionId = command.sessionId ?? context.generateSessionId();\n try {\n const sessionInfo = await context.loadSession(sessionId, command.sessionPath);\n return {\n type: \"response\" as const,\n command: \"load_session\" as const,\n success: true,\n data: { sessionId, sessionInfo },\n };\n } catch (error) {\n return {\n type: \"response\" as const,\n command: \"load_session\" as const,\n success: false,\n error: error instanceof Error ? error.message : String(error),\n };\n }\n};\n\n// =============================================================================\n// METRICS & HEALTH HANDLERS\n// =============================================================================\n\nconst handleGetMetrics: ServerCommandHandler = (_command, context) => {\n const response = context.getMetrics();\n // Add memory sink metrics if available (ADR-0016)\n if (context.getMemoryMetrics && response.success) {\n const memoryMetrics = context.getMemoryMetrics();\n if (memoryMetrics && \"data\" in response && response.data) {\n (response.data as Record<string, unknown>).metrics = memoryMetrics;\n }\n }\n return response;\n};\n\nconst handleHealthCheck: ServerCommandHandler = (_command, context) => {\n return context.getHealth();\n};\n\n// =============================================================================\n// EXTENSION UI HANDLER\n// =============================================================================\n\nconst handleExtensionUIResponse: ServerCommandHandler = (command, context) => {\n const result = context.handleUIResponse({\n id: command.id,\n sessionId: command.sessionId,\n type: \"extension_ui_response\",\n requestId: command.requestId,\n response: command.response,\n });\n\n if (result.success) {\n return {\n id: command.id,\n type: \"response\" as const,\n command: \"extension_ui_response\" as const,\n success: true,\n };\n }\n return {\n id: command.id,\n type: \"response\" as const,\n command: \"extension_ui_response\" as const,\n success: false,\n error: result.error ?? \"Unknown error\",\n };\n};\n\n// =============================================================================\n// LLM COMMAND EXECUTION HELPER\n// =============================================================================\n\n/** LLM commands that should be protected by circuit breaker */\nconst LLM_COMMANDS = new Set([\"prompt\", \"steer\", \"follow_up\", \"compact\"]);\n\n/**\n * Execute an LLM command with circuit breaker protection.\n * Returns undefined if not an LLM command (caller should route normally).\n */\nexport async function executeLLMCommand(\n command: any,\n session: AgentSession,\n context: ServerCommandContext\n): Promise<RpcResponse | undefined> {\n const commandType = command.type;\n const provider = session.model?.provider;\n\n // Not an LLM command - let normal routing handle it\n if (!LLM_COMMANDS.has(commandType) || !provider) {\n return undefined;\n }\n\n const breaker = context.getCircuitBreakers().getBreaker(provider);\n const breakerCheck = breaker.canExecute();\n\n if (!breakerCheck.allowed) {\n return {\n id: command.id,\n type: \"response\" as const,\n command: commandType,\n success: false,\n error: breakerCheck.reason ?? \"Circuit breaker open\",\n };\n }\n\n const startTime = Date.now();\n try {\n const routed = context.routeSessionCommand(session, command, context.getSessionInfo);\n if (routed === undefined) {\n return undefined;\n }\n\n const response = await Promise.resolve(routed);\n const elapsedMs = Date.now() - startTime;\n\n if (response.success) {\n breaker.recordSuccess(elapsedMs);\n } else {\n const errorMsg = response.error?.toLowerCase() ?? \"\";\n if (errorMsg.includes(\"timeout\") || errorMsg.includes(\"timed out\")) {\n breaker.recordFailure(\"timeout\");\n } else {\n breaker.recordFailure(\"error\");\n }\n }\n\n return response;\n } catch (error) {\n const elapsedMs = Date.now() - startTime;\n const errorMsg = error instanceof Error ? error.message.toLowerCase() : \"\";\n if (errorMsg.includes(\"timeout\") || elapsedMs >= context.getDefaultCommandTimeoutMs()) {\n breaker.recordFailure(\"timeout\");\n } else {\n breaker.recordFailure(\"error\");\n }\n throw error;\n }\n}\n\n// =============================================================================\n// BASH COMMAND EXECUTION HELPER\n// =============================================================================\n\n/**\n * Execute a bash command with circuit breaker protection.\n * Returns undefined if not a bash command (caller should route normally).\n *\n * Design: docs/design-bash-circuit-breaker.md\n *\n * Key difference from LLM circuit breaker:\n * - Only TIMEOUT counts as failure (non-zero exit codes are often legitimate)\n * - Hybrid protection: per-session + global circuit breakers\n */\nexport async function executeBashCommand(\n command: any,\n session: AgentSession,\n context: ServerCommandContext\n): Promise<RpcResponse | undefined> {\n const commandType = command.type;\n const sessionId = command.sessionId;\n\n // Not a bash command - let normal routing handle it\n if (commandType !== \"bash\") {\n return undefined;\n }\n\n const bashBreaker = context.getBashCircuitBreaker();\n const breakerCheck = bashBreaker.canExecute(sessionId);\n\n if (!breakerCheck.allowed) {\n return {\n id: command.id,\n type: \"response\" as const,\n command: commandType,\n success: false,\n error: breakerCheck.reason ?? \"Bash circuit breaker open\",\n };\n }\n\n try {\n const routed = context.routeSessionCommand(session, command, context.getSessionInfo);\n if (routed === undefined) {\n return undefined;\n }\n\n const response = await Promise.resolve(routed);\n\n // Determine if this was a timeout\n // Only TIMEOUT counts as failure - non-zero exit codes are legitimate\n if (response.success) {\n bashBreaker.recordSuccess(sessionId);\n } else {\n const errorMsg = (response.error ?? \"\").toLowerCase();\n if (errorMsg.includes(\"timeout\") || errorMsg.includes(\"timed out\")) {\n bashBreaker.recordTimeout(sessionId);\n } else {\n // Non-timeout error (e.g., exit code != 0) - record success\n // The command executed, just didn't succeed\n bashBreaker.recordSuccess(sessionId);\n }\n }\n\n return response;\n } catch (error) {\n // Exception during execution - check if timeout\n const errorMsg = error instanceof Error ? error.message.toLowerCase() : \"\";\n if (errorMsg.includes(\"timeout\") || errorMsg.includes(\"timed out\")) {\n bashBreaker.recordTimeout(sessionId);\n } else {\n // Spawn error or other - treat as error (not timeout)\n bashBreaker.recordSpawnError(sessionId);\n }\n throw error;\n }\n}\n\n// =============================================================================\n// HANDLER MAP\n// =============================================================================\n\nexport const serverCommandHandlers: Record<string, ServerCommandHandler> = {\n // Session lifecycle\n list_sessions: handleListSessions,\n create_session: handleCreateSession,\n delete_session: handleDeleteSession,\n switch_session: handleSwitchSession,\n // Persistence (ADR-0007)\n list_stored_sessions: handleListStoredSessions,\n load_session: handleLoadSession,\n // Metrics & health\n get_metrics: handleGetMetrics,\n health_check: handleHealthCheck,\n // Extension UI\n extension_ui_response: handleExtensionUIResponse,\n};\n\n// =============================================================================\n// ROUTING FUNCTION\n// =============================================================================\n\n/**\n * Route a server command to the appropriate handler.\n * Returns a response or undefined if no handler exists (unknown command).\n */\nexport function routeServerCommand(\n command: any,\n context: ServerCommandContext\n): Promise<RpcResponse> | RpcResponse | undefined {\n const handler = serverCommandHandlers[command.type];\n if (!handler) {\n return undefined;\n }\n return handler(command, context);\n}\n\n/**\n * Get list of supported server command types.\n */\nexport function getSupportedServerCommands(): string[] {\n return Object.keys(serverCommandHandlers);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server UI Context - implements ExtensionUIContext for remote clients.
|
|
3
|
+
*
|
|
4
|
+
* Extension UI requests (select, confirm, input, etc.) are:
|
|
5
|
+
* 1. Broadcast to all subscribed clients via extension_ui_request event
|
|
6
|
+
* 2. Tracked as pending requests in ExtensionUIManager
|
|
7
|
+
* 3. Resolved when a client sends extension_ui_response command
|
|
8
|
+
*
|
|
9
|
+
* This enables skills, prompt templates, and custom tools that need user input
|
|
10
|
+
* to work over the WebSocket/stdio transport.
|
|
11
|
+
*/
|
|
12
|
+
import type { ExtensionUIContext } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
import type { ExtensionUIManager } from "./extension-ui.js";
|
|
14
|
+
/**
|
|
15
|
+
* Create an ExtensionUIContext that routes UI requests to remote clients.
|
|
16
|
+
*
|
|
17
|
+
* @param sessionId The session ID for request routing
|
|
18
|
+
* @param extensionUI The manager that tracks pending requests
|
|
19
|
+
* @param broadcast Function to broadcast events to subscribers
|
|
20
|
+
*/
|
|
21
|
+
export declare function createServerUIContext(sessionId: string, extensionUI: ExtensionUIManager, broadcast: (sessionId: string, event: any) => void): ExtensionUIContext;
|
|
22
|
+
//# sourceMappingURL=server-ui-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-ui-context.d.ts","sourceRoot":"","sources":["../src/server-ui-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAwB,MAAM,+BAA+B,CAAC;AAC9F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAQ5D;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,kBAAkB,EAC/B,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,IAAI,GACjD,kBAAkB,CAqQpB"}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isSelectResponse,
|
|
3
|
+
isConfirmResponse,
|
|
4
|
+
isInputResponse,
|
|
5
|
+
isEditorResponse
|
|
6
|
+
} from "./extension-ui.js";
|
|
7
|
+
function createServerUIContext(sessionId, extensionUI, broadcast) {
|
|
8
|
+
return {
|
|
9
|
+
async select(title, options, opts) {
|
|
10
|
+
const request = extensionUI.createPendingRequest(sessionId, "select", {
|
|
11
|
+
title,
|
|
12
|
+
options,
|
|
13
|
+
timeout: opts?.timeout
|
|
14
|
+
});
|
|
15
|
+
if (!request) return void 0;
|
|
16
|
+
extensionUI.broadcastUIRequest(sessionId, request.requestId, "select", {
|
|
17
|
+
title,
|
|
18
|
+
options,
|
|
19
|
+
timeout: opts?.timeout
|
|
20
|
+
});
|
|
21
|
+
try {
|
|
22
|
+
const response = await raceWithAbortAndSignal(
|
|
23
|
+
request.promise,
|
|
24
|
+
opts?.signal,
|
|
25
|
+
() => extensionUI.cancelRequest(request.requestId)
|
|
26
|
+
);
|
|
27
|
+
if (response.method === "cancelled") return void 0;
|
|
28
|
+
if (isSelectResponse(response)) return response.value;
|
|
29
|
+
return void 0;
|
|
30
|
+
} catch {
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
async confirm(title, message, opts) {
|
|
35
|
+
const request = extensionUI.createPendingRequest(sessionId, "confirm", {
|
|
36
|
+
title,
|
|
37
|
+
message,
|
|
38
|
+
timeout: opts?.timeout
|
|
39
|
+
});
|
|
40
|
+
if (!request) return false;
|
|
41
|
+
extensionUI.broadcastUIRequest(sessionId, request.requestId, "confirm", {
|
|
42
|
+
title,
|
|
43
|
+
message,
|
|
44
|
+
timeout: opts?.timeout
|
|
45
|
+
});
|
|
46
|
+
try {
|
|
47
|
+
const response = await raceWithAbortAndSignal(
|
|
48
|
+
request.promise,
|
|
49
|
+
opts?.signal,
|
|
50
|
+
() => extensionUI.cancelRequest(request.requestId)
|
|
51
|
+
);
|
|
52
|
+
if (response.method === "cancelled") return false;
|
|
53
|
+
if (isConfirmResponse(response)) return response.confirmed;
|
|
54
|
+
return false;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
async input(title, placeholder, opts) {
|
|
60
|
+
const request = extensionUI.createPendingRequest(sessionId, "input", {
|
|
61
|
+
title,
|
|
62
|
+
placeholder,
|
|
63
|
+
timeout: opts?.timeout
|
|
64
|
+
});
|
|
65
|
+
if (!request) return void 0;
|
|
66
|
+
extensionUI.broadcastUIRequest(sessionId, request.requestId, "input", {
|
|
67
|
+
title,
|
|
68
|
+
placeholder,
|
|
69
|
+
timeout: opts?.timeout
|
|
70
|
+
});
|
|
71
|
+
try {
|
|
72
|
+
const response = await raceWithAbortAndSignal(
|
|
73
|
+
request.promise,
|
|
74
|
+
opts?.signal,
|
|
75
|
+
() => extensionUI.cancelRequest(request.requestId)
|
|
76
|
+
);
|
|
77
|
+
if (response.method === "cancelled") return void 0;
|
|
78
|
+
if (isInputResponse(response)) return response.value;
|
|
79
|
+
return void 0;
|
|
80
|
+
} catch {
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
async editor(title, prefill) {
|
|
85
|
+
const request = extensionUI.createPendingRequest(sessionId, "editor", {
|
|
86
|
+
title,
|
|
87
|
+
prefill
|
|
88
|
+
});
|
|
89
|
+
if (!request) return void 0;
|
|
90
|
+
extensionUI.broadcastUIRequest(sessionId, request.requestId, "editor", {
|
|
91
|
+
title,
|
|
92
|
+
prefill
|
|
93
|
+
});
|
|
94
|
+
try {
|
|
95
|
+
const response = await raceWithAbortAndSignal(
|
|
96
|
+
request.promise,
|
|
97
|
+
void 0,
|
|
98
|
+
// editor doesn't typically use abort signal
|
|
99
|
+
() => extensionUI.cancelRequest(request.requestId)
|
|
100
|
+
);
|
|
101
|
+
if (response.method === "cancelled") return void 0;
|
|
102
|
+
if (isEditorResponse(response)) return response.value;
|
|
103
|
+
return void 0;
|
|
104
|
+
} catch {
|
|
105
|
+
return void 0;
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
notify(message, type) {
|
|
109
|
+
broadcast(sessionId, {
|
|
110
|
+
type: "extension_ui_request",
|
|
111
|
+
requestId: `notify-${Date.now()}`,
|
|
112
|
+
// Ephemeral, no response expected
|
|
113
|
+
method: "notify",
|
|
114
|
+
message,
|
|
115
|
+
notifyType: type ?? "info"
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
onTerminalInput(_handler) {
|
|
119
|
+
return () => {
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
setStatus(key, text) {
|
|
123
|
+
broadcast(sessionId, {
|
|
124
|
+
type: "extension_ui_request",
|
|
125
|
+
requestId: `status-${key}-${Date.now()}`,
|
|
126
|
+
method: "setStatus",
|
|
127
|
+
key,
|
|
128
|
+
text
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
setWorkingMessage(message) {
|
|
132
|
+
broadcast(sessionId, {
|
|
133
|
+
type: "extension_ui_request",
|
|
134
|
+
requestId: `working-${Date.now()}`,
|
|
135
|
+
method: "setWorkingMessage",
|
|
136
|
+
message
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
setWidget(key, content, options) {
|
|
140
|
+
if (Array.isArray(content) || content === void 0) {
|
|
141
|
+
broadcast(sessionId, {
|
|
142
|
+
type: "extension_ui_request",
|
|
143
|
+
requestId: `widget-${key}-${Date.now()}`,
|
|
144
|
+
method: "setWidget",
|
|
145
|
+
key,
|
|
146
|
+
content,
|
|
147
|
+
placement: options?.placement
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
setFooter(_factory) {
|
|
152
|
+
},
|
|
153
|
+
setHeader(_factory) {
|
|
154
|
+
},
|
|
155
|
+
setTitle(title) {
|
|
156
|
+
broadcast(sessionId, {
|
|
157
|
+
type: "extension_ui_request",
|
|
158
|
+
requestId: `title-${Date.now()}`,
|
|
159
|
+
method: "setTitle",
|
|
160
|
+
title
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
async custom(_factory, _options) {
|
|
164
|
+
throw new Error("Custom components are not supported in server mode");
|
|
165
|
+
},
|
|
166
|
+
pasteToEditor(_text) {
|
|
167
|
+
},
|
|
168
|
+
setEditorText(_text) {
|
|
169
|
+
},
|
|
170
|
+
getEditorText() {
|
|
171
|
+
return "";
|
|
172
|
+
},
|
|
173
|
+
setEditorComponent(_factory) {
|
|
174
|
+
},
|
|
175
|
+
// Theme methods - return stubs since themes are TUI-specific
|
|
176
|
+
get theme() {
|
|
177
|
+
return {
|
|
178
|
+
// Minimal theme stub for extensions that check theme properties
|
|
179
|
+
colors: {},
|
|
180
|
+
styles: {}
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
getAllThemes() {
|
|
184
|
+
return [];
|
|
185
|
+
},
|
|
186
|
+
getTheme(_name) {
|
|
187
|
+
return void 0;
|
|
188
|
+
},
|
|
189
|
+
setTheme(_theme) {
|
|
190
|
+
return { success: false, error: "Themes not supported in server mode" };
|
|
191
|
+
},
|
|
192
|
+
getToolsExpanded() {
|
|
193
|
+
return false;
|
|
194
|
+
},
|
|
195
|
+
setToolsExpanded(_expanded) {
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
async function raceWithAbortAndSignal(promise, signal, onCancel) {
|
|
200
|
+
if (!signal) {
|
|
201
|
+
return promise;
|
|
202
|
+
}
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
const abortHandler = () => {
|
|
205
|
+
onCancel();
|
|
206
|
+
reject(new Error("Aborted"));
|
|
207
|
+
};
|
|
208
|
+
signal.addEventListener("abort", abortHandler);
|
|
209
|
+
promise.then((result) => {
|
|
210
|
+
signal.removeEventListener("abort", abortHandler);
|
|
211
|
+
resolve(result);
|
|
212
|
+
}).catch((error) => {
|
|
213
|
+
signal.removeEventListener("abort", abortHandler);
|
|
214
|
+
reject(error);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
export {
|
|
219
|
+
createServerUIContext
|
|
220
|
+
};
|
|
221
|
+
//# sourceMappingURL=server-ui-context.js.map
|