feat: harden workflow gates and expose token usage
This commit is contained in:
@@ -7,11 +7,20 @@ export type ToolActivity = {
|
||||
status: "running" | "completed" | "failed";
|
||||
};
|
||||
|
||||
export type TokenUsage = {
|
||||
input: number;
|
||||
cacheRead: number;
|
||||
output: number;
|
||||
totalTokens: number;
|
||||
contextWindow: number;
|
||||
};
|
||||
|
||||
export type ClientEvent =
|
||||
| { type: "ui_request"; ui_request: any }
|
||||
| { type: "text_delta"; text: string }
|
||||
| { type: "activity_delta"; text: string }
|
||||
| { type: "activity_event"; activity: ToolActivity }
|
||||
| { type: "usage"; usage: TokenUsage }
|
||||
| { type: "info"; [k: string]: any }
|
||||
| { type: "system_event"; event: string };
|
||||
|
||||
@@ -25,6 +34,7 @@ export class SessionBridge {
|
||||
// del descriptor (che viaggia opaco nel `title`). Va memorizzato e rimandato indietro,
|
||||
// altrimenti Pi scarta la risposta e ctx.ui.input non si risolve mai (stuck senza output).
|
||||
private pendingPiId: string | null = null;
|
||||
private contextWindow = 0;
|
||||
private cbs = new Set<(e: ClientEvent) => void>();
|
||||
|
||||
constructor(private rpc: RpcClient) {
|
||||
@@ -36,17 +46,16 @@ export class SessionBridge {
|
||||
this.pendingPiId = m.id as string | null;
|
||||
this.state = "waiting";
|
||||
this.fan({ type: "ui_request", ui_request: descriptor });
|
||||
} else if (
|
||||
m.type === "message_end" &&
|
||||
m.message?.role === "assistant" &&
|
||||
m.message?.stopReason === "error"
|
||||
) {
|
||||
this.markFailed();
|
||||
this.fan({
|
||||
type: "info",
|
||||
level: "error",
|
||||
text: "Model request failed. Check provider connectivity, then Resume the session.",
|
||||
});
|
||||
} else if (m.type === "message_end" && m.message?.role === "assistant") {
|
||||
this.emitUsage(m.message.usage);
|
||||
if (m.message.stopReason === "error") {
|
||||
this.markFailed();
|
||||
this.fan({
|
||||
type: "info",
|
||||
level: "error",
|
||||
text: "Model request failed. Check provider connectivity, then Resume the session.",
|
||||
});
|
||||
}
|
||||
} else if (m.type === "extension_ui_request" && m.method === "notify") {
|
||||
this.fan({ type: "info", level: m.notifyType ?? "info", text: m.message ?? "" });
|
||||
} else if (m.type === "message_update" && m.assistantMessageEvent?.type === "text_delta") {
|
||||
@@ -93,12 +102,38 @@ export class SessionBridge {
|
||||
});
|
||||
}
|
||||
|
||||
private emitUsage(usage: any): void {
|
||||
if (!usage || this.contextWindow <= 0) return;
|
||||
const number = (value: unknown): number =>
|
||||
typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
||||
const input = number(usage.input);
|
||||
const cacheRead = number(usage.cacheRead);
|
||||
const output = number(usage.output);
|
||||
const reportedTotal = number(usage.totalTokens);
|
||||
this.fan({
|
||||
type: "usage",
|
||||
usage: {
|
||||
input,
|
||||
cacheRead,
|
||||
output,
|
||||
totalTokens: reportedTotal || input + cacheRead + output,
|
||||
contextWindow: this.contextWindow,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private fan(e: ClientEvent) { for (const cb of this.cbs) cb(e); }
|
||||
|
||||
turnState(): TurnState { return this.state; }
|
||||
|
||||
beginTurn(): void { this.state = "running"; }
|
||||
|
||||
setContextWindow(value: unknown): void {
|
||||
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
||||
this.contextWindow = value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Record a backend-detected failure; callers own any sanitized client message. */
|
||||
markFailed(): void { this.state = "failed"; }
|
||||
|
||||
|
||||
@@ -166,7 +166,10 @@ export class PiProcessManager {
|
||||
const thinking = o.thinking ?? this.cfg.defaults.thinking;
|
||||
|
||||
if (provider && model) {
|
||||
await rt.rpc.request({ type: "set_model", provider, modelId: model } as object & { type: string });
|
||||
const response = await rt.rpc.request(
|
||||
{ type: "set_model", provider, modelId: model } as object & { type: string },
|
||||
);
|
||||
rt.bridge.setContextWindow(response?.data?.contextWindow);
|
||||
}
|
||||
if (thinking) {
|
||||
await rt.rpc.request({ type: "set_thinking_level", level: thinking } as object & { type: string });
|
||||
|
||||
Reference in New Issue
Block a user