@harperfast/agent 0.13.1 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/dist/agent.js +140 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,6 +56,18 @@ Harper: What do you want to do together today?
|
|
|
56
56
|
>
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
### Non-interactive: pipe an initial prompt
|
|
60
|
+
|
|
61
|
+
You can pass an initial chat dump via stdin. This runs a one-shot interaction and exits after responding:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
cat somePrompt.md | harper-agent
|
|
65
|
+
# or
|
|
66
|
+
harper-agent < somePrompt.md
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
In this mode, the initial greeting question is suppressed, and the agent processes the provided prompt immediately.
|
|
70
|
+
|
|
59
71
|
## Model Selection
|
|
60
72
|
|
|
61
73
|
By default, `harper-agent` uses OpenAI. You can switch to other models using the `--model` (or `-m`) flag:
|
package/dist/agent.js
CHANGED
|
@@ -92,6 +92,17 @@ var CostTracker = class {
|
|
|
92
92
|
this.totalCompactionCost = 0;
|
|
93
93
|
this.hasUnknownPrices = false;
|
|
94
94
|
}
|
|
95
|
+
getTotalCost() {
|
|
96
|
+
return this.totalCost + this.totalCompactionCost;
|
|
97
|
+
}
|
|
98
|
+
getEstimatedTotalCost(currentTurnUsage, model, compactionModel) {
|
|
99
|
+
const { turnCost, compactionCost } = this.calculateUsageCosts(
|
|
100
|
+
model,
|
|
101
|
+
currentTurnUsage,
|
|
102
|
+
compactionModel
|
|
103
|
+
);
|
|
104
|
+
return this.getTotalCost() + turnCost + compactionCost;
|
|
105
|
+
}
|
|
95
106
|
recordTurn(model, usage, compactionModel) {
|
|
96
107
|
const { turnCost, compactionCost, unknownPrices } = this.calculateUsageCosts(model, usage, compactionModel);
|
|
97
108
|
this.totalInputTokens += usage.inputTokens;
|
|
@@ -315,6 +326,8 @@ var trackedState = {
|
|
|
315
326
|
useFlexTier: false,
|
|
316
327
|
disableSpinner: false,
|
|
317
328
|
enableInterruptions: true,
|
|
329
|
+
maxTurns: 30,
|
|
330
|
+
maxCost: null,
|
|
318
331
|
session: null
|
|
319
332
|
};
|
|
320
333
|
|
|
@@ -516,6 +529,12 @@ ${chalk3.bold("OPTIONS")}
|
|
|
516
529
|
Can also be set via HARPER_AGENT_COMPACTION_MODEL environment variable.
|
|
517
530
|
-s, --session Specify a path to a SQLite database file to persist the chat session.
|
|
518
531
|
Can also be set via HARPER_AGENT_SESSION environment variable.
|
|
532
|
+
--max-turns Specify the maximum number of turns for the agent run.
|
|
533
|
+
In task-driven mode, this defaults to unlimited.
|
|
534
|
+
Can also be set via HARPER_AGENT_MAX_TURNS environment variable.
|
|
535
|
+
--max-cost Specify the maximum cost (in USD) for the agent run.
|
|
536
|
+
If exceeded, the agent will exit with a non-zero code.
|
|
537
|
+
Can also be set via HARPER_AGENT_MAX_COST environment variable.
|
|
519
538
|
--flex-tier Force the use of the flex service tier for lower costs but potentially
|
|
520
539
|
more errors under high system load.
|
|
521
540
|
Can also be set via HARPER_AGENT_FLEX_TIER=true environment variable.
|
|
@@ -583,19 +602,31 @@ function parseArgs() {
|
|
|
583
602
|
const flagPairs = [
|
|
584
603
|
["model", ["--model", "-m", "model"]],
|
|
585
604
|
["compactionModel", ["--compaction-model", "-c", "compaction-model"]],
|
|
586
|
-
["sessionPath", ["--session", "-s", "session"]]
|
|
605
|
+
["sessionPath", ["--session", "-s", "session"]],
|
|
606
|
+
["maxTurns", ["--max-turns"]],
|
|
607
|
+
["maxCost", ["--max-cost"]]
|
|
587
608
|
];
|
|
588
609
|
let handled = false;
|
|
589
610
|
for (const [key, prefixes] of flagPairs) {
|
|
590
611
|
for (const prefix of prefixes) {
|
|
591
612
|
if (arg === prefix) {
|
|
592
613
|
if (args[i + 1]) {
|
|
593
|
-
|
|
614
|
+
const val = stripQuotes(args[++i]);
|
|
615
|
+
if (key === "maxTurns" || key === "maxCost") {
|
|
616
|
+
trackedState[key] = parseFloat(val);
|
|
617
|
+
} else {
|
|
618
|
+
trackedState[key] = val;
|
|
619
|
+
}
|
|
594
620
|
}
|
|
595
621
|
handled = true;
|
|
596
622
|
break;
|
|
597
623
|
} else if (arg.startsWith(`${prefix}=`)) {
|
|
598
|
-
|
|
624
|
+
const val = stripQuotes(arg.slice(prefix.length + 1));
|
|
625
|
+
if (key === "maxTurns" || key === "maxCost") {
|
|
626
|
+
trackedState[key] = parseFloat(val);
|
|
627
|
+
} else {
|
|
628
|
+
trackedState[key] = val;
|
|
629
|
+
}
|
|
599
630
|
handled = true;
|
|
600
631
|
break;
|
|
601
632
|
}
|
|
@@ -631,6 +662,12 @@ function parseArgs() {
|
|
|
631
662
|
if (!trackedState.sessionPath && process.env.HARPER_AGENT_SESSION) {
|
|
632
663
|
trackedState.sessionPath = process.env.HARPER_AGENT_SESSION;
|
|
633
664
|
}
|
|
665
|
+
if (process.env.HARPER_AGENT_MAX_TURNS) {
|
|
666
|
+
trackedState.maxTurns = parseFloat(process.env.HARPER_AGENT_MAX_TURNS);
|
|
667
|
+
}
|
|
668
|
+
if (process.env.HARPER_AGENT_MAX_COST) {
|
|
669
|
+
trackedState.maxCost = parseFloat(process.env.HARPER_AGENT_MAX_COST);
|
|
670
|
+
}
|
|
634
671
|
const sp = trackedState.sessionPath;
|
|
635
672
|
if (sp) {
|
|
636
673
|
trackedState.sessionPath = sp && !sp.startsWith("~") && !path.isAbsolute(sp) ? path.resolve(process.cwd(), sp) : sp;
|
|
@@ -1231,12 +1268,12 @@ async function requiredSkillForOperation(path8, type) {
|
|
|
1231
1268
|
}
|
|
1232
1269
|
const p = normalizedPath(path8);
|
|
1233
1270
|
const read = await getSkillsRead();
|
|
1234
|
-
if (p.includes("/resources/") || p.startsWith("resources/")) {
|
|
1235
|
-
if (!read.includes("
|
|
1236
|
-
return pickExistingSkill(["
|
|
1271
|
+
if (p.includes("/resources/") || p.startsWith("resources/") || p.endsWith("resources.ts") || p.endsWith("resources.js")) {
|
|
1272
|
+
if (!read.includes("automatic-apis")) {
|
|
1273
|
+
return pickExistingSkill(["automatic-apis"]);
|
|
1237
1274
|
}
|
|
1238
1275
|
}
|
|
1239
|
-
if (p.
|
|
1276
|
+
if (p.endsWith(".graphql")) {
|
|
1240
1277
|
if (!read.includes("adding-tables-with-schemas")) {
|
|
1241
1278
|
return pickExistingSkill(["adding-tables-with-schemas"]);
|
|
1242
1279
|
}
|
|
@@ -1275,40 +1312,42 @@ ${chalk7.bold.bgYellow.black(" Apply patch approval required: ")}`);
|
|
|
1275
1312
|
return false;
|
|
1276
1313
|
}
|
|
1277
1314
|
}
|
|
1315
|
+
async function execute11(operation) {
|
|
1316
|
+
try {
|
|
1317
|
+
const needed = await requiredSkillForOperation(operation.path, operation.type);
|
|
1318
|
+
if (needed) {
|
|
1319
|
+
const content = await execute10({ skill: needed });
|
|
1320
|
+
console.log(`Understanding ${needed} is necessary before applying this patch.`);
|
|
1321
|
+
return { status: "failed, skill guarded", output: content };
|
|
1322
|
+
}
|
|
1323
|
+
switch (operation.type) {
|
|
1324
|
+
case "create_file":
|
|
1325
|
+
if (!operation.diff) {
|
|
1326
|
+
return { status: "failed", output: "Error: diff is required for create_file" };
|
|
1327
|
+
}
|
|
1328
|
+
return await editor.createFile(operation);
|
|
1329
|
+
case "update_file":
|
|
1330
|
+
if (!operation.diff) {
|
|
1331
|
+
return { status: "failed", output: "Error: diff is required for update_file" };
|
|
1332
|
+
}
|
|
1333
|
+
return await editor.updateFile(operation);
|
|
1334
|
+
case "delete_file":
|
|
1335
|
+
return await editor.deleteFile(operation);
|
|
1336
|
+
default:
|
|
1337
|
+
return { status: "failed", output: `Error: Unknown operation type: ${operation.type}` };
|
|
1338
|
+
}
|
|
1339
|
+
} catch (err) {
|
|
1340
|
+
console.error("hit unexpected error in apply patch tool", err);
|
|
1341
|
+
return { status: "failed", output: `apply_patch threw: ${String(err)}` };
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1278
1344
|
function createApplyPatchTool() {
|
|
1279
1345
|
return tool11({
|
|
1280
1346
|
name: "apply_patch",
|
|
1281
1347
|
description: "Applies a patch (create, update, or delete a file) to the workspace.",
|
|
1282
1348
|
parameters: ApplyPatchParameters,
|
|
1283
1349
|
needsApproval,
|
|
1284
|
-
execute:
|
|
1285
|
-
try {
|
|
1286
|
-
const needed = await requiredSkillForOperation(operation.path, operation.type);
|
|
1287
|
-
if (needed) {
|
|
1288
|
-
const content = await execute10({ skill: needed });
|
|
1289
|
-
return { status: "completed", output: content };
|
|
1290
|
-
}
|
|
1291
|
-
switch (operation.type) {
|
|
1292
|
-
case "create_file":
|
|
1293
|
-
if (!operation.diff) {
|
|
1294
|
-
return { status: "failed", output: "Error: diff is required for create_file" };
|
|
1295
|
-
}
|
|
1296
|
-
return await editor.createFile(operation);
|
|
1297
|
-
case "update_file":
|
|
1298
|
-
if (!operation.diff) {
|
|
1299
|
-
return { status: "failed", output: "Error: diff is required for update_file" };
|
|
1300
|
-
}
|
|
1301
|
-
return await editor.updateFile(operation);
|
|
1302
|
-
case "delete_file":
|
|
1303
|
-
return await editor.deleteFile(operation);
|
|
1304
|
-
default:
|
|
1305
|
-
return { status: "failed", output: `Error: Unknown operation type: ${operation.type}` };
|
|
1306
|
-
}
|
|
1307
|
-
} catch (err) {
|
|
1308
|
-
console.error("hit unexpected error in apply patch tool", err);
|
|
1309
|
-
return { status: "failed", output: `apply_patch threw: ${String(err)}` };
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1350
|
+
execute: execute11
|
|
1312
1351
|
});
|
|
1313
1352
|
}
|
|
1314
1353
|
|
|
@@ -1319,7 +1358,7 @@ import { z as z12 } from "zod";
|
|
|
1319
1358
|
var ToolParameters11 = z12.object({
|
|
1320
1359
|
path: z12.string().describe("Directory to switch into. Can be absolute or relative to current workspace.")
|
|
1321
1360
|
});
|
|
1322
|
-
async function
|
|
1361
|
+
async function execute12({ path: path8 }) {
|
|
1323
1362
|
try {
|
|
1324
1363
|
const target = resolvePath(trackedState.cwd, path8);
|
|
1325
1364
|
const stat = statSync(target);
|
|
@@ -1350,7 +1389,7 @@ var changeCwdTool = tool12({
|
|
|
1350
1389
|
name: "change_cwd",
|
|
1351
1390
|
description: "Changes the current working directory for subsequent tools. Accepts absolute or relative paths.",
|
|
1352
1391
|
parameters: ToolParameters11,
|
|
1353
|
-
execute:
|
|
1392
|
+
execute: execute12
|
|
1354
1393
|
});
|
|
1355
1394
|
|
|
1356
1395
|
// tools/files/egrepTool.ts
|
|
@@ -1457,7 +1496,7 @@ var IMAGE_EXTENSIONS = {
|
|
|
1457
1496
|
".webp": "image/webp",
|
|
1458
1497
|
".bmp": "image/bmp"
|
|
1459
1498
|
};
|
|
1460
|
-
async function
|
|
1499
|
+
async function execute13({ fileName }) {
|
|
1461
1500
|
try {
|
|
1462
1501
|
const normalized = String(fileName).replace(/\\/g, "/");
|
|
1463
1502
|
const m = normalized.match(/(?:^|\/)skills\/([A-Za-z0-9_-]+)\.md(?:$|[?#])/);
|
|
@@ -1483,7 +1522,7 @@ var readFileTool = tool16({
|
|
|
1483
1522
|
name: "read_file",
|
|
1484
1523
|
description: "Reads the contents of a specified file. If the file is an image, it returns a structured image object.",
|
|
1485
1524
|
parameters: ToolParameters15,
|
|
1486
|
-
execute:
|
|
1525
|
+
execute: execute13
|
|
1487
1526
|
});
|
|
1488
1527
|
|
|
1489
1528
|
// tools/general/codeInterpreterTool.ts
|
|
@@ -1505,7 +1544,7 @@ var codeInterpreterTool = tool17({
|
|
|
1505
1544
|
name: "code_interpreter",
|
|
1506
1545
|
description: "Executes Python or JavaScript code in a local environment. This is useful for data analysis, complex calculations, and more. All code will be executed in the current workspace.",
|
|
1507
1546
|
parameters: CodeInterpreterParameters,
|
|
1508
|
-
execute:
|
|
1547
|
+
execute: execute14,
|
|
1509
1548
|
needsApproval: needsApproval2
|
|
1510
1549
|
});
|
|
1511
1550
|
async function needsApproval2(runContext, { code, language }, callId) {
|
|
@@ -1527,7 +1566,7 @@ ${chalk8.bold.bgYellow.black(` Code interpreter (${language}) approval required:
|
|
|
1527
1566
|
}
|
|
1528
1567
|
return !autoApproved;
|
|
1529
1568
|
}
|
|
1530
|
-
async function
|
|
1569
|
+
async function execute14({ code, language }) {
|
|
1531
1570
|
const extension = language === "javascript" ? "js" : "py";
|
|
1532
1571
|
const interpreter = language === "javascript" ? "node" : "python3";
|
|
1533
1572
|
const tempFile = path6.join(trackedState.cwd, `.temp_code_${Date.now()}.${extension}`);
|
|
@@ -2104,7 +2143,7 @@ var ToolParameters17 = z30.object({
|
|
|
2104
2143
|
directoryName: z30.string().describe("The name of the directory to create the application in."),
|
|
2105
2144
|
template: z30.enum(["vanilla-ts", "vanilla", "react-ts", "react"]).optional().describe("The template to use for the new application. Defaults to vanilla-ts.").default("vanilla-ts")
|
|
2106
2145
|
});
|
|
2107
|
-
async function
|
|
2146
|
+
async function execute15({ directoryName, template }) {
|
|
2108
2147
|
const currentCwd = trackedState.cwd;
|
|
2109
2148
|
const resolvedPath = resolvePath(currentCwd, directoryName);
|
|
2110
2149
|
const isCurrentDir = resolvedPath === currentCwd;
|
|
@@ -2121,7 +2160,7 @@ async function execute14({ directoryName, template }) {
|
|
|
2121
2160
|
});
|
|
2122
2161
|
console.log(`Initializing new Git repository in ${resolvedPath}...`);
|
|
2123
2162
|
execSync3("git init", { cwd: resolvedPath, stdio: "ignore" });
|
|
2124
|
-
const switchedDir = await
|
|
2163
|
+
const switchedDir = await execute12({ path: resolvedPath });
|
|
2125
2164
|
return `Successfully created a new Harper application in '${resolvedPath}' using template '${template}' with a matching Git repository initialized. Use the readDir and readFile tools to inspect the contents of the application. ${switchedDir}.`;
|
|
2126
2165
|
} catch (error) {
|
|
2127
2166
|
let errorMsg = `Error creating new Harper application: ${error.message}`;
|
|
@@ -2138,7 +2177,7 @@ var createNewHarperApplicationTool = tool30({
|
|
|
2138
2177
|
name: "create_new_harper_application",
|
|
2139
2178
|
description: "Creates a new Harper application using the best available package manager (yarn/pnpm/bun/deno, falling back to npm).",
|
|
2140
2179
|
parameters: ToolParameters17,
|
|
2141
|
-
execute:
|
|
2180
|
+
execute: execute15
|
|
2142
2181
|
});
|
|
2143
2182
|
|
|
2144
2183
|
// tools/harper/getHarperConfigSchemaTool.ts
|
|
@@ -2671,6 +2710,19 @@ async function ensureApiKey() {
|
|
|
2671
2710
|
}
|
|
2672
2711
|
}
|
|
2673
2712
|
|
|
2713
|
+
// utils/shell/getStdin.ts
|
|
2714
|
+
async function getStdin() {
|
|
2715
|
+
if (process.stdin.isTTY) {
|
|
2716
|
+
return "";
|
|
2717
|
+
}
|
|
2718
|
+
let result = "";
|
|
2719
|
+
process.stdin.setEncoding("utf8");
|
|
2720
|
+
for await (const chunk of process.stdin) {
|
|
2721
|
+
result += chunk;
|
|
2722
|
+
}
|
|
2723
|
+
return result.trim();
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2674
2726
|
// utils/sessions/createSession.ts
|
|
2675
2727
|
import { MemorySession as MemorySession3 } from "@openai/agents";
|
|
2676
2728
|
|
|
@@ -3115,6 +3167,7 @@ async function main() {
|
|
|
3115
3167
|
parseArgs();
|
|
3116
3168
|
await ensureApiKey();
|
|
3117
3169
|
sayHi();
|
|
3170
|
+
const stdinPrompt = await getStdin();
|
|
3118
3171
|
const agent = trackedState.agent = new Agent2({
|
|
3119
3172
|
name: "Harper App Development Assistant",
|
|
3120
3173
|
model: isOpenAIModel(trackedState.model) ? trackedState.model : getModel(trackedState.model),
|
|
@@ -3123,12 +3176,20 @@ async function main() {
|
|
|
3123
3176
|
tools: createTools()
|
|
3124
3177
|
});
|
|
3125
3178
|
const session = trackedState.session = createSession(trackedState.sessionPath);
|
|
3179
|
+
let firstIteration = true;
|
|
3126
3180
|
while (true) {
|
|
3127
3181
|
let task = "";
|
|
3128
3182
|
let lastToolCallInfo = null;
|
|
3129
3183
|
trackedState.controller = new AbortController();
|
|
3130
3184
|
if (!trackedState.approvalState) {
|
|
3131
|
-
|
|
3185
|
+
if (firstIteration && stdinPrompt) {
|
|
3186
|
+
task = stdinPrompt;
|
|
3187
|
+
console.log(`${chalk13.bold(">")} ${task}
|
|
3188
|
+
`);
|
|
3189
|
+
} else {
|
|
3190
|
+
task = await askQuestion("> ");
|
|
3191
|
+
}
|
|
3192
|
+
firstIteration = false;
|
|
3132
3193
|
if (!task) {
|
|
3133
3194
|
trackedState.emptyLines += 1;
|
|
3134
3195
|
if (trackedState.emptyLines >= 2) {
|
|
@@ -3145,7 +3206,7 @@ async function main() {
|
|
|
3145
3206
|
session,
|
|
3146
3207
|
stream: true,
|
|
3147
3208
|
signal: trackedState.controller.signal,
|
|
3148
|
-
maxTurns:
|
|
3209
|
+
maxTurns: trackedState.maxTurns
|
|
3149
3210
|
});
|
|
3150
3211
|
trackedState.approvalState = null;
|
|
3151
3212
|
let hasStartedResponse = false;
|
|
@@ -3222,6 +3283,26 @@ ${chalk13.yellow("\u{1F6E0}\uFE0F")} ${chalk13.cyan(name)}${chalk13.dim(display
|
|
|
3222
3283
|
trackedState.model || "gpt-5.2",
|
|
3223
3284
|
trackedState.compactionModel || "gpt-4o-mini"
|
|
3224
3285
|
);
|
|
3286
|
+
if (trackedState.maxCost !== null) {
|
|
3287
|
+
const estimatedTotalCost = costTracker.getEstimatedTotalCost(
|
|
3288
|
+
stream.state.usage,
|
|
3289
|
+
trackedState.model || "gpt-5.2",
|
|
3290
|
+
trackedState.compactionModel || "gpt-4o-mini"
|
|
3291
|
+
);
|
|
3292
|
+
if (estimatedTotalCost > trackedState.maxCost) {
|
|
3293
|
+
spinner.stop();
|
|
3294
|
+
console.log(
|
|
3295
|
+
chalk13.red(
|
|
3296
|
+
`Cost limit exceeded: $${estimatedTotalCost.toFixed(4)} > $${trackedState.maxCost.toFixed(4)}`
|
|
3297
|
+
)
|
|
3298
|
+
);
|
|
3299
|
+
if (trackedState.controller) {
|
|
3300
|
+
trackedState.controller.abort();
|
|
3301
|
+
}
|
|
3302
|
+
process.exitCode = 1;
|
|
3303
|
+
return handleExit();
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3225
3306
|
}
|
|
3226
3307
|
if (stream.interruptions?.length) {
|
|
3227
3308
|
for (const interruption of stream.interruptions) {
|
|
@@ -3250,6 +3331,19 @@ ${chalk13.yellow("\u{1F6E0}\uFE0F")} ${chalk13.cyan(name)}${chalk13.dim(display
|
|
|
3250
3331
|
stream.state.usage,
|
|
3251
3332
|
trackedState.compactionModel || "gpt-4o-mini"
|
|
3252
3333
|
);
|
|
3334
|
+
if (trackedState.maxCost !== null && costTracker.getTotalCost() > trackedState.maxCost) {
|
|
3335
|
+
spinner.stop();
|
|
3336
|
+
console.log(
|
|
3337
|
+
chalk13.red(
|
|
3338
|
+
`Cost limit exceeded: $${costTracker.getTotalCost().toFixed(4)} > $${trackedState.maxCost.toFixed(4)}`
|
|
3339
|
+
)
|
|
3340
|
+
);
|
|
3341
|
+
process.exitCode = 1;
|
|
3342
|
+
return handleExit();
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
if (stdinPrompt) {
|
|
3346
|
+
return handleExit();
|
|
3253
3347
|
}
|
|
3254
3348
|
} catch (error) {
|
|
3255
3349
|
spinner.stop();
|