Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/L0/_all/mod/_core/admin/views/agent/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ Current behavior:
- browser execution blocks are detected by the `_____javascript` separator
- `execution.js` runs browser-side JavaScript in an async wrapper; console logs and ordinary returned arrays or objects should both use the same YAML-first transcript serializer, with `log↓` or `info↓` or `warn↓` or `error↓` or related block labels for console output and `result↓` for returned values, falling back to JSON only when the lightweight YAML helper cannot serialize the shape
- `_core/admin/views/agent/store.js/evaluateAdminAssistantMessage` is the extension seam that evaluates a settled assistant message immediately before execution results are serialized into the admin `execution-output` follow-up turn; `_core/agent-chat` currently provides the first-party repeated-message detector there, emitting loop notices on the 2nd exact assistant-message send as `info`, on the 3rd send as `warn`, and on the 4th send onward as `error`
- `_core/agent-chat/runtime-hygiene.js` now owns the shared clone/apply/normalize/resolve/create helper flow that `store.js` uses to route admin `submit`, `assistant-response`, `history-compact`, `protocol-retry`, and `execution-output` messages through `_core/admin/views/agent/store.js/processAdminAgentMessage` without leaking hook mutations back into source history objects
- when an execution follow-up turn returns no assistant content, the runtime retries the same request once automatically before sending a short protocol-correction user message
- empty-response protocol-correction messages must not re-echo the prior execution output; they should tell the agent to continue from the execution output above or provide the user-facing answer
- loaded admin skills are passed through execution as typed runtime values, not pasted blindly into the prompt
Expand Down
137 changes: 111 additions & 26 deletions app/L0/_all/mod/_core/admin/views/agent/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ import * as agentView from "/mod/_core/admin/views/agent/view.js";
import {
mapManagerStateToAdminState
} from "/mod/_core/admin/views/agent/huggingface.js";
import { prependAssistantEvaluationLogs } from "/mod/_core/agent-chat/assistant-message-evaluation.js";
import {
normalizeAssistantEvaluationLogEntry,
prependAssistantEvaluationLogs
} from "/mod/_core/agent-chat/assistant-message-evaluation.js";
applyConversationMessage,
createProcessedConversationMessage,
normalizeAssistantEvaluation,
resolveProcessedConversationMessage
} from "/mod/_core/agent-chat/runtime-hygiene.js";
import { DEFAULT_MODEL_INPUT, DTYPE_OPTIONS, normalizeHuggingFaceModelInput } from "/mod/_core/huggingface/helpers.js";
import { getHuggingFaceManager } from "/mod/_core/huggingface/manager.js";
import { closeDialog, openDialog } from "/mod/_core/visual/forms/dialog.js";
Expand All @@ -30,6 +33,13 @@ import {

const huggingfaceManager = getHuggingFaceManager();

const processAdminAgentMessage = globalThis.space.extend(
import.meta,
async function processAdminAgentMessage(context = {}) {
return context;
}
);

function normalizePromptBudgetSingleMessageRatio(
value,
fallback = config.DEFAULT_ADMIN_CHAT_SETTINGS.promptBudgetRatios.singleMessage
Expand Down Expand Up @@ -210,15 +220,7 @@ const MAX_COMPACT_TRIM_ATTEMPTS = 4;
const evaluateAdminAssistantMessage = globalThis.space.extend(
import.meta,
async function evaluateAdminAssistantMessage(context = {}) {
return {
assistantContent: typeof context?.assistantContent === "string" ? context.assistantContent : "",
history: Array.isArray(context?.history) ? context.history : [],
logs: Array.isArray(context?.logs)
? context.logs.map((entry) => normalizeAssistantEvaluationLogEntry(entry)).filter(Boolean)
: [],
messageId: typeof context?.messageId === "string" ? context.messageId : "",
store: context?.store || null
};
return normalizeAssistantEvaluation(context);
}
);

Expand Down Expand Up @@ -1415,15 +1417,27 @@ const model = {
return true;
},

consumeNextQueuedSubmissionMessage() {
async consumeNextQueuedSubmissionMessage() {
if (!this.queuedSubmissions.length) {
return null;
}

const [snapshot, ...rest] = this.queuedSubmissions;
this.queuedSubmissions = rest;
return createMessage("user", snapshot.content, {
attachments: Array.isArray(snapshot.attachments) ? snapshot.attachments.slice() : []
return createProcessedConversationMessage({
content: snapshot.content,
context: {
draftSubmission: snapshot,
history: this.history,
phase: "submit",
store: this
},
createMessage,
options: {
attachments: Array.isArray(snapshot.attachments) ? snapshot.attachments.slice() : []
},
processMessage: processAdminAgentMessage,
role: "user"
});
},

Expand Down Expand Up @@ -2013,9 +2027,21 @@ const model = {
}

if (isAbortError(error) && this.stopRequested) {
const hasContent = Boolean(assistantMessage.content.trim());
let hasContent = Boolean(assistantMessage.content.trim());

if (hasContent) {
applyConversationMessage(
assistantMessage,
await resolveProcessedConversationMessage({
history: requestMessages,
message: assistantMessage,
phase: "assistant-response",
responseMeta,
processMessage: processAdminAgentMessage,
store: this
})
);
hasContent = Boolean(assistantMessage.content.trim());
await this.refreshPromptInputFromHistory(this.history);
await this.persistHistory({
immediate: true
Expand All @@ -2040,6 +2066,17 @@ const model = {
this.activeRequestController = null;
}

applyConversationMessage(
assistantMessage,
await resolveProcessedConversationMessage({
history: requestMessages,
message: assistantMessage,
phase: "assistant-response",
responseMeta,
processMessage: processAdminAgentMessage,
store: this
})
);
await this.refreshPromptInputFromHistory(this.history);
await this.persistHistory({
immediate: true
Expand Down Expand Up @@ -2138,8 +2175,20 @@ const model = {
throw new Error("History compaction returned no content.");
}

const compactedMessage = createMessage("user", normalizedCompactedHistory, {
kind: "history-compact"
const compactedMessage = await createProcessedConversationMessage({
content: normalizedCompactedHistory,
context: {
history: this.history,
mode,
phase: "history-compact",
store: this
},
createMessage,
options: {
kind: "history-compact"
},
processMessage: processAdminAgentMessage,
role: "user"
});
this.executionOutputOverrides = Object.create(null);
this.rerunningMessageId = "";
Expand Down Expand Up @@ -2319,8 +2368,20 @@ const model = {
continue;
}

nextUserMessage = createMessage("user", buildEmptyAssistantRetryMessage(), {
kind: "execution-retry"
nextUserMessage = await createProcessedConversationMessage({
content: buildEmptyAssistantRetryMessage(),
context: {
history: requestMessages,
phase: "protocol-retry",
responseMeta: streamResult.responseMeta,
store: this
},
createMessage,
options: {
kind: "execution-retry"
},
processMessage: processAdminAgentMessage,
role: "user"
});
this.status = hasVerifiedEmptyAssistantResponse(streamResult)
? "Retrying: assistant response was empty after execution..."
Expand Down Expand Up @@ -2360,8 +2421,20 @@ const model = {
return "complete";
}

const executionOutputMessage = createMessage("user", execution.formatExecutionResultsMessage(executionResults), {
kind: "execution-output"
const executionOutputMessage = await createProcessedConversationMessage({
content: execution.formatExecutionResultsMessage(executionResults),
context: {
executionResults,
history: this.history,
phase: "execution-output",
store: this
},
createMessage,
options: {
kind: "execution-output"
},
processMessage: processAdminAgentMessage,
role: "user"
});
await this.replaceHistory([...this.history, executionOutputMessage]);
await this.persistHistory({
Expand Down Expand Up @@ -2395,7 +2468,7 @@ const model = {
finalOutcome = outcome;

if (outcome === "queued") {
nextUserMessage = this.consumeNextQueuedSubmissionMessage();
nextUserMessage = await this.consumeNextQueuedSubmissionMessage();

if (nextUserMessage) {
this.stopRequested = false;
Expand All @@ -2412,7 +2485,7 @@ const model = {
break;
}

const queuedMessage = this.consumeNextQueuedSubmissionMessage();
const queuedMessage = await this.consumeNextQueuedSubmissionMessage();

if (queuedMessage) {
nextUserMessage = queuedMessage;
Expand Down Expand Up @@ -2474,8 +2547,20 @@ const model = {
}
}

const userMessage = createMessage("user", draftSubmission.content, {
attachments: draftSubmission.attachments
const userMessage = await createProcessedConversationMessage({
content: draftSubmission.content,
context: {
draftSubmission,
history: this.history,
phase: "submit",
store: this
},
createMessage,
options: {
attachments: draftSubmission.attachments
},
processMessage: processAdminAgentMessage,
role: "user"
});
this.clearComposerDraft();
await this.runSubmissionSeries(userMessage);
Expand Down
7 changes: 6 additions & 1 deletion app/L0/_all/mod/_core/agent-chat/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Documentation is top priority for this module. After any change under `_core/age
This module owns:

- `assistant-message-evaluation.js`: shared assistant-message normalization, exact-repeat detection, severity-based loop warning construction, and safe prepending of synthetic transcript logs ahead of real execution console output
- `runtime-hygiene.js`: shared conversation-message cloning, safe in-place message application, assistant-evaluation context normalization, and processed-message helper flow reused by the admin and overlay agent stores
- `ext/js/_core/onscreen_agent/store.js/evaluateOnscreenAssistantMessage/end/*.js`: hook implementations for overlay assistant-message evaluation
- `ext/js/_core/admin/views/agent/store.js/evaluateAdminAssistantMessage/end/*.js`: hook implementations for admin assistant-message evaluation

Expand All @@ -25,7 +26,11 @@ Current shared helper contract:
- severity must escalate as `info` on the 2nd exact send, `warn` on the 3rd exact send, and `error` on the 4th exact send onward
- the warning text should stay short, direct, and framed as loop pressure visible through the normal execution transcript channel
- prepending synthetic transcript logs must not rewrite or trim the real execution console entries that already exist on the execution result
- this module owns the first-party repeated-message loop policy for both overlay and admin chat; the consuming stores own only the evaluation seams and transcript insertion point
- `runtime-hygiene.js` should clone conversation messages before passing them into message-processing hooks, so hook mutations cannot leak back into the source history snapshots by aliasing
- `runtime-hygiene.js` should expose `applyConversationMessage(...)` for the stores that need to merge processed assistant-message updates back into existing history objects without sharing attachment-array references
- `normalizeAssistantEvaluation(...)` should sanitize assistant-evaluation inputs into the shared `{ assistantContent, history, logs, messageId, store }` shape for both chat surfaces
- `resolveProcessedConversationMessage(...)` and `createProcessedConversationMessage(...)` should be the shared way to route `submit`, `assistant-response`, `history-compact`, `protocol-retry`, and `execution-output` messages through surface-owned processing seams
- this module owns the first-party repeated-message loop policy for both overlay and admin chat, plus the shared runtime-hygiene helper flow; the consuming stores own only the evaluation seams, transcript insertion point, and surface-local message-processing policy

## Development Guidance

Expand Down
76 changes: 76 additions & 0 deletions app/L0/_all/mod/_core/agent-chat/runtime-hygiene.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { normalizeAssistantEvaluationLogEntry } from "./assistant-message-evaluation.js";

function cloneConversationAttachments(attachments) {
return Array.isArray(attachments) ? attachments.map((attachment) => ({ ...attachment })) : [];
}

export function cloneConversationMessage(message) {
if (!message || typeof message !== "object") {
return null;
}

return {
...message,
attachments: cloneConversationAttachments(message.attachments)
};
}

export function applyConversationMessage(targetMessage, nextMessage) {
if (!targetMessage || !nextMessage || targetMessage === nextMessage) {
return targetMessage;
}

Object.assign(targetMessage, nextMessage);
targetMessage.attachments = cloneConversationAttachments(nextMessage.attachments);
return targetMessage;
}

export function normalizeAssistantEvaluation(context = {}) {
return {
assistantContent: typeof context?.assistantContent === "string" ? context.assistantContent : "",
history: Array.isArray(context?.history)
? context.history.map((message) => cloneConversationMessage(message)).filter(Boolean)
: [],
logs: Array.isArray(context?.logs)
? context.logs.map((entry) => normalizeAssistantEvaluationLogEntry(entry)).filter(Boolean)
: [],
messageId: typeof context?.messageId === "string" ? context.messageId : "",
store: context?.store || null
};
}

export async function resolveProcessedConversationMessage(options = {}) {
const fallbackMessage = cloneConversationMessage(options.message);
const processMessage = typeof options.processMessage === "function" ? options.processMessage : async (context) => context;
const processedContext = await processMessage({
...options,
history: Array.isArray(options.history)
? options.history.map((message) => cloneConversationMessage(message)).filter(Boolean)
: [],
message: fallbackMessage
});
const processedMessage =
processedContext && typeof processedContext === "object" ? processedContext.message : fallbackMessage;

return cloneConversationMessage(processedMessage) || fallbackMessage;
}

export async function createProcessedConversationMessage(options = {}) {
const createMessage =
typeof options.createMessage === "function"
? options.createMessage
: (role, content, messageOptions = {}) => ({
...messageOptions,
content,
role
});

return resolveProcessedConversationMessage({
...(options.context && typeof options.context === "object" && !Array.isArray(options.context)
? options.context
: {}),
history: Array.isArray(options.context?.history) ? options.context.history : [],
message: createMessage(options.role, options.content, options.options),
processMessage: options.processMessage
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ That shared history styling resets rendered markdown bubbles back to normal whit
The settings and prompt-history dialogs reuse the shared `_core/visual/forms/dialog.css` shell layout. Their header and footer rows stay fixed while only the settings body or prompt-history frame scrolls, so the footer actions remain reachable even when the content is long.

Caught overlay runtime errors are logged through `console.error` and shown through the shared toast stack from `_core/visual/chrome/toast.js`. The composer placeholder still belongs to ready-state and lightweight status guidance, so raw exception text should not be pushed into the textarea placeholder.
Overlay execution transcripts now use the shared YAML-first formatter for both console logs and returned values, emitting block headers such as `log↓`, `warn↓`, `error↓`, and `result↓` so structured telemetry stays complete across the thread and execution cards. Immediately before those execution results are serialized back into the `execution-output` follow-up turn, the overlay also runs the shared assistant-message evaluation seam; the current first-party hook from `_core/agent-chat` prepends synthetic loop warnings when the exact same assistant message reappears, using `info` on the 2nd send, `warn` on the 3rd send, and `error` on the 4th send onward.
Overlay execution transcripts now use the shared YAML-first formatter for both console logs and returned values, emitting block headers such as `log↓`, `warn↓`, `error↓`, and `result↓` so structured telemetry stays complete across the thread and execution cards. Immediately before those execution results are serialized back into the `execution-output` follow-up turn, the overlay also runs the shared assistant-message evaluation seam; the current first-party hook from `_core/agent-chat` prepends synthetic loop warnings when the exact same assistant message reappears, using `info` on the 2nd send, `warn` on the 3rd send, and `error` on the 4th send onward. The same `_core/agent-chat` module now also owns the shared runtime-hygiene helper flow that the overlay store uses to clone processed messages, normalize assistant-evaluation input, and route `submit`, `assistant-response`, `history-compact`, `protocol-retry`, and `execution-output` turns through `processOnscreenAgentMessage` without leaking hook mutations back into source history objects.

The settings dialog now has two provider tabs named `API` and `Local`. `API` keeps the OpenAI-compatible endpoint, model, and key fields. `Local` mounts the shared Hugging Face config sidebar in onscreen mode, so the overlay reads the same saved-model list and live WebGPU worker state as the routed Local LLM page and the admin chat. Opening the Local tab should refresh saved-model shortcuts without booting the worker; saving local settings persists the selected repo id and dtype, then starts background model preparation. When no local model is selected and saved models exist, the Local panel preselects the browser-wide last successfully loaded saved model from `_core/huggingface/manager.js`, falling back to the first saved entry if that last-used entry was discarded. When no local model is selected, no local model is loaded, and the shared saved-model list is empty, the Local panel prefills the Hugging Face model field with the same testing-page default: `onnx-community/gemma-4-E4B-it-ONNX`.

Expand Down
Loading