fix: E2E-Befunde vm-test 2.0.16 eingearbeitet (az-fleet-7x8)
- Message-Teile liegen im Feld content (nicht parts — SDK-Typen sagen parts; beide werden gelesen) - Persistenz-Lag: Tool-Part ist zur Evaluations-Zeit ggf. noch nicht im Message-Kontext — Text-Parts der Call-Message gelten dann als Erklärung (liegen per Definition vor dem Call) - execute.before-Event: Tool-Call-ID im Feld id (callID-Fallback bleibt) plus messageID/agent — Shape per Diagnose-Plugin verifiziert - Event-Namen im Stream: permission.asked/replied (präfix-tolerant) - inspectCallContext mit DEBUG-Instrumentation (nMsgs/msgFound/partTypes) E2E vm-test: plugin-loaded 2.0.0, Config-Lesung (Managed+Global+Projekt, V1-Map), System-Regel je Model-Request (Modell erklärt im Zitat-Block- Format), evaluate allow+ask (bash→shell-Normalisierung), ask + ask.explained mit echter Modell-Erklärung. Mock-Tests 17+5 grün.
This commit is contained in:
@@ -491,9 +491,12 @@ function recordReplied(props) {
|
||||
|
||||
/**
|
||||
* Den Message-Turn untersuchen, der den Tool-Call enthält.
|
||||
* V2: ctx.session.context({sessionID}) liefert die Messages; Assistant-Parts
|
||||
* sind u. a. {type:"text", text} und {type:"tool", id, name, state} — die
|
||||
* Tool-Call-ID des Permission-Source (source.id) matcht die Part-ID.
|
||||
* V2: ctx.session.context({sessionID}) liefert die Messages; die Message-Teile
|
||||
* liegen im Feld `content` (verifiziert vm-test 2.0.16, az-fleet-7x8 — ältere
|
||||
* SDK-Typen nennen es `parts`, beides wird gelesen) mit {type:"text", text}
|
||||
* und {type:"tool", id, …}. Die Tool-Call-ID des Permission-Source (source.id)
|
||||
* matcht die Tool-Part-ID (gleiches tooluse_-Format); trägt der Tool-Part
|
||||
* keine ID, fällt die Suche auf den ersten Tool-Part der Message zurück.
|
||||
* Liefert { found: true, explanation } mit den Text-Parts vor dem Tool-Part,
|
||||
* { found: true } ohne Erklärung, oder { found: false }, wenn Call oder API
|
||||
* nicht verfügbar sind — Caller behandeln found:false als „niemals blocken,
|
||||
@@ -502,13 +505,22 @@ function recordReplied(props) {
|
||||
async function inspectCallContext(ctx, sessionID, callID, messageID) {
|
||||
try {
|
||||
const messages = await ctx.session.context({ sessionID });
|
||||
if (!Array.isArray(messages)) return { found: false };
|
||||
if (!Array.isArray(messages)) {
|
||||
if (DEBUG) debugLog("inspect", { callID, result: "kein Nachrichten-Array" });
|
||||
return { found: false };
|
||||
}
|
||||
const dbg = { callID, nMsgs: messages.length };
|
||||
|
||||
const isToolMatch = (part) =>
|
||||
part?.type === "tool" && (part.id === callID || part.callID === callID);
|
||||
|
||||
const scan = (parts) => {
|
||||
const toolIndex = parts.findIndex(isToolMatch);
|
||||
const scan = (parts, allowFallback) => {
|
||||
let toolIndex = parts.findIndex(isToolMatch);
|
||||
// Fallback (nur im messageID-Zweig, wo die Message feststeht): trägt der
|
||||
// Tool-Part keine ID, den ersten Tool-Part derselben Message nehmen.
|
||||
if (toolIndex === -1 && allowFallback && parts.some((p) => p?.type === "tool")) {
|
||||
toolIndex = parts.findIndex((p) => p?.type === "tool");
|
||||
}
|
||||
if (toolIndex === -1) return undefined;
|
||||
const texts = [];
|
||||
for (let i = 0; i < toolIndex; i++) {
|
||||
@@ -520,22 +532,49 @@ async function inspectCallContext(ctx, sessionID, callID, messageID) {
|
||||
return { found: true, explanation: texts.length ? texts.join("\n") : undefined };
|
||||
};
|
||||
|
||||
const messageParts = (msg) => {
|
||||
if (Array.isArray(msg?.parts)) return msg.parts;
|
||||
if (Array.isArray(msg?.content)) return msg.content;
|
||||
return null;
|
||||
};
|
||||
|
||||
// Bevorzugt die Message aus dem Permission-Source (messageID), Fallback:
|
||||
// alle Messages scannen.
|
||||
// alle Messages scannen (dort ohne Approximation — nur exakte Treffer).
|
||||
if (messageID) {
|
||||
const msg = messages.find((m) => m?.id === messageID);
|
||||
if (Array.isArray(msg?.parts)) {
|
||||
const hit = scan(msg.parts);
|
||||
if (hit) return hit;
|
||||
const parts = msg ? messageParts(msg) : null;
|
||||
dbg.msgFound = !!msg;
|
||||
dbg.partTypes = parts ? parts.map((p) => p.type).join(",") : null;
|
||||
if (parts) {
|
||||
const hit = scan(parts, true);
|
||||
if (hit) {
|
||||
if (DEBUG) debugLog("inspect", { ...dbg, via: "messageID+scan", explained: !!hit.explanation });
|
||||
return hit;
|
||||
}
|
||||
// Persistenz-Lag (vm-test 2.0.16): Zur Evaluations-Zeit ist der
|
||||
// Tool-Part im Message-Kontext ggf. noch nicht sichtbar. Die Message
|
||||
// gehört aber genau zu diesem Call — alle ihre Text-Parts liegen per
|
||||
// Definition VOR dem Call (der Call löst die Evaluation aus).
|
||||
const texts = parts
|
||||
.filter((p) => p?.type === "text" && typeof p.text === "string" && p.text.trim())
|
||||
.map((p) => p.text.trim());
|
||||
if (DEBUG) debugLog("inspect", { ...dbg, via: "messageID+lag", explained: texts.length > 0 });
|
||||
return { found: true, explanation: texts.length ? texts.join("\n") : undefined };
|
||||
}
|
||||
}
|
||||
for (const msg of messages) {
|
||||
if (!Array.isArray(msg?.parts)) continue;
|
||||
const hit = scan(msg.parts);
|
||||
if (hit) return hit;
|
||||
const parts = messageParts(msg);
|
||||
if (!parts) continue;
|
||||
const hit = scan(parts, false);
|
||||
if (hit) {
|
||||
if (DEBUG) debugLog("inspect", { ...dbg, via: "global-scan", explained: !!hit.explanation });
|
||||
return hit;
|
||||
}
|
||||
}
|
||||
if (DEBUG) debugLog("inspect", { ...dbg, via: "not-found" });
|
||||
return { found: false };
|
||||
} catch {
|
||||
} catch (err) {
|
||||
if (DEBUG) debugLog("inspect", { callID, error: String(err) });
|
||||
return { found: false };
|
||||
}
|
||||
}
|
||||
@@ -645,7 +684,7 @@ async function setup(ctx) {
|
||||
await ctx.tool.hook("execute.before", async (event) => {
|
||||
const callID = event?.callID ?? event?.id;
|
||||
if (!callID || !askedCallIDs.has(callID)) return;
|
||||
const result = await inspectCallContext(ctx, event.sessionID, callID, undefined);
|
||||
const result = await inspectCallContext(ctx, event.sessionID, callID, event.messageID);
|
||||
if (!result.found || result.explanation) return;
|
||||
askedCallIDs.delete(callID);
|
||||
throw new Error(EXPLAIN_FIRST_ERROR);
|
||||
|
||||
Reference in New Issue
Block a user