next-issue 0.19.0 → 1.0.0
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 +7 -25
- package/dist/agents.mjs +1 -6
- package/dist/args.mjs +1 -9
- package/dist/cli.mjs +0 -1
- package/dist/config.mjs +2 -8
- package/dist/pipeline.mjs +31 -11
- package/dist/prompts.mjs +10 -0
- package/dist/state.mjs +1 -1
- package/package.json +1 -1
- package/dist/pi.mjs +0 -159
package/README.md
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# next-issue
|
|
2
2
|
|
|
3
3
|
A small harness that works through the open issues of a GitHub repository with
|
|
4
|
-
the Claude Agent SDK
|
|
5
|
-
repository.
|
|
4
|
+
the Claude Agent SDK. Run it inside a git repository.
|
|
6
5
|
|
|
7
6
|
```bash
|
|
8
7
|
npx -y next-issue@latest
|
|
@@ -33,7 +32,9 @@ For each open issue, oldest first:
|
|
|
33
32
|
local branch that fell behind the remote is fast-forwarded. A fast-forward
|
|
34
33
|
of the base that is not safe leaves the local base branch as it is and
|
|
35
34
|
writes a `base.behind` event. A new branch then starts from the remote base.
|
|
36
|
-
4. Let the implementer agent do the work, then commit and push.
|
|
35
|
+
4. Let the implementer agent do the work, then commit and push. When a git
|
|
36
|
+
hook stops a commit, the agent that made the change gets the hook output,
|
|
37
|
+
fixes it, and the harness commits again. The budget is `maxCommitFixes`.
|
|
37
38
|
5. Open a draft pull request that closes the issue.
|
|
38
39
|
6. Wait for the checks, once the pull request shows the pushed commit. A
|
|
39
40
|
pending state waits again, up to `checkTimeoutMinutes`. A read that fails
|
|
@@ -157,8 +158,7 @@ verdict.
|
|
|
157
158
|
|
|
158
159
|
Every agent can run a shell command. The implementer and the fixer run with the
|
|
159
160
|
permission checks off; the reviewer runs with a smaller tool set, but its shell
|
|
160
|
-
has no limit on the commands.
|
|
161
|
-
agent the reviewer only runs without the `edit` and `write` tools. They all start in the worktree, but nothing holds
|
|
161
|
+
has no limit on the commands. They all start in the worktree, but nothing holds
|
|
162
162
|
them there: they can run any command, with your rights, anywhere on the machine.
|
|
163
163
|
The prompt holds the title, the body and the comments of the issue, which come
|
|
164
164
|
from GitHub.
|
|
@@ -213,7 +213,6 @@ Three levels of console output:
|
|
|
213
213
|
- `gh`, authenticated with write access to the repository
|
|
214
214
|
- Anthropic credentials for the Claude Agent SDK: `ANTHROPIC_API_KEY`, or a
|
|
215
215
|
Claude subscription login
|
|
216
|
-
- With the `pi` agent: `pi` on the `PATH`, set up with a model instead
|
|
217
216
|
|
|
218
217
|
## Configuration
|
|
219
218
|
|
|
@@ -234,10 +233,10 @@ does not know all stop the run, so a typo cannot pass without a word.
|
|
|
234
233
|
|
|
235
234
|
```json
|
|
236
235
|
{
|
|
237
|
-
"agent": "claude",
|
|
238
236
|
"remote": "origin",
|
|
239
237
|
"issueLimit": 100,
|
|
240
238
|
"maxCiFixes": 5,
|
|
239
|
+
"maxCommitFixes": 10,
|
|
241
240
|
"maxReviewRounds": 5,
|
|
242
241
|
"checkIntervalSeconds": 15,
|
|
243
242
|
"checkGraceSeconds": 60,
|
|
@@ -267,10 +266,10 @@ does not know all stop the run, so a typo cannot pass without a word.
|
|
|
267
266
|
|
|
268
267
|
| Field | Default | Effect |
|
|
269
268
|
| --- | --- | --- |
|
|
270
|
-
| `agent` | `claude` | The agent that does the work: `claude` or `pi` |
|
|
271
269
|
| `remote` | `origin` | The git remote for the repository and the branches |
|
|
272
270
|
| `issueLimit` | `100` | The maximum number of open issues to read |
|
|
273
271
|
| `maxCiFixes` | `5` | The budget for check fixes per issue |
|
|
272
|
+
| `maxCommitFixes` | `10` | The budget for fixes of a commit that a git hook stopped, per issue |
|
|
274
273
|
| `maxReviewRounds` | `5` | The budget for review rounds per issue |
|
|
275
274
|
| `checkIntervalSeconds` | `15` | The time between two check states |
|
|
276
275
|
| `checkGraceSeconds` | `60` | The time to wait for the first check to show |
|
|
@@ -295,22 +294,6 @@ The log gives the reason for a skipped issue as `stop-label`, `not-ready`,
|
|
|
295
294
|
A role set to `null`, or with no entry in `models`, uses the default model of
|
|
296
295
|
the agent.
|
|
297
296
|
|
|
298
|
-
## The pi agent
|
|
299
|
-
|
|
300
|
-
With `"agent": "pi"` or `--agent pi`, each agent call runs `pi --mode json` in
|
|
301
|
-
the worktree with the prompt on standard input. pi loads your own settings,
|
|
302
|
-
extensions, skills and context files, as it does when you run it yourself. The
|
|
303
|
-
flag wins over the config file.
|
|
304
|
-
|
|
305
|
-
- The implementer and the fixer use your default pi tools.
|
|
306
|
-
- The reviewer runs with `--tools -edit,-write`, so it keeps `bash`.
|
|
307
|
-
- A model in `models` goes to `pi --model` as it is, so use a name that pi
|
|
308
|
-
knows.
|
|
309
|
-
- pi has no structured output, so the reviewer is asked to end its reply with
|
|
310
|
-
a JSON verdict. A reply with no valid verdict hands the issue to a person.
|
|
311
|
-
- A run fails when its last reply ends with an error, when pi exits with an
|
|
312
|
-
error, or when pi stops before the run settles.
|
|
313
|
-
|
|
314
297
|
Without a `setupCommand`, the harness looks in the new worktree for a lock file
|
|
315
298
|
and runs the install command that goes with it:
|
|
316
299
|
|
|
@@ -357,7 +340,6 @@ that a label still holds back stays as it was.
|
|
|
357
340
|
| `--force` | Replace an existing config file, with `init` |
|
|
358
341
|
| `--once` | Stop after the first handled issue |
|
|
359
342
|
| `--max <n>` | Handle at most n issues |
|
|
360
|
-
| `--agent <a>` | Run the agents with `claude` or `pi`, whatever the config file says |
|
|
361
343
|
| `--merge` | Squash-merge the pull request after the approval, delete the remote branch and fix a failure of the checks on the base branch |
|
|
362
344
|
| `--reset` | Drop the saved budgets and findings, and keep the rest of the state |
|
|
363
345
|
| `--json` | Print the machine summary instead of the text one |
|
package/dist/agents.mjs
CHANGED
|
@@ -2,11 +2,9 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { track } from "./exec.mjs";
|
|
4
4
|
import { emptyUsage } from "./observe.mjs";
|
|
5
|
-
import { runPi } from "./pi.mjs";
|
|
6
5
|
const REVIEWING = {
|
|
7
6
|
tools: ["Read", "Grep", "Glob", "Bash"],
|
|
8
7
|
permissionMode: "dontAsk",
|
|
9
|
-
piTools: "-edit,-write",
|
|
10
8
|
};
|
|
11
9
|
const CODING = {
|
|
12
10
|
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"],
|
|
@@ -15,9 +13,6 @@ const CODING = {
|
|
|
15
13
|
};
|
|
16
14
|
export { CODING, REVIEWING };
|
|
17
15
|
export async function runAgent(recorder, request) {
|
|
18
|
-
if (request.agent === "pi") {
|
|
19
|
-
return runPi(recorder, request);
|
|
20
|
-
}
|
|
21
16
|
const log = recorder.scope({ role: request.name });
|
|
22
17
|
const usage = emptyUsage();
|
|
23
18
|
const toolNames = new Map();
|
|
@@ -146,7 +141,7 @@ function main(models) {
|
|
|
146
141
|
return name;
|
|
147
142
|
}
|
|
148
143
|
export function toolFields(name, input) {
|
|
149
|
-
if (
|
|
144
|
+
if (name !== "Bash" || typeof input !== "object" || input === null) {
|
|
150
145
|
return { tool: name };
|
|
151
146
|
}
|
|
152
147
|
const { command } = input;
|
package/dist/args.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CONFIG_FILE } from "./config.mjs";
|
|
2
2
|
export const USAGE = `next-issue [command] [options]
|
|
3
3
|
|
|
4
4
|
init write ${CONFIG_FILE} with the defaults
|
|
5
5
|
|
|
6
6
|
--once stop after the first handled issue
|
|
7
7
|
--max <n> handle at most n issues
|
|
8
|
-
--agent <a> run the agents with claude or pi
|
|
9
8
|
--merge squash-merge the pull request after the approval
|
|
10
9
|
--reset drop the saved budgets and findings first
|
|
11
10
|
--json print the machine summary instead of the text one
|
|
@@ -57,13 +56,6 @@ export function parseArgs(argv) {
|
|
|
57
56
|
else if (arg === "--quiet") {
|
|
58
57
|
args.level = "quiet";
|
|
59
58
|
}
|
|
60
|
-
else if (arg === "--agent") {
|
|
61
|
-
const value = argv[++index];
|
|
62
|
-
if (value === undefined || !AGENT_NAMES.includes(value)) {
|
|
63
|
-
throw new Error("--agent needs claude or pi");
|
|
64
|
-
}
|
|
65
|
-
args.agent = value;
|
|
66
|
-
}
|
|
67
59
|
else if (arg === "--max") {
|
|
68
60
|
args.max = whole(argv[++index], arg);
|
|
69
61
|
}
|
package/dist/cli.mjs
CHANGED
|
@@ -29,7 +29,6 @@ async function main() {
|
|
|
29
29
|
return init(root, args.force);
|
|
30
30
|
}
|
|
31
31
|
const config = await loadConfig(root);
|
|
32
|
-
config.agent = args.agent ?? config.agent;
|
|
33
32
|
setDefaultTimeout(config.commandTimeoutMinutes * 60_000);
|
|
34
33
|
const repo = await detectRepo(root, config.remote);
|
|
35
34
|
const recorder = await Recorder.create(repo.root, args.level);
|
package/dist/config.mjs
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { message } from "./observe.mjs";
|
|
4
|
-
export const AGENT_NAMES = ["claude", "pi"];
|
|
5
4
|
const DEFAULTS = {
|
|
6
|
-
agent: "claude",
|
|
7
5
|
remote: "origin",
|
|
8
6
|
issueLimit: 100,
|
|
9
7
|
maxCiFixes: 5,
|
|
8
|
+
maxCommitFixes: 10,
|
|
10
9
|
maxReviewRounds: 5,
|
|
11
10
|
checkIntervalSeconds: 15,
|
|
12
11
|
checkGraceSeconds: 60,
|
|
@@ -35,6 +34,7 @@ const FIELDS = [...Object.keys(DEFAULTS), "setupCommand"];
|
|
|
35
34
|
const WHOLE_FIELDS = [
|
|
36
35
|
["issueLimit", 1],
|
|
37
36
|
["maxCiFixes", 0],
|
|
37
|
+
["maxCommitFixes", 0],
|
|
38
38
|
["maxReviewRounds", 1],
|
|
39
39
|
["checkIntervalSeconds", 1],
|
|
40
40
|
["checkGraceSeconds", 0],
|
|
@@ -93,12 +93,6 @@ function parseConfig(value) {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
const config = defaults();
|
|
96
|
-
if (file.agent !== undefined) {
|
|
97
|
-
if (typeof file.agent !== "string" || !AGENT_NAMES.includes(file.agent)) {
|
|
98
|
-
throw new Error(`${CONFIG_FILE}: agent must be claude or pi.`);
|
|
99
|
-
}
|
|
100
|
-
config.agent = file.agent;
|
|
101
|
-
}
|
|
102
96
|
if (file.remote !== undefined) {
|
|
103
97
|
config.remote = text(file.remote, "remote", false);
|
|
104
98
|
}
|
package/dist/pipeline.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { run } from "./exec.mjs";
|
|
|
4
4
|
import { addWorktree, branchName, commitAll, deleteBranch, diff, discardChanges, hasWorktree, isDirty, push, removeWorktree, resetToBase, revision, worktreePath, } from "./git.mjs";
|
|
5
5
|
import { commentOnPr, createPr, failedBaseLogs, failedCheckLogs, findPr, issueComments, markPrReady, mergeCommit, mergePr, openBlockers, reopenIssue, setLabel, waitForBaseChecks, waitForChecks, } from "./github.mjs";
|
|
6
6
|
import { message } from "./observe.mjs";
|
|
7
|
-
import { capDiff, fixPrompt, implementPrompt, parseCommitSubject, parseUnrelated, reviewPrompt, } from "./prompts.mjs";
|
|
7
|
+
import { capDiff, fixPrompt, hookPrompt, implementPrompt, parseCommitSubject, parseUnrelated, reviewPrompt, } from "./prompts.mjs";
|
|
8
8
|
import { detectSetupCommand } from "./setup.mjs";
|
|
9
9
|
import { dropState, readState, resetState, writeState, } from "./state.mjs";
|
|
10
10
|
import { blockingFindings, fingerprint, formatFindings, formatVerdict, openFindings, readVerdict, VERDICT_SCHEMA, } from "./verdict.mjs";
|
|
@@ -162,11 +162,10 @@ async function work(context, issue, log, state) {
|
|
|
162
162
|
prompt: implementPrompt(issue, comments),
|
|
163
163
|
profile: CODING,
|
|
164
164
|
model: config.models.implementer,
|
|
165
|
-
agent: config.agent,
|
|
166
165
|
timeoutMs: config.agentTimeoutMinutes * 60_000,
|
|
167
166
|
}));
|
|
168
167
|
const subject = parseCommitSubject(result.text, `fix: resolve issue #${issue.number}`);
|
|
169
|
-
if (!(await commitWork(job, before, subject))) {
|
|
168
|
+
if (!(await commitWork(job, before, subject, "implementer"))) {
|
|
170
169
|
await escalate(job, "The implementer produced no change.");
|
|
171
170
|
return finish(job, "needs-human", "no implementation commit");
|
|
172
171
|
}
|
|
@@ -276,7 +275,6 @@ async function reviewRound(job) {
|
|
|
276
275
|
prompt: reviewPrompt(issue, comments, patch, history(state.reviewLog)),
|
|
277
276
|
profile: REVIEWING,
|
|
278
277
|
model: config.models.reviewer,
|
|
279
|
-
agent: config.agent,
|
|
280
278
|
timeoutMs: config.agentTimeoutMinutes * 60_000,
|
|
281
279
|
outputSchema: VERDICT_SCHEMA,
|
|
282
280
|
}));
|
|
@@ -440,7 +438,6 @@ async function fixRound(job, reason, detail, earlier, kind) {
|
|
|
440
438
|
prompt: fixPrompt(issue, reason, detail, earlier, kind),
|
|
441
439
|
profile: CODING,
|
|
442
440
|
model: context.config.models.fixer,
|
|
443
|
-
agent: context.config.agent,
|
|
444
441
|
timeoutMs: context.config.agentTimeoutMinutes * 60_000,
|
|
445
442
|
}));
|
|
446
443
|
const unrelated = parseUnrelated(result.text);
|
|
@@ -450,19 +447,42 @@ async function fixRound(job, reason, detail, earlier, kind) {
|
|
|
450
447
|
return { committed: false, unrelated, stray };
|
|
451
448
|
}
|
|
452
449
|
const subject = parseCommitSubject(result.text, `fix: address feedback on issue #${issue.number}`);
|
|
453
|
-
if (!(await commitWork(job, before, subject))) {
|
|
450
|
+
if (!(await commitWork(job, before, subject, "fixer"))) {
|
|
454
451
|
log.event("commit.empty", { reason }, "quiet");
|
|
455
452
|
return { committed: false };
|
|
456
453
|
}
|
|
457
454
|
await ports.push(worktree, context.config.remote, branch);
|
|
458
455
|
return { committed: true };
|
|
459
456
|
}
|
|
460
|
-
async function commitWork(job, before, subject) {
|
|
461
|
-
const { context, issue, log, worktree } = job;
|
|
462
|
-
|
|
463
|
-
|
|
457
|
+
async function commitWork(job, before, subject, role) {
|
|
458
|
+
const { context, issue, log, state, worktree, comments } = job;
|
|
459
|
+
const { repo, config, ports } = context;
|
|
460
|
+
for (;;) {
|
|
461
|
+
try {
|
|
462
|
+
if (await ports.commitAll(worktree, `${subject}\n\nRefs #${issue.number}`)) {
|
|
463
|
+
log.event("commit", { subject });
|
|
464
|
+
}
|
|
465
|
+
return (await ports.revision(worktree)) !== before;
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
const used = state.commitFixes ?? 0;
|
|
469
|
+
if (used >= config.maxCommitFixes) {
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
state.commitFixes = used + 1;
|
|
473
|
+
await writeState(repo, state);
|
|
474
|
+
const output = message(error).slice(-config.logMaxChars);
|
|
475
|
+
log.event("commit.fail", { role, output }, "quiet");
|
|
476
|
+
await log.step("commit-fix", { role }, () => ports.runAgent(log, {
|
|
477
|
+
name: role,
|
|
478
|
+
cwd: worktree,
|
|
479
|
+
prompt: hookPrompt(issue, comments, role, output),
|
|
480
|
+
profile: CODING,
|
|
481
|
+
model: config.models[role],
|
|
482
|
+
timeoutMs: config.agentTimeoutMinutes * 60_000,
|
|
483
|
+
}));
|
|
484
|
+
}
|
|
464
485
|
}
|
|
465
|
-
return (await context.ports.revision(worktree)) !== before;
|
|
466
486
|
}
|
|
467
487
|
async function cleanUp(context, log, issue) {
|
|
468
488
|
const path = worktreePath(context.repo, issue);
|
package/dist/prompts.mjs
CHANGED
|
@@ -21,6 +21,16 @@ export function implementPrompt(issue, comments) {
|
|
|
21
21
|
COMMIT_RULES,
|
|
22
22
|
].join("\n\n");
|
|
23
23
|
}
|
|
24
|
+
export function hookPrompt(issue, comments, role, output) {
|
|
25
|
+
return [
|
|
26
|
+
`You are the ${role}. The harness could not commit your work for the issue below, because a git hook failed.`,
|
|
27
|
+
issueBlock(issue, comments),
|
|
28
|
+
`## Hook output\n${output}`,
|
|
29
|
+
"## Rules",
|
|
30
|
+
"Fix what the hook reports and keep the rest of the work in this worktree. Do not skip or weaken the hook.",
|
|
31
|
+
COMMIT_RULES,
|
|
32
|
+
].join("\n\n");
|
|
33
|
+
}
|
|
24
34
|
export function capDiff(diff, maxChars) {
|
|
25
35
|
return diff.length <= maxChars
|
|
26
36
|
? diff
|
package/dist/state.mjs
CHANGED
|
@@ -16,7 +16,7 @@ export async function readState(repo, issue) {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
export function resetState(state) {
|
|
19
|
-
return { ...state, ciFixes: 0, reviewRounds: 0, reviewLog: [] };
|
|
19
|
+
return { ...state, ciFixes: 0, commitFixes: 0, reviewRounds: 0, reviewLog: [] };
|
|
20
20
|
}
|
|
21
21
|
export async function writeState(repo, state) {
|
|
22
22
|
await mkdir(dir(repo), { recursive: true });
|
package/package.json
CHANGED
package/dist/pi.mjs
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import { toolFields } from "./agents.mjs";
|
|
2
|
-
import { run } from "./exec.mjs";
|
|
3
|
-
import { emptyUsage } from "./observe.mjs";
|
|
4
|
-
const NO_LIMIT = 2 ** 31 - 1;
|
|
5
|
-
const OUTPUT_TAIL = 200_000;
|
|
6
|
-
export async function runPi(recorder, request) {
|
|
7
|
-
const log = recorder.scope({ role: request.name });
|
|
8
|
-
const usage = emptyUsage();
|
|
9
|
-
let text = "";
|
|
10
|
-
let toolCalls = 0;
|
|
11
|
-
let turns = 0;
|
|
12
|
-
let costUsd = 0;
|
|
13
|
-
let model = request.model;
|
|
14
|
-
let sessionId;
|
|
15
|
-
let settled = false;
|
|
16
|
-
let last;
|
|
17
|
-
const read = (line) => {
|
|
18
|
-
let event;
|
|
19
|
-
try {
|
|
20
|
-
event = JSON.parse(line);
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
if (event.type === "session") {
|
|
26
|
-
sessionId = event.id;
|
|
27
|
-
}
|
|
28
|
-
else if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
29
|
-
const message = event.message;
|
|
30
|
-
last = message;
|
|
31
|
-
turns += 1;
|
|
32
|
-
model = message.model ?? model;
|
|
33
|
-
const input = message.usage?.input ?? 0;
|
|
34
|
-
const output = message.usage?.output ?? 0;
|
|
35
|
-
const cacheRead = message.usage?.cacheRead ?? 0;
|
|
36
|
-
const cacheWrite = message.usage?.cacheWrite ?? 0;
|
|
37
|
-
usage.input += input;
|
|
38
|
-
usage.output += output;
|
|
39
|
-
usage.cacheRead += cacheRead;
|
|
40
|
-
usage.cacheWrite += cacheWrite;
|
|
41
|
-
usage.total += input + output + cacheRead + cacheWrite;
|
|
42
|
-
costUsd += message.usage?.cost?.total ?? 0;
|
|
43
|
-
const said = textOf(message.content);
|
|
44
|
-
if (said.length > 0) {
|
|
45
|
-
text = said;
|
|
46
|
-
if (log.level === "verbose") {
|
|
47
|
-
process.stderr.write(said);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
else if (event.type === "tool_execution_start") {
|
|
52
|
-
toolCalls += 1;
|
|
53
|
-
log.event("tool", toolFields(event.toolName ?? "unknown", event.args));
|
|
54
|
-
}
|
|
55
|
-
else if (event.type === "tool_execution_end" && event.isError === true) {
|
|
56
|
-
log.event("tool.error", { tool: event.toolName }, "quiet");
|
|
57
|
-
}
|
|
58
|
-
else if (event.type === "agent_settled") {
|
|
59
|
-
settled = true;
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
const started = Date.now();
|
|
63
|
-
log.event("agent.start", { model: request.model, timeoutMs: request.timeoutMs, agent: "pi" }, "normal");
|
|
64
|
-
let rest = "";
|
|
65
|
-
const result = await run("pi", piArgs(request), {
|
|
66
|
-
cwd: request.cwd,
|
|
67
|
-
input: piPrompt(request),
|
|
68
|
-
timeoutMs: request.timeoutMs ?? NO_LIMIT,
|
|
69
|
-
tailChars: OUTPUT_TAIL,
|
|
70
|
-
onStdout: (chunk) => {
|
|
71
|
-
rest += chunk;
|
|
72
|
-
let end = rest.indexOf("\n");
|
|
73
|
-
while (end !== -1) {
|
|
74
|
-
read(rest.slice(0, end).replace(/\r$/, ""));
|
|
75
|
-
rest = rest.slice(end + 1);
|
|
76
|
-
end = rest.indexOf("\n");
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
|
-
});
|
|
80
|
-
if (rest.length > 0) {
|
|
81
|
-
read(rest);
|
|
82
|
-
}
|
|
83
|
-
const failed = (reason) => new Error(`The ${request.name} agent failed: ${reason}`);
|
|
84
|
-
if (result.timedOut) {
|
|
85
|
-
throw new Error(`The ${request.name} agent did not finish in ${request.timeoutMs} ms.`);
|
|
86
|
-
}
|
|
87
|
-
if (last?.stopReason === "error" || last?.stopReason === "aborted") {
|
|
88
|
-
throw failed(last.errorMessage ?? last.stopReason);
|
|
89
|
-
}
|
|
90
|
-
if (result.code !== 0) {
|
|
91
|
-
throw failed(`pi exited with code ${result.code}: ${result.stderr.trim()}`);
|
|
92
|
-
}
|
|
93
|
-
if (!settled) {
|
|
94
|
-
throw failed("pi stopped before the run settled");
|
|
95
|
-
}
|
|
96
|
-
if (last?.stopReason === "length") {
|
|
97
|
-
log.event("agent.incomplete", { subtype: "length" }, "quiet");
|
|
98
|
-
}
|
|
99
|
-
const ms = Date.now() - started;
|
|
100
|
-
recorder.usage(request.name, usage, ms, {
|
|
101
|
-
turns,
|
|
102
|
-
toolCalls,
|
|
103
|
-
model,
|
|
104
|
-
session: sessionId,
|
|
105
|
-
costUsd,
|
|
106
|
-
});
|
|
107
|
-
return {
|
|
108
|
-
text: text.trim(),
|
|
109
|
-
usage,
|
|
110
|
-
turns,
|
|
111
|
-
toolCalls,
|
|
112
|
-
ms,
|
|
113
|
-
costUsd,
|
|
114
|
-
model,
|
|
115
|
-
sessionId,
|
|
116
|
-
structured: request.outputSchema === undefined ? undefined : jsonOf(text),
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
function piArgs(request) {
|
|
120
|
-
return [
|
|
121
|
-
"--mode",
|
|
122
|
-
"json",
|
|
123
|
-
...(request.model === undefined ? [] : ["--model", request.model]),
|
|
124
|
-
...(request.profile.piTools === undefined ? [] : ["--tools", request.profile.piTools]),
|
|
125
|
-
];
|
|
126
|
-
}
|
|
127
|
-
function piPrompt(request) {
|
|
128
|
-
if (request.outputSchema === undefined) {
|
|
129
|
-
return request.prompt;
|
|
130
|
-
}
|
|
131
|
-
return [
|
|
132
|
-
request.prompt,
|
|
133
|
-
"",
|
|
134
|
-
"End your reply with only a JSON object in a json code block. The object must match this JSON schema:",
|
|
135
|
-
JSON.stringify(request.outputSchema),
|
|
136
|
-
].join("\n");
|
|
137
|
-
}
|
|
138
|
-
function textOf(content) {
|
|
139
|
-
if (typeof content === "string") {
|
|
140
|
-
return content;
|
|
141
|
-
}
|
|
142
|
-
if (!Array.isArray(content)) {
|
|
143
|
-
return "";
|
|
144
|
-
}
|
|
145
|
-
return content
|
|
146
|
-
.filter((block) => block?.type === "text" && typeof block.text === "string")
|
|
147
|
-
.map((block) => block.text)
|
|
148
|
-
.join("");
|
|
149
|
-
}
|
|
150
|
-
function jsonOf(text) {
|
|
151
|
-
const fenced = [...text.matchAll(/```(?:json)?[ \t]*\n([\s\S]*?)```/g)].at(-1)?.[1];
|
|
152
|
-
const candidate = fenced ?? text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1);
|
|
153
|
-
try {
|
|
154
|
-
return JSON.parse(candidate);
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
return undefined;
|
|
158
|
-
}
|
|
159
|
-
}
|