refactor: polish trigger and manifest seams

This commit is contained in:
Peter Steinberger 2026-03-23 22:22:44 -07:00
parent 535b792808
commit 013385e5c2
No known key found for this signature in database
8 changed files with 135 additions and 47 deletions

View File

@ -26,6 +26,7 @@ import {
wrapStreamFnSanitizeMalformedToolCalls,
wrapStreamFnTrimToolCallNames,
} from "./attempt.js";
import { shouldInjectHeartbeatPromptForTrigger } from "./trigger-policy.js";
type FakeWrappedStream = {
result: () => Promise<unknown>;
@ -320,6 +321,17 @@ describe("resolvePromptModeForSession", () => {
});
describe("shouldInjectHeartbeatPrompt", () => {
it("uses trigger policy defaults for non-cron triggers", () => {
expect(shouldInjectHeartbeatPromptForTrigger("user")).toBe(true);
expect(shouldInjectHeartbeatPromptForTrigger("heartbeat")).toBe(true);
expect(shouldInjectHeartbeatPromptForTrigger("memory")).toBe(true);
expect(shouldInjectHeartbeatPromptForTrigger(undefined)).toBe(true);
});
it("uses trigger policy overrides for cron", () => {
expect(shouldInjectHeartbeatPromptForTrigger("cron")).toBe(false);
});
it("injects the heartbeat prompt for default-agent non-cron runs", () => {
expect(shouldInjectHeartbeatPrompt({ isDefaultAgent: true, trigger: "user" })).toBe(true);
expect(shouldInjectHeartbeatPrompt({ isDefaultAgent: true, trigger: "heartbeat" })).toBe(true);

View File

@ -158,7 +158,7 @@ import {
} from "./compaction-timeout.js";
import { pruneProcessedHistoryImages } from "./history-image-prune.js";
import { detectAndLoadPromptImages } from "./images.js";
import type { EmbeddedRunTrigger } from "./params.js";
import { shouldInjectHeartbeatPromptForTrigger } from "./trigger-policy.js";
import type { EmbeddedRunAttemptParams, EmbeddedRunAttemptResult } from "./types.js";
type PromptBuildHookRunner = {
@ -1527,9 +1527,9 @@ export function resolvePromptModeForSession(sessionKey?: string): "minimal" | "f
export function shouldInjectHeartbeatPrompt(params: {
isDefaultAgent: boolean;
trigger?: EmbeddedRunTrigger;
trigger?: EmbeddedRunAttemptParams["trigger"];
}): boolean {
return params.isDefaultAgent && params.trigger !== "cron";
return params.isDefaultAgent && shouldInjectHeartbeatPromptForTrigger(params.trigger);
}
export function resolveAttemptFsWorkspaceOnly(params: {

View File

@ -0,0 +1,22 @@
import type { EmbeddedRunTrigger } from "./params.js";
type EmbeddedRunTriggerPolicy = {
injectHeartbeatPrompt: boolean;
};
const DEFAULT_EMBEDDED_RUN_TRIGGER_POLICY: EmbeddedRunTriggerPolicy = {
injectHeartbeatPrompt: true,
};
const EMBEDDED_RUN_TRIGGER_POLICY: Partial<Record<EmbeddedRunTrigger, EmbeddedRunTriggerPolicy>> = {
cron: {
injectHeartbeatPrompt: false,
},
};
export function shouldInjectHeartbeatPromptForTrigger(trigger?: EmbeddedRunTrigger): boolean {
return (
(trigger ? EMBEDDED_RUN_TRIGGER_POLICY[trigger] : undefined)?.injectHeartbeatPrompt ??
DEFAULT_EMBEDDED_RUN_TRIGGER_POLICY.injectHeartbeatPrompt
);
}

View File

@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import {
BUNDLED_RUNTIME_SIDECAR_BASENAMES,
BUNDLED_RUNTIME_SIDECAR_PATHS,
GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES,
getPublicArtifactBasename,
} from "./public-artifacts.js";
describe("public artifact manifests", () => {
it("derives bundled sidecar basenames from the runtime sidecar paths", () => {
expect(BUNDLED_RUNTIME_SIDECAR_BASENAMES).toEqual([
...new Set(BUNDLED_RUNTIME_SIDECAR_PATHS.map(getPublicArtifactBasename)),
]);
});
it("keeps every bundled sidecar basename in the guarded public surface list", () => {
const guardedBasenames = new Set(GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES);
for (const basename of BUNDLED_RUNTIME_SIDECAR_BASENAMES) {
expect(guardedBasenames.has(basename), basename).toBe(true);
}
});
});

View File

@ -1,34 +1,61 @@
export const BUNDLED_RUNTIME_SIDECAR_PATHS = [
"dist/extensions/whatsapp/light-runtime-api.js",
"dist/extensions/whatsapp/runtime-api.js",
"dist/extensions/matrix/helper-api.js",
"dist/extensions/matrix/runtime-api.js",
"dist/extensions/matrix/thread-bindings-runtime.js",
"dist/extensions/msteams/runtime-api.js",
] as const;
function assertUniqueValues<T extends string>(values: readonly T[], label: string): readonly T[] {
const seen = new Set<string>();
const duplicates = new Set<string>();
for (const value of values) {
if (seen.has(value)) {
duplicates.add(value);
continue;
}
seen.add(value);
}
if (duplicates.size > 0) {
throw new Error(`Duplicate ${label}: ${Array.from(duplicates).join(", ")}`);
}
return values;
}
const EXTRA_GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES = [
"action-runtime.runtime.js",
"action-runtime-api.js",
"allow-from.js",
"api.js",
"auth-presence.js",
"index.js",
"login-qr-api.js",
"onboard.js",
"openai-codex-catalog.js",
"provider-catalog.js",
"session-key-api.js",
"setup-api.js",
"setup-entry.js",
"timeouts.js",
] as const;
export function getPublicArtifactBasename(relativePath: string): string {
return relativePath.split("/").at(-1) ?? relativePath;
}
export const GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES = [
...new Set([
...BUNDLED_RUNTIME_SIDECAR_PATHS.map(
(relativePath) => relativePath.split("/").at(-1) ?? relativePath,
),
...EXTRA_GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES,
]),
] as const;
export const BUNDLED_RUNTIME_SIDECAR_PATHS = assertUniqueValues(
[
"dist/extensions/whatsapp/light-runtime-api.js",
"dist/extensions/whatsapp/runtime-api.js",
"dist/extensions/matrix/helper-api.js",
"dist/extensions/matrix/runtime-api.js",
"dist/extensions/matrix/thread-bindings-runtime.js",
"dist/extensions/msteams/runtime-api.js",
] as const,
"bundled runtime sidecar path",
);
const EXTRA_GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES = assertUniqueValues(
[
"action-runtime.runtime.js",
"action-runtime-api.js",
"allow-from.js",
"api.js",
"auth-presence.js",
"index.js",
"login-qr-api.js",
"onboard.js",
"openai-codex-catalog.js",
"provider-catalog.js",
"session-key-api.js",
"setup-api.js",
"setup-entry.js",
"timeouts.js",
] as const,
"extra guarded extension public surface basename",
);
export const BUNDLED_RUNTIME_SIDECAR_BASENAMES = assertUniqueValues(
[...new Set(BUNDLED_RUNTIME_SIDECAR_PATHS.map(getPublicArtifactBasename))],
"bundled runtime sidecar basename",
);
export const GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES = assertUniqueValues(
[...BUNDLED_RUNTIME_SIDECAR_BASENAMES, ...EXTRA_GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES],
"guarded extension public surface basename",
);

View File

@ -305,13 +305,9 @@ async function dispatchSlashCommand(
}
const targetSessionKey = host.sessionKey;
const result = await executeSlashCommand(
host.client,
targetSessionKey,
name,
args,
host.chatModelCatalog,
);
const result = await executeSlashCommand(host.client, targetSessionKey, name, args, {
chatModelCatalog: host.chatModelCatalog,
});
if (result.content) {
injectCommandResult(host, result.content);

View File

@ -285,7 +285,9 @@ describe("executeSlashCommand directives", () => {
"main",
"model",
"gpt-5-mini",
[{ id: "gpt-5-mini", name: "gpt-5-mini", provider: "openai" }],
{
chatModelCatalog: [{ id: "gpt-5-mini", name: "gpt-5-mini", provider: "openai" }],
},
);
expect(request).toHaveBeenCalledWith("sessions.patch", {
@ -317,7 +319,9 @@ describe("executeSlashCommand directives", () => {
"main",
"model",
"gpt-5-mini",
[{ id: "gpt-5-mini", name: "GPT-5 Mini", provider: "openai" }],
{
chatModelCatalog: [{ id: "gpt-5-mini", name: "GPT-5 Mini", provider: "openai" }],
},
);
expect(result.sessionPatch?.modelOverride).toEqual({

View File

@ -50,12 +50,16 @@ export type SlashCommandResult = {
};
};
export type SlashCommandContext = {
chatModelCatalog?: ModelCatalogEntry[];
};
export async function executeSlashCommand(
client: GatewayBrowserClient,
sessionKey: string,
commandName: string,
args: string,
chatModelCatalog: ModelCatalogEntry[] = [],
context: SlashCommandContext = {},
): Promise<SlashCommandResult> {
switch (commandName) {
case "help":
@ -73,7 +77,7 @@ export async function executeSlashCommand(
case "compact":
return await executeCompact(client, sessionKey);
case "model":
return await executeModel(client, sessionKey, args, chatModelCatalog);
return await executeModel(client, sessionKey, args, context);
case "think":
return await executeThink(client, sessionKey, args);
case "fast":
@ -130,7 +134,7 @@ async function executeModel(
client: GatewayBrowserClient,
sessionKey: string,
args: string,
chatModelCatalog: ModelCatalogEntry[],
context: SlashCommandContext,
): Promise<SlashCommandResult> {
if (!args) {
try {
@ -163,6 +167,7 @@ async function executeModel(
});
const patchedModel = patched.resolved?.model ?? args.trim();
const rawOverride = createChatModelOverride(patchedModel.trim());
const chatModelCatalog = context.chatModelCatalog ?? [];
const resolvedValue = rawOverride
? normalizeChatModelOverrideValue(rawOverride, chatModelCatalog) ||
resolveServerChatModelValue(patchedModel, patched.resolved?.modelProvider)