Skip to content

Commit bb90876

Browse files
Harden prompt builder against trap-prone step arrays
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent a22ab9c commit bb90876

3 files changed

Lines changed: 101 additions & 7 deletions

File tree

currentState.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ HyperAgent exposes a TypeScript SDK for browser automation with three primary pa
134134
- Hardened CDP frame-filter host/path matching to avoid query-text false positives (e.g. unrelated URLs containing `https://yahoo.com/pixel` in query params) while preserving legitimate host-suffix + path rule matching.
135135
- Hardened prompt token budgeting for variables by capping serialized variable entries per step-message build and emitting omitted-count context instead of unbounded variable dumps.
136136
- Expanded trap-safe per-call override regression coverage for sync task execution and replay params to ensure `cdpActions` / `filterAdTrackingFrames` reliably fall back to agent defaults when option getters throw.
137+
- Hardened prompt-step history materialization with trap-safe step-array reads so malformed/trap-prone `steps` payloads degrade gracefully instead of crashing message assembly.
137138
- Expanded top-level package exports for key workflow/config types at `@hyperbrowser/agent`.
138139
- Removed stale script entry (`build-dom-tree-script`) and improved README usage docs.
139140
- Added canonical single-action debug writer helper (`writePerformDebug`) while preserving deprecated alias compatibility.

src/agent/messages/builder.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,82 @@ describe("buildAgentStepMessages", () => {
214214
expect(joined).toContain("Action output unavailable");
215215
});
216216

217+
it("falls back to no previous-actions section when step array length getter traps", async () => {
218+
const page = createFakePage("https://example.com/current", [
219+
"https://example.com/current",
220+
]);
221+
const trappedSteps = new Proxy([createStep(0)], {
222+
get: (target, prop, receiver) => {
223+
if (prop === "length") {
224+
throw new Error("steps length trap");
225+
}
226+
return Reflect.get(target, prop, receiver);
227+
},
228+
});
229+
230+
const messages = await buildAgentStepMessages(
231+
[{ role: "system", content: "system" }],
232+
trappedSteps as unknown as AgentStep[],
233+
"task",
234+
page,
235+
{
236+
elements: new Map(),
237+
domState: "dom",
238+
xpathMap: {},
239+
backendNodeMap: {},
240+
},
241+
undefined,
242+
[]
243+
);
244+
245+
const joined = messages
246+
.map((message) =>
247+
typeof message.content === "string" ? message.content : ""
248+
)
249+
.join("\n");
250+
251+
expect(joined).not.toContain("=== Previous Actions ===");
252+
expect(joined).toContain("=== Final Goal ===");
253+
});
254+
255+
it("ignores unreadable step array entries when index getter traps", async () => {
256+
const page = createFakePage("https://example.com/current", [
257+
"https://example.com/current",
258+
]);
259+
const trappedSteps = new Proxy([createStep(0)], {
260+
get: (target, prop, receiver) => {
261+
if (prop === "0") {
262+
throw new Error("steps item trap");
263+
}
264+
return Reflect.get(target, prop, receiver);
265+
},
266+
});
267+
268+
const messages = await buildAgentStepMessages(
269+
[{ role: "system", content: "system" }],
270+
trappedSteps as unknown as AgentStep[],
271+
"task",
272+
page,
273+
{
274+
elements: new Map(),
275+
domState: "dom",
276+
xpathMap: {},
277+
backendNodeMap: {},
278+
},
279+
undefined,
280+
[]
281+
);
282+
283+
const joined = messages
284+
.map((message) =>
285+
typeof message.content === "string" ? message.content : ""
286+
)
287+
.join("\n");
288+
289+
expect(joined).not.toContain("=== Previous Actions ===");
290+
expect(joined).not.toContain("thought-0");
291+
});
292+
217293
it("falls back to zeroed page state when scroll info lookup fails", async () => {
218294
retry.mockRejectedValue({ reason: "scroll failed" });
219295
const page = createFakePage("https://example.com/current", [

src/agent/messages/builder.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,22 @@ function getBoundedVariables(variables: HyperVariable[]): {
240240
};
241241
}
242242

243+
function materializeSafeSteps(steps: AgentStep[]): AgentStep[] {
244+
const total = safeArrayLength(steps);
245+
if (total === 0) {
246+
return [];
247+
}
248+
249+
const normalizedSteps: AgentStep[] = [];
250+
for (let index = 0; index < total; index += 1) {
251+
const step = safeReadArrayItem<AgentStep>(steps, index);
252+
if (typeof step !== "undefined") {
253+
normalizedSteps.push(step);
254+
}
255+
}
256+
return normalizedSteps;
257+
}
258+
243259
function normalizeStepText(value: unknown, fallback: string): string {
244260
if (typeof value === "string") {
245261
return truncatePromptText(value);
@@ -405,6 +421,7 @@ export const buildAgentStepMessages = async (
405421
variables: HyperVariable[]
406422
): Promise<HyperAgentMessage[]> => {
407423
const messages = [...baseMessages];
424+
const normalizedSteps = materializeSafeSteps(steps);
408425

409426
// Add the final goal section
410427
messages.push({
@@ -432,20 +449,20 @@ export const buildAgentStepMessages = async (
432449
});
433450

434451
// Add previous actions section if there are steps
435-
if (steps.length > 0) {
452+
if (normalizedSteps.length > 0) {
436453
const relevantSteps =
437-
steps.length > MAX_HISTORY_STEPS
438-
? steps.slice(-MAX_HISTORY_STEPS)
439-
: steps;
440-
const hiddenStepCount = steps.length - relevantSteps.length;
454+
normalizedSteps.length > MAX_HISTORY_STEPS
455+
? normalizedSteps.slice(-MAX_HISTORY_STEPS)
456+
: normalizedSteps;
457+
const hiddenStepCount = normalizedSteps.length - relevantSteps.length;
441458
const omittedSteps =
442-
hiddenStepCount > 0 ? steps.slice(0, hiddenStepCount) : [];
459+
hiddenStepCount > 0 ? normalizedSteps.slice(0, hiddenStepCount) : [];
443460

444461
messages.push({
445462
role: "user",
446463
content:
447464
hiddenStepCount > 0
448-
? `=== Previous Actions ===\n(Showing latest ${relevantSteps.length} of ${steps.length} steps; ${hiddenStepCount} older steps omitted for context budget.)\n`
465+
? `=== Previous Actions ===\n(Showing latest ${relevantSteps.length} of ${normalizedSteps.length} steps; ${hiddenStepCount} older steps omitted for context budget.)\n`
449466
: "=== Previous Actions ===\n",
450467
});
451468
if (hiddenStepCount > 0) {

0 commit comments

Comments
 (0)