Skip to content
Merged
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Most agent CLIs are powerful because they can touch your machine directly: shell

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.

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.

What that gets you:

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

```bash
hyperagent --skill pptx-expert --profile web-research \
--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."
--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"
```

The agent can use `ha:pptx`, `ha:pptx-charts`, and `ha:pptx-tables` to create editable PowerPoint files instead of screenshots glued into slides.
Expand Down
163 changes: 130 additions & 33 deletions src/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1662,11 +1662,12 @@ const editHandlerTool = defineTool("edit_handler", {
description: [
"Make a surgical edit to an existing handler without re-sending all the code.",
"",
"Finds oldString exactly once in the handler and replaces it with newString.",
"Much faster and safer than re-registering the entire handler for small fixes.",
"Either replace oldString exactly once, or replace a line range from get_handler_source.",
"Much faster and safer than regenerating the entire handler for small fixes.",
"",
"⚠️ oldString must match EXACTLY ONCE. If it matches 0 or 2+ times, the edit",
"fails. Add more surrounding context to make the match unique.",
"String mode: provide oldString and newString. oldString must match EXACTLY ONCE.",
"Line mode: provide startLine, optional endLine, and replacement. Use the line",
"numbers returned by get_handler_source, but do not include the 'N |' prefixes.",
"",
"Returns the edited region with surrounding context for verification.",
].join("\n"),
Expand All @@ -1679,62 +1680,157 @@ const editHandlerTool = defineTool("edit_handler", {
},
oldString: {
type: "string",
description:
"Exact string to find and replace. Must occur exactly once.",
description: "String mode: exact string to find and replace once.",
},
newString: {
type: "string",
description: "Replacement string.",
description: "String mode: replacement string.",
},
startLine: {
type: "number",
description:
"Line mode: 1-based start line to replace, from get_handler_source.",
},
endLine: {
type: "number",
description:
"Line mode: optional 1-based end line to replace. Defaults to startLine.",
},
replacement: {
type: "string",
description: "Line mode: replacement code for the selected line range.",
},
},
required: ["name", "oldString", "newString"],
required: ["name"],
},
handler: async ({
name,
oldString,
newString,
startLine,
endLine,
replacement,
}: {
name: string;
oldString: string;
newString: string;
oldString?: string;
newString?: string;
startLine?: number;
endLine?: number;
replacement?: string;
}) => {
// ── Preview the edit and validate before applying ─────────────
// Get current source to build the edited version
const sourceResult = sandbox.getHandlerSource(name, {
lineNumbers: false,
}) as { success: true; source: string } | { success: false; error: string };
});

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

const currentSource = sourceResult.source;
const currentSource = sourceResult.code ?? "";

// Check exact-once match
const firstIdx = currentSource.indexOf(oldString);
if (firstIdx === -1) {
const error =
"oldString not found in handler. Use get_handler_source to see current code, then copy the EXACT text to replace.";
console.error(` ${C.err("❌ " + error)}`);
return { success: false, error };
}
const secondIdx = currentSource.indexOf(
oldString,
firstIdx + oldString.length,
);
if (secondIdx !== -1) {
const hasStringEdit = oldString !== undefined || newString !== undefined;
const hasLineEdit =
startLine !== undefined ||
endLine !== undefined ||
replacement !== undefined;

if (hasStringEdit && hasLineEdit) {
const error =
"oldString matches multiple times. Add more surrounding context to make it unique.";
"Use either string mode (oldString + newString) or line mode (startLine/endLine + replacement), not both.";
console.error(` ${C.err("❌ " + error)}`);
return { success: false, error };
}

// Build the edited code
const editedCode =
currentSource.slice(0, firstIdx) +
newString +
currentSource.slice(firstIdx + oldString.length);
let editedCode: string;
let applyEdit: () => Promise<{
success: boolean;
message?: string;
error?: string;
handlers?: string[];
codeSize?: number;
contextAfter?: string;
}>;

if (hasLineEdit) {
if (typeof replacement !== "string") {
const error = "Line mode requires replacement.";
console.error(` ${C.err("❌ " + error)}`);
return { success: false, error };
}
if (
typeof startLine !== "number" ||
!Number.isInteger(startLine) ||
startLine < 1
) {
const error = "Line mode requires startLine as a positive integer.";
console.error(` ${C.err("❌ " + error)}`);
return { success: false, error };
}

const lines = currentSource.split("\n");
const rangeEnd = endLine ?? startLine;
if (
typeof rangeEnd !== "number" ||
!Number.isInteger(rangeEnd) ||
rangeEnd < startLine
) {
const error =
"endLine must be a positive integer greater than or equal to startLine.";
console.error(` ${C.err("❌ " + error)}`);
return { success: false, error };
}
if (startLine > lines.length || rangeEnd > lines.length) {
const error = `Line range ${startLine}-${rangeEnd} is outside handler "${name}" (${lines.length} lines).`;
console.error(` ${C.err("❌ " + error)}`);
return { success: false, error };
}

const replacementLines =
replacement === "" ? [] : replacement.split("\n");
const editedLines = [
...lines.slice(0, startLine - 1),
...replacementLines,
...lines.slice(rangeEnd),
];
editedCode = editedLines.join("\n");
applyEdit = async () =>
sandbox.editHandlerLines(name, startLine, rangeEnd, replacement);
} else {
if (typeof oldString !== "string" || typeof newString !== "string") {
const error = "String mode requires oldString and newString.";
console.error(` ${C.err("❌ " + error)}`);
return { success: false, error };
}

// Check exact-once match
const firstIdx = currentSource.indexOf(oldString);
if (firstIdx === -1) {
const error =
"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.";
console.error(` ${C.err("❌ " + error)}`);
return { success: false, error };
}
const secondIdx = currentSource.indexOf(
oldString,
firstIdx + oldString.length,
);
if (secondIdx !== -1) {
const error =
"oldString matches multiple times. Add more surrounding context to make it unique, or use startLine/replacement line mode.";
console.error(` ${C.err("❌ " + error)}`);
return { success: false, error };
}

// Build the edited code
editedCode =
currentSource.slice(0, firstIdx) +
newString +
currentSource.slice(firstIdx + oldString.length);
applyEdit = async () => sandbox.editHandler(name, oldString, newString);
}

// Validate the edited code through the same pipeline as register_handler
try {
Expand Down Expand Up @@ -1762,7 +1858,7 @@ const editHandlerTool = defineTool("edit_handler", {
}

// Validation passed — apply the edit
const result = await sandbox.editHandler(name, oldString, newString);
const result = await applyEdit();
if (result.success) {
console.error(
` ${C.ok("✅")} Edited handler "${name}" (${result.codeSize} bytes)`,
Expand Down Expand Up @@ -5353,9 +5449,10 @@ async function main(): Promise<void> {
if (mcpManager) {
const mcpPlugin = pluginManager.getPlugin("mcp");
if (mcpPlugin && mcpPlugin.state !== "enabled") {
const mcpSource = pluginManager.loadSource("mcp");
// Compute current content hash so the audit matches the source
const mcpHash = computePluginHash(mcpPlugin.dir);
if (mcpHash) {
if (mcpSource && mcpHash) {
pluginManager.setAuditResult("mcp", {
contentHash: mcpHash,
auditedAt: new Date().toISOString(),
Expand Down
13 changes: 13 additions & 0 deletions src/sandbox/tool.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,19 @@ export type SandboxTool = {
codeSize?: number;
contextAfter?: string;
}>;
editHandlerLines: (
name: string,
startLine: number,
endLine: number,
replacement: string,
) => Promise<{
success: boolean;
message?: string;
error?: string;
handlers?: string[];
codeSize?: number;
contextAfter?: string;
}>;
registerModule: (
name: string,
source: string,
Expand Down
Loading
Loading