Skip to content

Commit df1a03c

Browse files
authored
fix(agent): improve handler edits and mcp gateway load (#88)
* fix(agent): improve handler edits and mcp gateway load * fix(agent): address handler edit review feedback
1 parent 378b41d commit df1a03c

6 files changed

Lines changed: 376 additions & 35 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Most agent CLIs are powerful because they can touch your machine directly: shell
1414

1515
HyperAgent takes a different route. The model acts by writing JavaScript handlers, and those handlers run inside a hardware-isolated Hyperlight micro-VM. By default there is no shell, no filesystem, no network, and no process access. When the task needs host capabilities, they are added deliberately through plugins, profiles, or MCP servers.
1616

17+
Because handlers can fetch, parse, filter, aggregate, and validate data before returning, the model does not have to read every raw API response, file, or plugin result itself. Well-written handlers can reduce large tool outputs into compact, relevant results, keeping more of the conversation focused on decisions instead of data plumbing and often dramatically reducing token consumption during research, repair, and analysis loops.
18+
1719
What that gets you:
1820

1921
| Instead of | HyperAgent gives you |
@@ -51,7 +53,7 @@ These are the kinds of jobs HyperAgent is designed to handle.
5153

5254
```bash
5355
hyperagent --skill pptx-expert --profile web-research \
54-
--prompt "Create a visually rich Artemis II mission briefing deck. Use NASA public imagery where available, include mission objectives, crew, Orion/SLS architecture, lunar flyby timeline, key risks, and why the mission matters. Make it dramatic but factual, with strong full-bleed image slides and clean diagrams. Save it as artemis-ii-briefing.pptx."
56+
--prompt "Create a presentation on the NASA Artemis II mission include lots of statistics and data, use an appropriate theme and color scheme for the subject, your aim is to inspire the audience to find out more and get involved, make sure you go to the Internet to get the very latest mission information from https://www.nasa.gov/mission/artemis-ii and images/multimedia from https://www.nasa.gov/artemis-ii-multimedia/, ensure you include photos taken by the crew during the mission, make it stunning"
5557
```
5658

5759
The agent can use `ha:pptx`, `ha:pptx-charts`, and `ha:pptx-tables` to create editable PowerPoint files instead of screenshots glued into slides.

src/agent/index.ts

Lines changed: 130 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1662,11 +1662,12 @@ const editHandlerTool = defineTool("edit_handler", {
16621662
description: [
16631663
"Make a surgical edit to an existing handler without re-sending all the code.",
16641664
"",
1665-
"Finds oldString exactly once in the handler and replaces it with newString.",
1666-
"Much faster and safer than re-registering the entire handler for small fixes.",
1665+
"Either replace oldString exactly once, or replace a line range from get_handler_source.",
1666+
"Much faster and safer than regenerating the entire handler for small fixes.",
16671667
"",
1668-
"⚠️ oldString must match EXACTLY ONCE. If it matches 0 or 2+ times, the edit",
1669-
"fails. Add more surrounding context to make the match unique.",
1668+
"String mode: provide oldString and newString. oldString must match EXACTLY ONCE.",
1669+
"Line mode: provide startLine, optional endLine, and replacement. Use the line",
1670+
"numbers returned by get_handler_source, but do not include the 'N |' prefixes.",
16701671
"",
16711672
"Returns the edited region with surrounding context for verification.",
16721673
].join("\n"),
@@ -1679,62 +1680,157 @@ const editHandlerTool = defineTool("edit_handler", {
16791680
},
16801681
oldString: {
16811682
type: "string",
1682-
description:
1683-
"Exact string to find and replace. Must occur exactly once.",
1683+
description: "String mode: exact string to find and replace once.",
16841684
},
16851685
newString: {
16861686
type: "string",
1687-
description: "Replacement string.",
1687+
description: "String mode: replacement string.",
1688+
},
1689+
startLine: {
1690+
type: "number",
1691+
description:
1692+
"Line mode: 1-based start line to replace, from get_handler_source.",
1693+
},
1694+
endLine: {
1695+
type: "number",
1696+
description:
1697+
"Line mode: optional 1-based end line to replace. Defaults to startLine.",
1698+
},
1699+
replacement: {
1700+
type: "string",
1701+
description: "Line mode: replacement code for the selected line range.",
16881702
},
16891703
},
1690-
required: ["name", "oldString", "newString"],
1704+
required: ["name"],
16911705
},
16921706
handler: async ({
16931707
name,
16941708
oldString,
16951709
newString,
1710+
startLine,
1711+
endLine,
1712+
replacement,
16961713
}: {
16971714
name: string;
1698-
oldString: string;
1699-
newString: string;
1715+
oldString?: string;
1716+
newString?: string;
1717+
startLine?: number;
1718+
endLine?: number;
1719+
replacement?: string;
17001720
}) => {
17011721
// ── Preview the edit and validate before applying ─────────────
17021722
// Get current source to build the edited version
17031723
const sourceResult = sandbox.getHandlerSource(name, {
17041724
lineNumbers: false,
1705-
}) as { success: true; source: string } | { success: false; error: string };
1725+
});
17061726

17071727
if (!sourceResult.success) {
17081728
console.error(` ${C.err("❌ " + sourceResult.error)}`);
17091729
return { success: false, error: sourceResult.error };
17101730
}
17111731

1712-
const currentSource = sourceResult.source;
1732+
const currentSource = sourceResult.code ?? "";
17131733

1714-
// Check exact-once match
1715-
const firstIdx = currentSource.indexOf(oldString);
1716-
if (firstIdx === -1) {
1717-
const error =
1718-
"oldString not found in handler. Use get_handler_source to see current code, then copy the EXACT text to replace.";
1719-
console.error(` ${C.err("❌ " + error)}`);
1720-
return { success: false, error };
1721-
}
1722-
const secondIdx = currentSource.indexOf(
1723-
oldString,
1724-
firstIdx + oldString.length,
1725-
);
1726-
if (secondIdx !== -1) {
1734+
const hasStringEdit = oldString !== undefined || newString !== undefined;
1735+
const hasLineEdit =
1736+
startLine !== undefined ||
1737+
endLine !== undefined ||
1738+
replacement !== undefined;
1739+
1740+
if (hasStringEdit && hasLineEdit) {
17271741
const error =
1728-
"oldString matches multiple times. Add more surrounding context to make it unique.";
1742+
"Use either string mode (oldString + newString) or line mode (startLine/endLine + replacement), not both.";
17291743
console.error(` ${C.err("❌ " + error)}`);
17301744
return { success: false, error };
17311745
}
17321746

1733-
// Build the edited code
1734-
const editedCode =
1735-
currentSource.slice(0, firstIdx) +
1736-
newString +
1737-
currentSource.slice(firstIdx + oldString.length);
1747+
let editedCode: string;
1748+
let applyEdit: () => Promise<{
1749+
success: boolean;
1750+
message?: string;
1751+
error?: string;
1752+
handlers?: string[];
1753+
codeSize?: number;
1754+
contextAfter?: string;
1755+
}>;
1756+
1757+
if (hasLineEdit) {
1758+
if (typeof replacement !== "string") {
1759+
const error = "Line mode requires replacement.";
1760+
console.error(` ${C.err("❌ " + error)}`);
1761+
return { success: false, error };
1762+
}
1763+
if (
1764+
typeof startLine !== "number" ||
1765+
!Number.isInteger(startLine) ||
1766+
startLine < 1
1767+
) {
1768+
const error = "Line mode requires startLine as a positive integer.";
1769+
console.error(` ${C.err("❌ " + error)}`);
1770+
return { success: false, error };
1771+
}
1772+
1773+
const lines = currentSource.split("\n");
1774+
const rangeEnd = endLine ?? startLine;
1775+
if (
1776+
typeof rangeEnd !== "number" ||
1777+
!Number.isInteger(rangeEnd) ||
1778+
rangeEnd < startLine
1779+
) {
1780+
const error =
1781+
"endLine must be a positive integer greater than or equal to startLine.";
1782+
console.error(` ${C.err("❌ " + error)}`);
1783+
return { success: false, error };
1784+
}
1785+
if (startLine > lines.length || rangeEnd > lines.length) {
1786+
const error = `Line range ${startLine}-${rangeEnd} is outside handler "${name}" (${lines.length} lines).`;
1787+
console.error(` ${C.err("❌ " + error)}`);
1788+
return { success: false, error };
1789+
}
1790+
1791+
const replacementLines =
1792+
replacement === "" ? [] : replacement.split("\n");
1793+
const editedLines = [
1794+
...lines.slice(0, startLine - 1),
1795+
...replacementLines,
1796+
...lines.slice(rangeEnd),
1797+
];
1798+
editedCode = editedLines.join("\n");
1799+
applyEdit = async () =>
1800+
sandbox.editHandlerLines(name, startLine, rangeEnd, replacement);
1801+
} else {
1802+
if (typeof oldString !== "string" || typeof newString !== "string") {
1803+
const error = "String mode requires oldString and newString.";
1804+
console.error(` ${C.err("❌ " + error)}`);
1805+
return { success: false, error };
1806+
}
1807+
1808+
// Check exact-once match
1809+
const firstIdx = currentSource.indexOf(oldString);
1810+
if (firstIdx === -1) {
1811+
const error =
1812+
"oldString not found in handler. Use get_handler_source to see current code, then copy the EXACT text to replace, or use startLine/replacement line mode.";
1813+
console.error(` ${C.err("❌ " + error)}`);
1814+
return { success: false, error };
1815+
}
1816+
const secondIdx = currentSource.indexOf(
1817+
oldString,
1818+
firstIdx + oldString.length,
1819+
);
1820+
if (secondIdx !== -1) {
1821+
const error =
1822+
"oldString matches multiple times. Add more surrounding context to make it unique, or use startLine/replacement line mode.";
1823+
console.error(` ${C.err("❌ " + error)}`);
1824+
return { success: false, error };
1825+
}
1826+
1827+
// Build the edited code
1828+
editedCode =
1829+
currentSource.slice(0, firstIdx) +
1830+
newString +
1831+
currentSource.slice(firstIdx + oldString.length);
1832+
applyEdit = async () => sandbox.editHandler(name, oldString, newString);
1833+
}
17381834

17391835
// Validate the edited code through the same pipeline as register_handler
17401836
try {
@@ -1762,7 +1858,7 @@ const editHandlerTool = defineTool("edit_handler", {
17621858
}
17631859

17641860
// Validation passed — apply the edit
1765-
const result = await sandbox.editHandler(name, oldString, newString);
1861+
const result = await applyEdit();
17661862
if (result.success) {
17671863
console.error(
17681864
` ${C.ok("✅")} Edited handler "${name}" (${result.codeSize} bytes)`,
@@ -5353,9 +5449,10 @@ async function main(): Promise<void> {
53535449
if (mcpManager) {
53545450
const mcpPlugin = pluginManager.getPlugin("mcp");
53555451
if (mcpPlugin && mcpPlugin.state !== "enabled") {
5452+
const mcpSource = pluginManager.loadSource("mcp");
53565453
// Compute current content hash so the audit matches the source
53575454
const mcpHash = computePluginHash(mcpPlugin.dir);
5358-
if (mcpHash) {
5455+
if (mcpSource && mcpHash) {
53595456
pluginManager.setAuditResult("mcp", {
53605457
contentHash: mcpHash,
53615458
auditedAt: new Date().toISOString(),

src/sandbox/tool.d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,19 @@ export type SandboxTool = {
223223
codeSize?: number;
224224
contextAfter?: string;
225225
}>;
226+
editHandlerLines: (
227+
name: string,
228+
startLine: number,
229+
endLine: number,
230+
replacement: string,
231+
) => Promise<{
232+
success: boolean;
233+
message?: string;
234+
error?: string;
235+
handlers?: string[];
236+
codeSize?: number;
237+
contextAfter?: string;
238+
}>;
226239
registerModule: (
227240
name: string,
228241
source: string,

0 commit comments

Comments
 (0)