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:
@@ -152,7 +152,7 @@ laden automatisch neu.
|
|||||||
| `permission.replied`-Event | `permission.replied` im öffentlichen Event-Stream (`ctx.event.subscribe`), Payload `{sessionID, requestID, reply}` — feuert nur bei echter Client-Antwort, NICHT bei Non-Interactive-Auto-Reject (vm-test 2.0.16) |
|
| `permission.replied`-Event | `permission.replied` im öffentlichen Event-Stream (`ctx.event.subscribe`), Payload `{sessionID, requestID, reply}` — feuert nur bei echter Client-Antwort, NICHT bei Non-Interactive-Auto-Reject (vm-test 2.0.16) |
|
||||||
| `experimental.chat.system.transform` | `ctx.session.hook("context")` → `event.system.push({type: "text", text})` — Agent-Loop inkl. Tool-Continuations |
|
| `experimental.chat.system.transform` | `ctx.session.hook("context")` → `event.system.push({type: "text", text})` — Agent-Loop inkl. Tool-Continuations |
|
||||||
| `tool.execute.before` (ENFORCE) | `ctx.tool.hook("execute.before")` — Event `{tool, sessionID, callID, input}` |
|
| `tool.execute.before` (ENFORCE) | `ctx.tool.hook("execute.before")` — Event `{tool, sessionID, callID, input}` |
|
||||||
| `client.session.messages({path:{id}})` | `ctx.session.context({sessionID})` — Tool-Parts tragen die Call-ID als `id` (matcht `source.id` der Permission-Evaluation) |
|
| `client.session.messages({path:{id}})` | `ctx.session.context({sessionID})` — Message-Teile im Feld `content` (2.0.16; `parts` wird tolerierend mitgelesen), Tool-Parts tragen die Call-ID als `id` (matcht `source.id` der Permission-Evaluation); im Flight befindliche Assistant-Messages sind bereits sichtbar |
|
||||||
| `plugin-loaded`-Audit-Eintrag beim Laden | in `setup()` (jetzt mit Plugin-ID, opencode-Version, Location) |
|
| `plugin-loaded`-Audit-Eintrag beim Laden | in `setup()` (jetzt mit Plugin-ID, opencode-Version, Location) |
|
||||||
|
|
||||||
Unverändert übernommen: Notification-Logik je Plattform (PowerShell-WinRT-AUMID,
|
Unverändert übernommen: Notification-Logik je Plattform (PowerShell-WinRT-AUMID,
|
||||||
|
|||||||
@@ -491,9 +491,12 @@ function recordReplied(props) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Den Message-Turn untersuchen, der den Tool-Call enthält.
|
* Den Message-Turn untersuchen, der den Tool-Call enthält.
|
||||||
* V2: ctx.session.context({sessionID}) liefert die Messages; Assistant-Parts
|
* V2: ctx.session.context({sessionID}) liefert die Messages; die Message-Teile
|
||||||
* sind u. a. {type:"text", text} und {type:"tool", id, name, state} — die
|
* liegen im Feld `content` (verifiziert vm-test 2.0.16, az-fleet-7x8 — ältere
|
||||||
* Tool-Call-ID des Permission-Source (source.id) matcht die Part-ID.
|
* 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,
|
* Liefert { found: true, explanation } mit den Text-Parts vor dem Tool-Part,
|
||||||
* { found: true } ohne Erklärung, oder { found: false }, wenn Call oder API
|
* { found: true } ohne Erklärung, oder { found: false }, wenn Call oder API
|
||||||
* nicht verfügbar sind — Caller behandeln found:false als „niemals blocken,
|
* 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) {
|
async function inspectCallContext(ctx, sessionID, callID, messageID) {
|
||||||
try {
|
try {
|
||||||
const messages = await ctx.session.context({ sessionID });
|
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) =>
|
const isToolMatch = (part) =>
|
||||||
part?.type === "tool" && (part.id === callID || part.callID === callID);
|
part?.type === "tool" && (part.id === callID || part.callID === callID);
|
||||||
|
|
||||||
const scan = (parts) => {
|
const scan = (parts, allowFallback) => {
|
||||||
const toolIndex = parts.findIndex(isToolMatch);
|
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;
|
if (toolIndex === -1) return undefined;
|
||||||
const texts = [];
|
const texts = [];
|
||||||
for (let i = 0; i < toolIndex; i++) {
|
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 };
|
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:
|
// Bevorzugt die Message aus dem Permission-Source (messageID), Fallback:
|
||||||
// alle Messages scannen.
|
// alle Messages scannen (dort ohne Approximation — nur exakte Treffer).
|
||||||
if (messageID) {
|
if (messageID) {
|
||||||
const msg = messages.find((m) => m?.id === messageID);
|
const msg = messages.find((m) => m?.id === messageID);
|
||||||
if (Array.isArray(msg?.parts)) {
|
const parts = msg ? messageParts(msg) : null;
|
||||||
const hit = scan(msg.parts);
|
dbg.msgFound = !!msg;
|
||||||
if (hit) return hit;
|
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) {
|
for (const msg of messages) {
|
||||||
if (!Array.isArray(msg?.parts)) continue;
|
const parts = messageParts(msg);
|
||||||
const hit = scan(msg.parts);
|
if (!parts) continue;
|
||||||
if (hit) return hit;
|
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 };
|
return { found: false };
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
if (DEBUG) debugLog("inspect", { callID, error: String(err) });
|
||||||
return { found: false };
|
return { found: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -645,7 +684,7 @@ async function setup(ctx) {
|
|||||||
await ctx.tool.hook("execute.before", async (event) => {
|
await ctx.tool.hook("execute.before", async (event) => {
|
||||||
const callID = event?.callID ?? event?.id;
|
const callID = event?.callID ?? event?.id;
|
||||||
if (!callID || !askedCallIDs.has(callID)) return;
|
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;
|
if (!result.found || result.explanation) return;
|
||||||
askedCallIDs.delete(callID);
|
askedCallIDs.delete(callID);
|
||||||
throw new Error(EXPLAIN_FIRST_ERROR);
|
throw new Error(EXPLAIN_FIRST_ERROR);
|
||||||
|
|||||||
Reference in New Issue
Block a user