@matthias-hausberger/beige 1.0.0-beta3 → 1.0.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/dist/channels/tui.d.ts +1 -3
- package/dist/channels/tui.d.ts.map +1 -1
- package/dist/channels/tui.js +288 -135
- package/dist/channels/tui.js.map +1 -1
- package/dist/cli.js +37 -89
- package/dist/cli.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +20 -3
- package/dist/config/loader.js.map +1 -1
- package/dist/gateway/agent-manager.d.ts +36 -0
- package/dist/gateway/agent-manager.d.ts.map +1 -1
- package/dist/gateway/agent-manager.js +88 -3
- package/dist/gateway/agent-manager.js.map +1 -1
- package/dist/gateway/api.d.ts +13 -0
- package/dist/gateway/api.d.ts.map +1 -1
- package/dist/gateway/api.js +365 -0
- package/dist/gateway/api.js.map +1 -1
- package/dist/gateway/gateway.d.ts.map +1 -1
- package/dist/gateway/gateway.js +43 -10
- package/dist/gateway/gateway.js.map +1 -1
- package/dist/plugins/index.d.ts +1 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +1 -0
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/installer.d.ts +23 -0
- package/dist/plugins/installer.d.ts.map +1 -1
- package/dist/plugins/installer.js +71 -11
- package/dist/plugins/installer.js.map +1 -1
- package/package.json +1 -1
package/dist/channels/tui.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../../src/channels/tui.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../../src/channels/tui.ts"],"names":[],"mappings":"AA0FA,MAAM,WAAW,UAAU;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAyBD,wBAAsB,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAmK/D"}
|
package/dist/channels/tui.js
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { createAgentSession, InteractiveMode, AuthStorage, ModelRegistry, SessionManager, SettingsManager, createExtensionRuntime, } from "@mariozechner/pi-coding-agent";
|
|
3
|
-
import {
|
|
3
|
+
import { createAssistantMessageEventStream, } from "@mariozechner/pi-ai";
|
|
4
4
|
import { resolve, basename } from "path";
|
|
5
5
|
import { mkdirSync } from "fs";
|
|
6
6
|
import { beigeDir } from "../paths.js";
|
|
7
7
|
import { buildSystemPrompt, readWorkspaceAgentsMd } from "../gateway/agent-manager.js";
|
|
8
8
|
import { SessionSettingsStore, resolveSessionSetting } from "../gateway/session-settings.js";
|
|
9
9
|
import { BeigeSessionStore } from "../gateway/sessions.js";
|
|
10
|
-
import { loadSkills, validateSkillDeps } from "../skills/registry.js";
|
|
11
10
|
import { RestrictedModelRegistry, buildAllowedModels } from "../config/restricted-model-registry.js";
|
|
12
11
|
/**
|
|
13
12
|
* TUI channel — runs in a separate process and connects to the gateway HTTP API.
|
|
14
13
|
*
|
|
15
14
|
* Architecture:
|
|
16
|
-
* -
|
|
17
|
-
* - Core tool execution (read/write/patch/exec) is proxied to the gateway
|
|
18
|
-
* - The gateway owns
|
|
15
|
+
* - LLM calls are proxied through the gateway (the only process that needs API keys)
|
|
16
|
+
* - Core tool execution (read/write/patch/exec) is also proxied to the gateway
|
|
17
|
+
* - The gateway owns auth, sandboxes, audit, and policy enforcement
|
|
18
|
+
* - The TUI never loads the config file and never needs API keys
|
|
19
19
|
*
|
|
20
|
-
* This gives you
|
|
20
|
+
* This gives you:
|
|
21
21
|
* - Full pi TUI (editor, streaming, model switching, compaction, history)
|
|
22
22
|
* - Sandboxed tool execution managed by the gateway
|
|
23
|
+
* - LLM calls routed through the gateway's auth + provider setup
|
|
23
24
|
*
|
|
24
25
|
* Commands:
|
|
25
26
|
* - /new — Start a fresh session (pi built-in, works correctly)
|
|
@@ -28,15 +29,10 @@ import { RestrictedModelRegistry, buildAllowedModels } from "../config/restricte
|
|
|
28
29
|
* - /beige-agent [name] — Switch to a different beige agent
|
|
29
30
|
* - /beige-verbose on|off — Toggle verbose tool-call notifications
|
|
30
31
|
* - /v on|off — Shorthand for /beige-verbose
|
|
31
|
-
*
|
|
32
|
-
* Note: Beige-specific commands use the "beige-" prefix to avoid conflicts
|
|
33
|
-
* with pi built-ins. /new is pi's built-in command which resets the session
|
|
34
|
-
* in-place — this is the correct approach (creating a new AgentSession would
|
|
35
|
-
* leave InteractiveMode with a stale reference).
|
|
36
32
|
*/
|
|
37
33
|
const DEFAULT_GATEWAY_URL = "http://127.0.0.1:7433";
|
|
34
|
+
// ── Entry point ──────────────────────────────────────────────────────────────
|
|
38
35
|
export async function launchTUI(opts) {
|
|
39
|
-
const { config } = opts;
|
|
40
36
|
const gatewayUrl = opts.gatewayUrl ?? DEFAULT_GATEWAY_URL;
|
|
41
37
|
// Verify gateway is reachable
|
|
42
38
|
try {
|
|
@@ -67,52 +63,80 @@ export async function launchTUI(opts) {
|
|
|
67
63
|
console.error(`[TUI] Unknown agent '${agentName}'. Available: ${agentNames.join(", ")}`);
|
|
68
64
|
process.exit(1);
|
|
69
65
|
}
|
|
70
|
-
// ── Auth (
|
|
71
|
-
// Use beige's own auth/models files so credentials are isolated from pi's
|
|
72
|
-
// ~/.pi/agent/auth.json. /login and /logout persist to ~/.beige/auth.json.
|
|
66
|
+
// ── Auth storage (no real keys — LLM calls go through gateway proxy) ──
|
|
73
67
|
const beigeAuthPath = resolve(beigeDir(), "auth.json");
|
|
74
68
|
const beigeModelsPath = resolve(beigeDir(), "models.json");
|
|
75
69
|
const authStorage = AuthStorage.create(beigeAuthPath);
|
|
76
|
-
for (const [provider, providerConfig] of Object.entries(config.llm.providers)) {
|
|
77
|
-
if (providerConfig.apiKey) {
|
|
78
|
-
authStorage.setRuntimeApiKey(provider, providerConfig.apiKey);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
70
|
const modelRegistry = new ModelRegistry(authStorage, beigeModelsPath);
|
|
82
|
-
// Register
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
71
|
+
// ── Register proxy providers ─────────────────────────────
|
|
72
|
+
// For each provider used by this agent, register a provider with a custom
|
|
73
|
+
// streamSimple that proxies LLM calls through the gateway. The gateway
|
|
74
|
+
// resolves the API key and forwards to the real LLM provider.
|
|
75
|
+
const modelsRes = await fetch(`${gatewayUrl}/api/agents/${encodeURIComponent(agentName)}/models`);
|
|
76
|
+
const { models: modelInfos } = (await modelsRes.json());
|
|
77
|
+
if (modelInfos.length === 0) {
|
|
78
|
+
console.error(`[TUI] No models found for agent '${agentName}'`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
// Group models by provider and register each provider with proxy streamSimple
|
|
82
|
+
const providerModels = new Map();
|
|
83
|
+
for (const m of modelInfos) {
|
|
84
|
+
const list = providerModels.get(m.provider) ?? [];
|
|
85
|
+
list.push(m);
|
|
86
|
+
providerModels.set(m.provider, list);
|
|
87
|
+
}
|
|
88
|
+
for (const [provider, providerModelInfos] of providerModels) {
|
|
89
|
+
// Set a dummy runtime key so the model appears "available" to pi's registry.
|
|
90
|
+
// The actual API key is resolved by the gateway proxy.
|
|
91
|
+
authStorage.setRuntimeApiKey(provider, "beige-gateway-proxy");
|
|
92
|
+
modelRegistry.registerProvider(provider, {
|
|
93
|
+
// api is required by ModelRegistry when streamSimple is provided.
|
|
94
|
+
// All models for a given provider share the same API protocol.
|
|
95
|
+
api: providerModelInfos[0].api,
|
|
96
|
+
// apiKey is required when defining models — the gateway resolves the
|
|
97
|
+
// real key; this placeholder just satisfies ModelRegistry validation.
|
|
98
|
+
apiKey: "beige-gateway-proxy",
|
|
99
|
+
baseUrl: modelInfos[0].baseUrl,
|
|
100
|
+
streamSimple: createProxyStreamSimple(gatewayUrl, () => agentName),
|
|
101
|
+
models: providerModelInfos.map((m) => ({
|
|
102
|
+
id: m.id,
|
|
103
|
+
name: m.name,
|
|
104
|
+
api: m.api,
|
|
105
|
+
reasoning: m.reasoning,
|
|
106
|
+
input: m.input,
|
|
107
|
+
cost: m.cost,
|
|
108
|
+
contextWindow: m.contextWindow,
|
|
109
|
+
maxTokens: m.maxTokens,
|
|
110
|
+
headers: m.headers,
|
|
111
|
+
compat: m.compat,
|
|
112
|
+
})),
|
|
113
|
+
});
|
|
91
114
|
}
|
|
92
|
-
// ── Load skills ────────────────────────────────────────────
|
|
93
|
-
const loadedSkills = await loadSkills(config);
|
|
94
115
|
// ── Create restricted model registry ──────────────────────
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
const allowedModels = buildAllowedModels(
|
|
116
|
+
const primaryRef = agentInfo.model;
|
|
117
|
+
const fallbackRefs = agentInfo.fallbackModels ?? [];
|
|
118
|
+
const allowedModels = buildAllowedModels(primaryRef, fallbackRefs);
|
|
98
119
|
const restrictedModelRegistry = new RestrictedModelRegistry(modelRegistry, allowedModels);
|
|
120
|
+
// ── Workspace dir ────────────────────────────────────────
|
|
121
|
+
const agentDir = resolve(beigeDir(), "agents", agentName);
|
|
122
|
+
const workspaceDir = agentInfo.workspaceDir ?? resolve(agentDir, "workspace");
|
|
99
123
|
// ── Shared state ──────────────────────────────────────────
|
|
100
124
|
const settingsStore = new SessionSettingsStore();
|
|
101
125
|
const sessionStore = new BeigeSessionStore();
|
|
102
126
|
const toolStartHandlerRef = { fn: undefined };
|
|
103
127
|
const state = {
|
|
104
128
|
agentName,
|
|
105
|
-
|
|
129
|
+
agentModelRef: primaryRef,
|
|
130
|
+
agentFallbackRefs: fallbackRefs,
|
|
131
|
+
workspaceDir,
|
|
106
132
|
session: null,
|
|
107
133
|
toolStartHandlerRef,
|
|
108
134
|
gatewayUrl,
|
|
109
|
-
config,
|
|
110
135
|
authStorage,
|
|
111
136
|
modelRegistry: restrictedModelRegistry,
|
|
112
137
|
underlyingModelRegistry: modelRegistry,
|
|
113
138
|
settingsStore,
|
|
114
139
|
sessionStore,
|
|
115
|
-
loadedSkills,
|
|
116
140
|
};
|
|
117
141
|
// Wire initial verbose state
|
|
118
142
|
const sessionKey = BeigeSessionStore.tuiKey(agentName);
|
|
@@ -121,9 +145,6 @@ export async function launchTUI(opts) {
|
|
|
121
145
|
toolStartHandlerRef.fn = makeTUIToolStartHandler();
|
|
122
146
|
}
|
|
123
147
|
// ── Build extension and create session ───────────────────
|
|
124
|
-
// systemPromptRef and agentsFilesRef are shared mutable refs read by the
|
|
125
|
-
// resource loader. They are updated by /beige-agent (switch agent) and by
|
|
126
|
-
// the session_switch handler (re-read AGENTS.md after /new).
|
|
127
148
|
const systemPromptRef = { value: "" };
|
|
128
149
|
const agentsFilesRef = { value: [] };
|
|
129
150
|
const extensionsResult = await buildBeigeExtension(state, agentNames, systemPromptRef, agentsFilesRef);
|
|
@@ -133,14 +154,14 @@ export async function launchTUI(opts) {
|
|
|
133
154
|
process.exit(1);
|
|
134
155
|
}
|
|
135
156
|
// ── Launch pi TUI ─────────────────────────────────────────
|
|
136
|
-
console.log(`[TUI] Agent: ${agentName} (${
|
|
137
|
-
// Show allowed models (for model switching)
|
|
157
|
+
console.log(`[TUI] Agent: ${agentName} (${primaryRef.provider}/${primaryRef.model})`);
|
|
138
158
|
const availableModels = state.modelRegistry.getAvailable();
|
|
139
159
|
if (availableModels.length > 1) {
|
|
140
160
|
const modelList = availableModels.map(m => `${m.provider}/${m.id}`).join(", ");
|
|
141
161
|
console.log(`[TUI] Allowed models: ${modelList}`);
|
|
142
162
|
}
|
|
143
163
|
console.log(`[TUI] Tools: ${agentInfo.tools.join(", ") || "(core only)"}`);
|
|
164
|
+
console.log(`[TUI] LLM: proxied via gateway (no local API keys needed)`);
|
|
144
165
|
console.log(`[TUI] Verbose: ${initialVerbose ? "on" : "off"} — use /beige-verbose on|off to toggle`);
|
|
145
166
|
console.log(`[TUI] Commands: /new, /beige-resume, /beige-sessions, /beige-agent <name>, /beige-verbose on|off (or /v on|off)`);
|
|
146
167
|
// Suppress pi's "update available" banner — beige manages its own update lifecycle.
|
|
@@ -148,6 +169,117 @@ export async function launchTUI(opts) {
|
|
|
148
169
|
const mode = new InteractiveMode(state.session, {});
|
|
149
170
|
await mode.run();
|
|
150
171
|
}
|
|
172
|
+
// ── LLM proxy ────────────────────────────────────────────────────────────────
|
|
173
|
+
/**
|
|
174
|
+
* Create a streamSimple function that proxies LLM calls through the gateway.
|
|
175
|
+
*
|
|
176
|
+
* The gateway resolves the API key, selects the correct stream function based
|
|
177
|
+
* on the model's api field, and streams AssistantMessageEvent objects back as
|
|
178
|
+
* newline-delimited JSON.
|
|
179
|
+
*/
|
|
180
|
+
function createProxyStreamSimple(gatewayUrl, getAgentName) {
|
|
181
|
+
return (model, context, options) => {
|
|
182
|
+
const stream = createAssistantMessageEventStream();
|
|
183
|
+
(async () => {
|
|
184
|
+
try {
|
|
185
|
+
const res = await fetch(`${gatewayUrl}/api/chat/stream`, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
headers: { "Content-Type": "application/json" },
|
|
188
|
+
body: JSON.stringify({
|
|
189
|
+
provider: model.provider,
|
|
190
|
+
modelId: model.id,
|
|
191
|
+
context,
|
|
192
|
+
agentName: getAgentName(),
|
|
193
|
+
sessionKey: `tui:${getAgentName()}:default`,
|
|
194
|
+
options: {
|
|
195
|
+
reasoning: options?.reasoning,
|
|
196
|
+
maxTokens: options?.maxTokens,
|
|
197
|
+
temperature: options?.temperature,
|
|
198
|
+
},
|
|
199
|
+
}),
|
|
200
|
+
signal: options?.signal,
|
|
201
|
+
});
|
|
202
|
+
if (!res.ok) {
|
|
203
|
+
const errorText = await res.text();
|
|
204
|
+
stream.push({
|
|
205
|
+
type: "error",
|
|
206
|
+
reason: "error",
|
|
207
|
+
error: makeErrorMessage(model, "error", `Gateway ${res.status}: ${errorText}`),
|
|
208
|
+
});
|
|
209
|
+
stream.end();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// Read newline-delimited JSON stream
|
|
213
|
+
const reader = res.body.getReader();
|
|
214
|
+
const decoder = new TextDecoder();
|
|
215
|
+
let buffer = "";
|
|
216
|
+
while (true) {
|
|
217
|
+
const { done, value } = await reader.read();
|
|
218
|
+
if (done)
|
|
219
|
+
break;
|
|
220
|
+
buffer += decoder.decode(value, { stream: true });
|
|
221
|
+
const lines = buffer.split("\n");
|
|
222
|
+
buffer = lines.pop() ?? "";
|
|
223
|
+
for (const line of lines) {
|
|
224
|
+
if (!line.trim())
|
|
225
|
+
continue;
|
|
226
|
+
const event = JSON.parse(line);
|
|
227
|
+
// ── Gateway-specific event types ─────────────────────────
|
|
228
|
+
if (event.type === "blocked") {
|
|
229
|
+
// The gateway's prePrompt hook blocked this message.
|
|
230
|
+
// Write to stderr so the user sees why nothing happened,
|
|
231
|
+
// then treat it as an abort so the agent loop stops cleanly.
|
|
232
|
+
process.stderr.write(`\r❌ ${event.reason ?? "Message blocked by plugin hook."}\n`);
|
|
233
|
+
stream.push({
|
|
234
|
+
type: "error",
|
|
235
|
+
reason: "aborted",
|
|
236
|
+
error: makeErrorMessage(model, "aborted"),
|
|
237
|
+
});
|
|
238
|
+
stream.end();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (event.type === "model_fallback") {
|
|
242
|
+
// Gateway transparently switched to a fallback model due to a
|
|
243
|
+
// rate limit — no action required, subsequent events will come
|
|
244
|
+
// from the new model and its metadata will be in the done event.
|
|
245
|
+
console.log(`[TUI] Gateway switched to fallback model: ${event.provider}/${event.modelId}`);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
stream.push(event);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
stream.end();
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
const isAborted = err.name === "AbortError";
|
|
255
|
+
stream.push({
|
|
256
|
+
type: "error",
|
|
257
|
+
reason: isAborted ? "aborted" : "error",
|
|
258
|
+
error: makeErrorMessage(model, isAborted ? "aborted" : "error", isAborted ? undefined : (err instanceof Error ? err.message : String(err))),
|
|
259
|
+
});
|
|
260
|
+
stream.end();
|
|
261
|
+
}
|
|
262
|
+
})();
|
|
263
|
+
return stream;
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/** Build a minimal AssistantMessage for error/abort events. */
|
|
267
|
+
function makeErrorMessage(model, stopReason, errorMessage) {
|
|
268
|
+
return {
|
|
269
|
+
role: "assistant",
|
|
270
|
+
content: [],
|
|
271
|
+
api: model.api,
|
|
272
|
+
provider: model.provider,
|
|
273
|
+
model: model.id,
|
|
274
|
+
usage: {
|
|
275
|
+
input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0,
|
|
276
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
277
|
+
},
|
|
278
|
+
stopReason,
|
|
279
|
+
errorMessage,
|
|
280
|
+
timestamp: Date.now(),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
151
283
|
// ── Session creation ──────────────────────────────────────────────────────────
|
|
152
284
|
/**
|
|
153
285
|
* Create a new pi session for the current agent in state.
|
|
@@ -159,31 +291,25 @@ export async function launchTUI(opts) {
|
|
|
159
291
|
* AgentSession.
|
|
160
292
|
*/
|
|
161
293
|
async function createSession(state, extensionsResult, systemPromptRef, agentsFilesRef) {
|
|
162
|
-
const { agentName,
|
|
163
|
-
// Fetch agent info from gateway
|
|
294
|
+
const { agentName, agentModelRef, gatewayUrl, authStorage, modelRegistry, underlyingModelRegistry, toolStartHandlerRef, workspaceDir } = state;
|
|
295
|
+
// Fetch agent info from gateway (for tool/skill context)
|
|
164
296
|
const agentsRes = await fetch(`${gatewayUrl}/api/agents`);
|
|
165
297
|
const { agents } = (await agentsRes.json());
|
|
166
298
|
const agentInfo = agents.find((a) => a.name === agentName);
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
// Pass a getter so tool calls always route to the currently-active agent,
|
|
174
|
-
// even after /beige-agent switches state.agentName.
|
|
299
|
+
// Resolve the model from the proxy registry
|
|
300
|
+
const model = underlyingModelRegistry.find(agentModelRef.provider, agentModelRef.model);
|
|
301
|
+
if (!model) {
|
|
302
|
+
throw new Error(`Model not found: ${agentModelRef.provider}/${agentModelRef.model}`);
|
|
303
|
+
}
|
|
304
|
+
// Pass a getter so tool calls always route to the currently-active agent
|
|
175
305
|
const coreTools = createProxyTools(() => state.agentName, gatewayUrl, toolStartHandlerRef);
|
|
176
|
-
// Use pre-built tool/skill context from the gateway
|
|
306
|
+
// Use pre-built tool/skill context from the gateway
|
|
177
307
|
const toolContext = agentInfo?.toolContext ?? "";
|
|
178
308
|
const skillContext = agentInfo?.skillContext ?? "";
|
|
179
|
-
// Populate the shared mutable ref with the initial system prompt.
|
|
180
309
|
systemPromptRef.value = buildSystemPrompt(agentName, toolContext, skillContext);
|
|
181
310
|
// Read workspace AGENTS.md so it's injected into the system prompt context.
|
|
182
|
-
const agentDir = resolve(beigeDir(), "agents", agentName);
|
|
183
|
-
const workspaceDir = agentConfig.workspaceDir ?? resolve(agentDir, "workspace");
|
|
184
311
|
agentsFilesRef.value = readWorkspaceAgentsMd(workspaceDir);
|
|
185
312
|
const sessionsDir = resolve(beigeDir(), "sessions", agentName);
|
|
186
|
-
// Always start a fresh session. Users can resume via /beige-resume.
|
|
187
313
|
const sessionManager = SessionManager.create(process.cwd(), sessionsDir);
|
|
188
314
|
const resourceLoader = {
|
|
189
315
|
getExtensions: () => extensionsResult,
|
|
@@ -191,16 +317,20 @@ async function createSession(state, extensionsResult, systemPromptRef, agentsFil
|
|
|
191
317
|
getPrompts: () => ({ prompts: [], diagnostics: [] }),
|
|
192
318
|
getThemes: () => ({ themes: [], diagnostics: [] }),
|
|
193
319
|
getAgentsFiles: () => ({ agentsFiles: agentsFilesRef.value }),
|
|
194
|
-
// Read from mutable ref so updates from /beige-agent are reflected.
|
|
195
320
|
getSystemPrompt: () => systemPromptRef.value,
|
|
196
321
|
getAppendSystemPrompt: () => [],
|
|
197
322
|
getPathMetadata: () => new Map(),
|
|
198
323
|
extendResources: () => { },
|
|
199
324
|
reload: async () => { },
|
|
200
325
|
};
|
|
326
|
+
// Find the GatewayModelInfo for the thinking level
|
|
327
|
+
const modelsRes = await fetch(`${gatewayUrl}/api/agents/${encodeURIComponent(agentName)}/models`);
|
|
328
|
+
const { models: modelInfos } = (await modelsRes.json());
|
|
329
|
+
const currentModelInfo = modelInfos.find((m) => m.id === agentModelRef.model && m.provider === agentModelRef.provider);
|
|
330
|
+
const thinkingLevel = (currentModelInfo?.thinkingLevel ?? "off");
|
|
201
331
|
const { session } = await createAgentSession({
|
|
202
332
|
model,
|
|
203
|
-
thinkingLevel
|
|
333
|
+
thinkingLevel,
|
|
204
334
|
tools: [],
|
|
205
335
|
customTools: coreTools,
|
|
206
336
|
sessionManager,
|
|
@@ -210,11 +340,9 @@ async function createSession(state, extensionsResult, systemPromptRef, agentsFil
|
|
|
210
340
|
}),
|
|
211
341
|
resourceLoader,
|
|
212
342
|
authStorage,
|
|
213
|
-
// Pass underlying registry for session creation
|
|
214
343
|
modelRegistry: modelRegistry.getUnderlying(),
|
|
215
344
|
});
|
|
216
345
|
// Replace the session's modelRegistry with our restricted version
|
|
217
|
-
// This ensures the TUI's model switcher only shows allowed models
|
|
218
346
|
session._modelRegistry = modelRegistry;
|
|
219
347
|
state.session = session;
|
|
220
348
|
}
|
|
@@ -272,7 +400,6 @@ async function buildBeigeExtension(state, availableAgents, systemPromptRef, agen
|
|
|
272
400
|
let index = parseInt(arg, 10) - 1;
|
|
273
401
|
if (isNaN(index) || index < 0 || index >= sessions.length) {
|
|
274
402
|
if (arg === "") {
|
|
275
|
-
// Show list if no arg provided
|
|
276
403
|
ctx.ui.notify(`Usage: /beige-resume <number>\n\nSessions:\n${sessions
|
|
277
404
|
.slice(0, 5)
|
|
278
405
|
.map((s, i) => ` ${i + 1}. ${basename(s.file)}`)
|
|
@@ -284,10 +411,6 @@ async function buildBeigeExtension(state, availableAgents, systemPromptRef, agen
|
|
|
284
411
|
return;
|
|
285
412
|
}
|
|
286
413
|
const targetSession = sessions[index];
|
|
287
|
-
// Delegate to the pi SDK's switchSession(), which loads the session
|
|
288
|
-
// file into the existing AgentSession and re-renders the chat UI.
|
|
289
|
-
// This avoids a stale-session-reference bug where InteractiveMode's
|
|
290
|
-
// internal this.session would still point to the old disposed session.
|
|
291
414
|
await ctx.switchSession(targetSession.file);
|
|
292
415
|
ctx.ui.notify(`📂 Resumed session: ${basename(targetSession.file)}`, "info");
|
|
293
416
|
};
|
|
@@ -306,66 +429,89 @@ async function buildBeigeExtension(state, availableAgents, systemPromptRef, agen
|
|
|
306
429
|
ctx.ui.notify(`Already using agent '${arg}'.`, "info");
|
|
307
430
|
return;
|
|
308
431
|
}
|
|
309
|
-
//
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
432
|
+
// Fetch new agent info from gateway
|
|
433
|
+
const agentsRes = await fetch(`${state.gatewayUrl}/api/agents`);
|
|
434
|
+
const { agents } = (await agentsRes.json());
|
|
435
|
+
const newAgentInfo = agents.find((a) => a.name === arg);
|
|
436
|
+
if (!newAgentInfo) {
|
|
437
|
+
ctx.ui.notify(`Agent '${arg}' not found on gateway.`, "error");
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
// Fetch model metadata for the new agent
|
|
441
|
+
const modelsRes = await fetch(`${state.gatewayUrl}/api/agents/${encodeURIComponent(arg)}/models`);
|
|
442
|
+
const { models: newModelInfos } = (await modelsRes.json());
|
|
443
|
+
if (newModelInfos.length === 0) {
|
|
444
|
+
ctx.ui.notify(`No models configured for agent '${arg}'.`, "error");
|
|
313
445
|
return;
|
|
314
446
|
}
|
|
315
|
-
//
|
|
447
|
+
// Register proxy providers for the new agent's models
|
|
448
|
+
const providerModels = new Map();
|
|
449
|
+
for (const m of newModelInfos) {
|
|
450
|
+
const list = providerModels.get(m.provider) ?? [];
|
|
451
|
+
list.push(m);
|
|
452
|
+
providerModels.set(m.provider, list);
|
|
453
|
+
}
|
|
454
|
+
for (const [provider, pModelInfos] of providerModels) {
|
|
455
|
+
state.authStorage.setRuntimeApiKey(provider, "beige-gateway-proxy");
|
|
456
|
+
state.underlyingModelRegistry.registerProvider(provider, {
|
|
457
|
+
api: pModelInfos[0].api,
|
|
458
|
+
apiKey: "beige-gateway-proxy",
|
|
459
|
+
baseUrl: pModelInfos[0].baseUrl,
|
|
460
|
+
streamSimple: createProxyStreamSimple(state.gatewayUrl, () => state.agentName),
|
|
461
|
+
models: pModelInfos.map((m) => ({
|
|
462
|
+
id: m.id,
|
|
463
|
+
name: m.name,
|
|
464
|
+
api: m.api,
|
|
465
|
+
reasoning: m.reasoning,
|
|
466
|
+
input: m.input,
|
|
467
|
+
cost: m.cost,
|
|
468
|
+
contextWindow: m.contextWindow,
|
|
469
|
+
maxTokens: m.maxTokens,
|
|
470
|
+
headers: m.headers,
|
|
471
|
+
compat: m.compat,
|
|
472
|
+
})),
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
// Update state
|
|
316
476
|
state.agentName = arg;
|
|
317
|
-
state.
|
|
318
|
-
|
|
319
|
-
|
|
477
|
+
state.agentModelRef = newAgentInfo.model;
|
|
478
|
+
state.agentFallbackRefs = newAgentInfo.fallbackModels ?? [];
|
|
479
|
+
state.workspaceDir = newAgentInfo.workspaceDir ?? resolve(beigeDir(), "agents", arg, "workspace");
|
|
480
|
+
// Update restricted model registry
|
|
481
|
+
const allowedModels = buildAllowedModels(state.agentModelRef, state.agentFallbackRefs);
|
|
320
482
|
state.modelRegistry = new RestrictedModelRegistry(state.underlyingModelRegistry, allowedModels);
|
|
321
483
|
if (state.session) {
|
|
322
484
|
state.session._modelRegistry = state.modelRegistry;
|
|
323
485
|
}
|
|
324
|
-
// Rebuild
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const agentInfo = agents.find((a) => a.name === arg);
|
|
330
|
-
// Use pre-built tool/skill context from the gateway (which has full tool manifests).
|
|
331
|
-
const toolContext = agentInfo?.toolContext ?? "";
|
|
332
|
-
const skillContext = agentInfo?.skillContext ?? "";
|
|
333
|
-
const newSystemPrompt = buildSystemPrompt(arg, toolContext, skillContext);
|
|
334
|
-
systemPromptRef.value = newSystemPrompt;
|
|
335
|
-
// Update agents files for the new agent's workspace.
|
|
336
|
-
const newAgentDir = resolve(beigeDir(), "agents", arg);
|
|
337
|
-
const newWorkspaceDir = newAgentConfig.workspaceDir ?? resolve(newAgentDir, "workspace");
|
|
338
|
-
agentsFilesRef.value = readWorkspaceAgentsMd(newWorkspaceDir);
|
|
486
|
+
// Rebuild system prompt
|
|
487
|
+
const toolContext = newAgentInfo.toolContext ?? "";
|
|
488
|
+
const skillContext = newAgentInfo.skillContext ?? "";
|
|
489
|
+
systemPromptRef.value = buildSystemPrompt(arg, toolContext, skillContext);
|
|
490
|
+
agentsFilesRef.value = readWorkspaceAgentsMd(state.workspaceDir);
|
|
339
491
|
if (state.session) {
|
|
340
|
-
// Trigger a full rebuild so pi's buildSystemPrompt picks up both the
|
|
341
|
-
// updated system prompt ref AND the fresh AGENTS.md from getAgentsFiles().
|
|
342
492
|
const toolNames = state.session.getActiveToolNames();
|
|
343
493
|
state.session._baseSystemPrompt = state.session._rebuildSystemPrompt(toolNames);
|
|
344
494
|
state.session.agent.setSystemPrompt(state.session._baseSystemPrompt);
|
|
345
495
|
}
|
|
346
|
-
// Update verbose handler
|
|
496
|
+
// Update verbose handler
|
|
347
497
|
const sessionKey = BeigeSessionStore.tuiKey(arg);
|
|
348
498
|
const verbose = resolveSessionSetting("verbose", false, undefined, state.settingsStore.get(sessionKey, "verbose"));
|
|
349
499
|
state.toolStartHandlerRef.fn = verbose ? makeTUIToolStartHandler() : undefined;
|
|
350
|
-
// Point
|
|
351
|
-
// ctx.newSession() creates the .jsonl file under ~/.beige/sessions/<new-agent>/
|
|
352
|
-
// instead of the original agent's directory.
|
|
500
|
+
// Point session manager at new agent's session directory
|
|
353
501
|
if (state.session) {
|
|
354
502
|
const newSessionsDir = resolve(beigeDir(), "sessions", arg);
|
|
355
503
|
mkdirSync(newSessionsDir, { recursive: true });
|
|
356
504
|
state.session.sessionManager.sessionDir = newSessionsDir;
|
|
357
505
|
}
|
|
358
|
-
// Switch to
|
|
359
|
-
// inherit the previous agent's model. setModel() validates the API key
|
|
360
|
-
// via the (already-updated) restricted model registry, records a model
|
|
361
|
-
// change event in the session journal, and re-clamps the thinking level.
|
|
506
|
+
// Switch to new agent's model
|
|
362
507
|
if (state.session) {
|
|
363
|
-
const newModel =
|
|
364
|
-
|
|
508
|
+
const newModel = state.underlyingModelRegistry.find(state.agentModelRef.provider, state.agentModelRef.model);
|
|
509
|
+
if (newModel) {
|
|
510
|
+
const newModelInfo = newModelInfos.find((m) => m.id === state.agentModelRef.model && m.provider === state.agentModelRef.provider);
|
|
511
|
+
await state.session.setModel(newModel);
|
|
512
|
+
}
|
|
365
513
|
}
|
|
366
|
-
// Start a fresh session
|
|
367
|
-
// ctx.newSession() resets the existing AgentSession in-place (no new
|
|
368
|
-
// instance), so InteractiveMode's internal reference stays valid.
|
|
514
|
+
// Start a fresh session
|
|
369
515
|
await ctx.newSession();
|
|
370
516
|
ctx.ui.notify(`🔄 Switched to agent '${arg}'.`, "info");
|
|
371
517
|
};
|
|
@@ -374,18 +520,51 @@ async function buildBeigeExtension(state, availableAgents, systemPromptRef, agen
|
|
|
374
520
|
path: "<beige-tui>",
|
|
375
521
|
resolvedPath: "<beige-tui>",
|
|
376
522
|
handlers: new Map([
|
|
377
|
-
// Re-read AGENTS.md on /new and session switches so that edits the
|
|
378
|
-
// agent made during the previous session are picked up.
|
|
379
523
|
["session_switch", [async () => {
|
|
380
|
-
|
|
381
|
-
const ws = state.agentConfig.workspaceDir ?? resolve(dir, "workspace");
|
|
382
|
-
agentsFilesRef.value = readWorkspaceAgentsMd(ws);
|
|
524
|
+
agentsFilesRef.value = readWorkspaceAgentsMd(state.workspaceDir);
|
|
383
525
|
if (state.session) {
|
|
384
526
|
const toolNames = state.session.getActiveToolNames();
|
|
385
527
|
state.session._baseSystemPrompt = state.session._rebuildSystemPrompt(toolNames);
|
|
386
528
|
state.session.agent.setSystemPrompt(state.session._baseSystemPrompt);
|
|
387
529
|
}
|
|
388
530
|
}]],
|
|
531
|
+
// prePrompt and postResponse hooks are now executed server-side inside
|
|
532
|
+
// the gateway's /api/chat/stream handler before/after the LLM call.
|
|
533
|
+
// This eliminates the extra HTTP round-trips and ensures hooks run in
|
|
534
|
+
// the same process as the gateway regardless of which channel triggered
|
|
535
|
+
// the call. The `blocked` stream event handles the abort case.
|
|
536
|
+
// sessionCreated hook: fires on initial session load and /new.
|
|
537
|
+
["session_start", [async () => {
|
|
538
|
+
try {
|
|
539
|
+
await fetch(`${state.gatewayUrl}/api/agents/${encodeURIComponent(state.agentName)}/hooks/session-created`, {
|
|
540
|
+
method: "POST",
|
|
541
|
+
headers: { "Content-Type": "application/json" },
|
|
542
|
+
body: JSON.stringify({
|
|
543
|
+
sessionKey: `tui:${state.agentName}:default`,
|
|
544
|
+
channel: "tui",
|
|
545
|
+
}),
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
// Non-fatal: hook failures should not block the TUI
|
|
550
|
+
}
|
|
551
|
+
}]],
|
|
552
|
+
// sessionDisposed hook: fires on process exit.
|
|
553
|
+
["session_shutdown", [async () => {
|
|
554
|
+
try {
|
|
555
|
+
await fetch(`${state.gatewayUrl}/api/agents/${encodeURIComponent(state.agentName)}/hooks/session-disposed`, {
|
|
556
|
+
method: "POST",
|
|
557
|
+
headers: { "Content-Type": "application/json" },
|
|
558
|
+
body: JSON.stringify({
|
|
559
|
+
sessionKey: `tui:${state.agentName}:default`,
|
|
560
|
+
channel: "tui",
|
|
561
|
+
}),
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
// Non-fatal: gateway may already be shutting down
|
|
566
|
+
}
|
|
567
|
+
}]],
|
|
389
568
|
]),
|
|
390
569
|
tools: new Map(),
|
|
391
570
|
messageRenderers: new Map(),
|
|
@@ -401,14 +580,6 @@ async function buildBeigeExtension(state, availableAgents, systemPromptRef, agen
|
|
|
401
580
|
};
|
|
402
581
|
return { extensions: [extension], errors: [], runtime };
|
|
403
582
|
}
|
|
404
|
-
/**
|
|
405
|
-
* List human-initiated sessions for an agent, sorted newest-first.
|
|
406
|
-
*
|
|
407
|
-
* Delegates to BeigeSessionStore.listSessions() so that sessions created
|
|
408
|
-
* by toolkit tools (e.g. agent-to-agent sub-agent sessions, which carry
|
|
409
|
-
* metadata.depth > 0) are automatically excluded. Only sessions the user
|
|
410
|
-
* started directly appear in /beige-sessions and /beige-resume.
|
|
411
|
-
*/
|
|
412
583
|
function listSessions(agentName) {
|
|
413
584
|
const store = new BeigeSessionStore();
|
|
414
585
|
return store.listSessions(agentName).map((info) => ({
|
|
@@ -525,22 +696,4 @@ function createProxyTools(getAgentName, gatewayUrl, handlerRef) {
|
|
|
525
696
|
},
|
|
526
697
|
];
|
|
527
698
|
}
|
|
528
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
529
|
-
function resolveModel(agentConfig, modelRegistry) {
|
|
530
|
-
const { provider, model: modelId } = agentConfig.model;
|
|
531
|
-
// Prefer ModelRegistry.find() over the static getModel() because the registry
|
|
532
|
-
// applies OAuth provider transformations (e.g. modifyModels) that update baseUrl.
|
|
533
|
-
// For GitHub Copilot business subscriptions, the OAuth provider rewrites baseUrl
|
|
534
|
-
// from api.individual.githubcopilot.com → api.business.githubcopilot.com based
|
|
535
|
-
// on the proxy-ep in the access token. Using the static getModel() would return
|
|
536
|
-
// the unmodified built-in model with the wrong baseUrl, causing 421 Misdirected
|
|
537
|
-
// Request errors.
|
|
538
|
-
const registryModel = modelRegistry.find(provider, modelId);
|
|
539
|
-
if (registryModel)
|
|
540
|
-
return registryModel;
|
|
541
|
-
const model = getModel(provider, modelId);
|
|
542
|
-
if (model)
|
|
543
|
-
return model;
|
|
544
|
-
throw new Error(`Model not found: ${provider}/${modelId}`);
|
|
545
|
-
}
|
|
546
699
|
//# sourceMappingURL=tui.js.map
|