@oh-my-pi/pi-coding-agent 14.7.4 → 14.7.6
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/CHANGELOG.md +26 -0
- package/package.json +7 -7
- package/src/cli.ts +7 -1
- package/src/eval/js/context-manager.ts +75 -3
- package/src/modes/controllers/input-controller.ts +1 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/modes/interactive-mode.ts +32 -4
- package/src/modes/loop-limit.ts +140 -0
- package/src/modes/types.ts +3 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/sdk.ts +21 -5
- package/src/session/agent-session.ts +1 -0
- package/src/slash-commands/builtin-registry.ts +4 -2
- package/src/system-prompt.ts +106 -91
- package/src/task/executor.ts +6 -0
- package/src/task/index.ts +4 -0
- package/src/tools/fetch.ts +10 -9
- package/src/tools/index.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [14.7.6] - 2026-05-07
|
|
6
|
+
### Changed
|
|
7
|
+
|
|
8
|
+
- Changed the "Hide Thinking Blocks" setting (Ctrl+T) to also instruct the provider to omit thinking/reasoning summaries from responses, instead of just hiding them client-side. Anthropic sees `thinking.display = "omitted"` (where supported); OpenAI Responses / Azure / Codex requests drop `reasoning.summary` entirely.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Fixed the `Hide Thinking Blocks` toggle so changing it updates the active session’s request settings immediately, ensuring new responses reflect the current hide-thinking preference
|
|
13
|
+
- Fixed system prompt preparation to keep successful context data and only fall back to minimal defaults for preparation steps that fail
|
|
14
|
+
- Fixed system prompt preparation timeout to apply per-step instead of all-or-nothing: a single slow step (e.g. `buildAgentsMdSearch` on a huge directory tree, `buildWorkspaceTree`, `loadProjectContextFiles`) now falls back to its own minimal default while the other steps still populate, and the warning names which steps timed out.
|
|
15
|
+
- Fixed subagents re-running expensive workspace scans (`buildAgentsMdSearch`, `buildWorkspaceTree`) on every spawn: parents now forward their already-resolved `AGENTS.md` search and workspace tree to subagents through `createAgentSession`, matching how `contextFiles`, `skills`, and `promptTemplates` are already inherited. On large monorepos this removes seconds of redundant work per `task` invocation and prevents the per-subagent system-prompt timeout warnings.
|
|
16
|
+
|
|
17
|
+
## [14.7.5] - 2026-05-07
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Added optional `/loop` limits: `/loop 10` stops after 10 auto-iterations, while duration forms such as `/loop 10m` and `/loop 10min` stop after the time limit.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Changed `/loop` to include the configured limit and remaining budget in the enabled status message
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Fixed `/loop` handling of malformed count or duration arguments by showing usage errors instead of enabling unbounded loop mode
|
|
29
|
+
- Fixed inherited disabled macOS malloc stack logging variables leaking into shell sessions and spamming Bun subprocess output with `MallocStackLogging` warnings.
|
|
30
|
+
|
|
5
31
|
## [14.7.4] - 2026-05-07
|
|
6
32
|
|
|
7
33
|
### Breaking Changes
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "14.7.
|
|
4
|
+
"version": "14.7.6",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@agentclientprotocol/sdk": "0.21.0",
|
|
48
48
|
"@mozilla/readability": "^0.6.0",
|
|
49
|
-
"@oh-my-pi/omp-stats": "14.7.
|
|
50
|
-
"@oh-my-pi/pi-agent-core": "14.7.
|
|
51
|
-
"@oh-my-pi/pi-ai": "14.7.
|
|
52
|
-
"@oh-my-pi/pi-natives": "14.7.
|
|
53
|
-
"@oh-my-pi/pi-tui": "14.7.
|
|
54
|
-
"@oh-my-pi/pi-utils": "14.7.
|
|
49
|
+
"@oh-my-pi/omp-stats": "14.7.6",
|
|
50
|
+
"@oh-my-pi/pi-agent-core": "14.7.6",
|
|
51
|
+
"@oh-my-pi/pi-ai": "14.7.6",
|
|
52
|
+
"@oh-my-pi/pi-natives": "14.7.6",
|
|
53
|
+
"@oh-my-pi/pi-tui": "14.7.6",
|
|
54
|
+
"@oh-my-pi/pi-utils": "14.7.6",
|
|
55
55
|
"@puppeteer/browsers": "^2.13.0",
|
|
56
56
|
"@sinclair/typebox": "^0.34.49",
|
|
57
57
|
"@types/turndown": "5.0.6",
|
package/src/cli.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { APP_NAME, MIN_BUN_VERSION, VERSION } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { APP_NAME, MIN_BUN_VERSION, procmgr, VERSION } from "@oh-my-pi/pi-utils";
|
|
3
|
+
|
|
4
|
+
// Strip macOS malloc-stack-logging env vars before any subprocess is spawned.
|
|
5
|
+
// Otherwise every child bun process (subagents, plugin installs, ptree spawns,
|
|
6
|
+
// etc.) prints a `MallocStackLogging: can't turn off …` warning to stderr.
|
|
7
|
+
procmgr.scrubProcessEnv();
|
|
8
|
+
|
|
3
9
|
/**
|
|
4
10
|
* CLI entry point — registers all commands explicitly and delegates to the
|
|
5
11
|
* lightweight CLI runner from pi-utils.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import * as path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
3
5
|
import * as util from "node:util";
|
|
4
6
|
import * as vm from "node:vm";
|
|
5
7
|
|
|
@@ -417,6 +419,8 @@ async function createVmState(
|
|
|
417
419
|
btoa,
|
|
418
420
|
Buffer,
|
|
419
421
|
process: createProcessSubset(cwd),
|
|
422
|
+
require: buildRequire(cwd),
|
|
423
|
+
createRequire,
|
|
420
424
|
fs,
|
|
421
425
|
fetch,
|
|
422
426
|
Blob,
|
|
@@ -478,13 +482,81 @@ async function runQueued<T>(state: VmContextState, work: () => Promise<T>): Prom
|
|
|
478
482
|
}
|
|
479
483
|
}
|
|
480
484
|
|
|
485
|
+
function buildRequire(cwd: string): NodeJS.Require {
|
|
486
|
+
// Anchor `require` resolution at the session cwd. The filename does not need to exist;
|
|
487
|
+
// Node only uses it as a base for module resolution.
|
|
488
|
+
return createRequire(pathToFileURL(path.join(cwd, "[eval]")).href);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Static `import ... from "x"` is not valid inside vm.runInContext. Rewrite the common
|
|
492
|
+
// forms to dynamic `await import(...)` so users can paste ESM-style imports verbatim.
|
|
493
|
+
const STATIC_IMPORT_RE = /^[ \t]*import\b(?:[ \t]+([^'"\n]+?)[ \t]+from)?[ \t]*(['"])([^'"\n]+)\2[ \t]*;?[ \t]*$/gm;
|
|
494
|
+
|
|
495
|
+
function splitTopLevel(clause: string): string[] {
|
|
496
|
+
const out: string[] = [];
|
|
497
|
+
let depth = 0;
|
|
498
|
+
let buf = "";
|
|
499
|
+
for (const ch of clause) {
|
|
500
|
+
if (ch === "{") depth++;
|
|
501
|
+
else if (ch === "}") depth--;
|
|
502
|
+
if (ch === "," && depth === 0) {
|
|
503
|
+
if (buf.trim()) out.push(buf.trim());
|
|
504
|
+
buf = "";
|
|
505
|
+
} else {
|
|
506
|
+
buf += ch;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (buf.trim()) out.push(buf.trim());
|
|
510
|
+
return out;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function rewriteImportClause(clause: string, sourceLiteral: string): string {
|
|
514
|
+
let defaultName: string | undefined;
|
|
515
|
+
let namespaceName: string | undefined;
|
|
516
|
+
let namedBlock: string | undefined;
|
|
517
|
+
for (const part of splitTopLevel(clause)) {
|
|
518
|
+
if (part.startsWith("{")) {
|
|
519
|
+
namedBlock = part;
|
|
520
|
+
} else if (part.startsWith("*")) {
|
|
521
|
+
const m = part.match(/^\*\s+as\s+([A-Za-z_$][\w$]*)$/);
|
|
522
|
+
if (!m) return `await import(${sourceLiteral}); /* unrewritten import: ${clause} */`;
|
|
523
|
+
namespaceName = m[1];
|
|
524
|
+
} else if (/^[A-Za-z_$][\w$]*$/.test(part)) {
|
|
525
|
+
defaultName = part;
|
|
526
|
+
} else {
|
|
527
|
+
return `await import(${sourceLiteral}); /* unrewritten import: ${clause} */`;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (namedBlock) {
|
|
531
|
+
const inner = namedBlock.slice(1, -1).trim();
|
|
532
|
+
const renamed = inner.replace(/([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)/g, "$1: $2");
|
|
533
|
+
const props = defaultName ? `default: ${defaultName}, ${renamed}` : renamed;
|
|
534
|
+
return `const { ${props} } = await import(${sourceLiteral});`;
|
|
535
|
+
}
|
|
536
|
+
if (namespaceName && defaultName) {
|
|
537
|
+
return `const ${namespaceName} = await import(${sourceLiteral}); const ${defaultName} = ${namespaceName}.default;`;
|
|
538
|
+
}
|
|
539
|
+
if (namespaceName) return `const ${namespaceName} = await import(${sourceLiteral});`;
|
|
540
|
+
if (defaultName) return `const ${defaultName} = (await import(${sourceLiteral})).default;`;
|
|
541
|
+
return `await import(${sourceLiteral});`;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export function rewriteStaticImports(code: string): string {
|
|
545
|
+
return code.replace(STATIC_IMPORT_RE, (_match, clause: string | undefined, _quote, source: string) => {
|
|
546
|
+
const literal = JSON.stringify(source);
|
|
547
|
+
if (!clause) return `await import(${literal});`;
|
|
548
|
+
return rewriteImportClause(clause.trim(), literal);
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
481
552
|
function wrapCode(code: string): { source: string; asyncWrapped: boolean } {
|
|
482
|
-
const
|
|
553
|
+
const rewritten = rewriteStaticImports(code);
|
|
554
|
+
const needsAsyncWrapper = /\bawait\b|\breturn\b/.test(rewritten);
|
|
483
555
|
if (!needsAsyncWrapper) {
|
|
484
|
-
return { source:
|
|
556
|
+
return { source: rewritten, asyncWrapped: false };
|
|
485
557
|
}
|
|
486
558
|
return {
|
|
487
|
-
source: `(async () => {\n${
|
|
559
|
+
source: `(async () => {\n${rewritten}\n})()`,
|
|
488
560
|
asyncWrapped: true,
|
|
489
561
|
};
|
|
490
562
|
}
|
|
@@ -701,6 +701,7 @@ export class InputController {
|
|
|
701
701
|
toggleThinkingBlockVisibility(): void {
|
|
702
702
|
this.ctx.hideThinkingBlock = !this.ctx.hideThinkingBlock;
|
|
703
703
|
settings.set("hideThinkingBlock", this.ctx.hideThinkingBlock);
|
|
704
|
+
this.ctx.session.agent.hideThinkingSummary = this.ctx.hideThinkingBlock;
|
|
704
705
|
|
|
705
706
|
// Rebuild chat from session messages
|
|
706
707
|
this.ctx.chatContainer.clear();
|
|
@@ -270,6 +270,7 @@ export class SelectorController {
|
|
|
270
270
|
break;
|
|
271
271
|
case "hideThinking":
|
|
272
272
|
this.ctx.hideThinkingBlock = value as boolean;
|
|
273
|
+
this.ctx.session.agent.hideThinkingSummary = value as boolean;
|
|
273
274
|
for (const child of this.ctx.chatContainer.children) {
|
|
274
275
|
if (child instanceof AssistantMessageComponent) {
|
|
275
276
|
child.setHideThinkingBlock(value as boolean);
|
|
@@ -73,6 +73,15 @@ import { MCPCommandController } from "./controllers/mcp-command-controller";
|
|
|
73
73
|
import { SelectorController } from "./controllers/selector-controller";
|
|
74
74
|
import { SSHCommandController } from "./controllers/ssh-command-controller";
|
|
75
75
|
import { TodoCommandController } from "./controllers/todo-command-controller";
|
|
76
|
+
import {
|
|
77
|
+
consumeLoopLimitIteration,
|
|
78
|
+
createLoopLimitRuntime,
|
|
79
|
+
describeLoopLimit,
|
|
80
|
+
describeLoopLimitRuntime,
|
|
81
|
+
isLoopDurationExpired,
|
|
82
|
+
type LoopLimitRuntime,
|
|
83
|
+
parseLoopLimitArgs,
|
|
84
|
+
} from "./loop-limit";
|
|
76
85
|
import { OAuthManualInputManager } from "./oauth-manual-input";
|
|
77
86
|
import { SessionObserverRegistry } from "./session-observer-registry";
|
|
78
87
|
import type { Theme } from "./theme/theme";
|
|
@@ -158,6 +167,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
158
167
|
planModePlanFilePath: string | undefined = undefined;
|
|
159
168
|
loopModeEnabled = false;
|
|
160
169
|
loopPrompt: string | undefined = undefined;
|
|
170
|
+
loopLimit: LoopLimitRuntime | undefined = undefined;
|
|
161
171
|
#loopAutoSubmitTimer: NodeJS.Timeout | undefined;
|
|
162
172
|
todoPhases: TodoPhase[] = [];
|
|
163
173
|
hideThinkingBlock = false;
|
|
@@ -535,25 +545,35 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
535
545
|
}
|
|
536
546
|
|
|
537
547
|
async #runLoopIteration(action: "prompt" | "compact" | "reset", prompt: string): Promise<void> {
|
|
548
|
+
if (!consumeLoopLimitIteration(this.loopLimit)) {
|
|
549
|
+
this.disableLoopMode("Loop limit reached. Loop mode disabled.");
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
538
553
|
if (action === "compact") {
|
|
539
554
|
await this.handleCompactCommand();
|
|
540
555
|
} else if (action === "reset") {
|
|
541
556
|
await this.handleClearCommand();
|
|
542
557
|
}
|
|
543
558
|
if (!this.loopModeEnabled || !this.onInputCallback) return;
|
|
559
|
+
if (isLoopDurationExpired(this.loopLimit)) {
|
|
560
|
+
this.disableLoopMode("Loop time limit reached. Loop mode disabled.");
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
544
563
|
this.onInputCallback(this.startPendingSubmission({ text: prompt }));
|
|
545
564
|
}
|
|
546
565
|
|
|
547
|
-
disableLoopMode(): void {
|
|
566
|
+
disableLoopMode(message = "Loop mode disabled."): void {
|
|
548
567
|
const wasEnabled = this.loopModeEnabled;
|
|
549
568
|
this.loopModeEnabled = false;
|
|
550
569
|
this.loopPrompt = undefined;
|
|
570
|
+
this.loopLimit = undefined;
|
|
551
571
|
this.#cancelLoopAutoSubmit();
|
|
552
572
|
this.statusLine.setLoopModeStatus(undefined);
|
|
553
573
|
this.updateEditorTopBorder();
|
|
554
574
|
this.ui.requestRender();
|
|
555
575
|
if (wasEnabled) {
|
|
556
|
-
this.showStatus(
|
|
576
|
+
this.showStatus(message);
|
|
557
577
|
}
|
|
558
578
|
}
|
|
559
579
|
|
|
@@ -567,18 +587,26 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
567
587
|
this.#cancelLoopAutoSubmit();
|
|
568
588
|
}
|
|
569
589
|
|
|
570
|
-
async handleLoopCommand(): Promise<void> {
|
|
590
|
+
async handleLoopCommand(args = ""): Promise<void> {
|
|
571
591
|
if (this.loopModeEnabled) {
|
|
572
592
|
this.disableLoopMode();
|
|
573
593
|
return;
|
|
574
594
|
}
|
|
595
|
+
const parsedLimit = parseLoopLimitArgs(args);
|
|
596
|
+
if (typeof parsedLimit === "string") {
|
|
597
|
+
this.showError(parsedLimit);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
575
600
|
this.loopModeEnabled = true;
|
|
576
601
|
this.loopPrompt = undefined;
|
|
602
|
+
this.loopLimit = createLoopLimitRuntime(parsedLimit);
|
|
577
603
|
this.statusLine.setLoopModeStatus({ enabled: true });
|
|
578
604
|
this.updateEditorTopBorder();
|
|
579
605
|
this.ui.requestRender();
|
|
606
|
+
const limitSuffix = parsedLimit ? ` Limited to ${describeLoopLimit(parsedLimit)}.` : "";
|
|
607
|
+
const remainingSuffix = this.loopLimit ? ` ${describeLoopLimitRuntime(this.loopLimit)}.` : "";
|
|
580
608
|
this.showStatus(
|
|
581
|
-
|
|
609
|
+
`Loop mode enabled.${limitSuffix}${remainingSuffix} Your next prompt will repeat after each turn. Esc cancels the current iteration; /loop again to disable.`,
|
|
582
610
|
);
|
|
583
611
|
}
|
|
584
612
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export type LoopLimitConfig =
|
|
2
|
+
| {
|
|
3
|
+
kind: "iterations";
|
|
4
|
+
iterations: number;
|
|
5
|
+
}
|
|
6
|
+
| {
|
|
7
|
+
kind: "duration";
|
|
8
|
+
durationMs: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type LoopLimitRuntime =
|
|
12
|
+
| {
|
|
13
|
+
kind: "iterations";
|
|
14
|
+
initial: number;
|
|
15
|
+
remaining: number;
|
|
16
|
+
}
|
|
17
|
+
| {
|
|
18
|
+
kind: "duration";
|
|
19
|
+
durationMs: number;
|
|
20
|
+
deadlineMs: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const TIME_UNITS_MS = new Map<string, number>([
|
|
24
|
+
["s", 1_000],
|
|
25
|
+
["sec", 1_000],
|
|
26
|
+
["secs", 1_000],
|
|
27
|
+
["second", 1_000],
|
|
28
|
+
["seconds", 1_000],
|
|
29
|
+
["m", 60_000],
|
|
30
|
+
["min", 60_000],
|
|
31
|
+
["mins", 60_000],
|
|
32
|
+
["minute", 60_000],
|
|
33
|
+
["minutes", 60_000],
|
|
34
|
+
["h", 3_600_000],
|
|
35
|
+
["hr", 3_600_000],
|
|
36
|
+
["hrs", 3_600_000],
|
|
37
|
+
["hour", 3_600_000],
|
|
38
|
+
["hours", 3_600_000],
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
export function parseLoopLimitArgs(args: string): LoopLimitConfig | undefined | string {
|
|
42
|
+
const trimmed = args.trim().toLowerCase();
|
|
43
|
+
if (!trimmed) return undefined;
|
|
44
|
+
|
|
45
|
+
const parts = trimmed.split(/\s+/);
|
|
46
|
+
if (parts.length > 2) {
|
|
47
|
+
return "Usage: /loop [count|duration]. Examples: /loop 10, /loop 10m, /loop 10min.";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (parts.length === 2) {
|
|
51
|
+
return parseDurationParts(parts[0], parts[1]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const token = parts[0];
|
|
55
|
+
const iterationMatch = /^(\d+)$/.exec(token);
|
|
56
|
+
if (iterationMatch) {
|
|
57
|
+
const iterations = Number(iterationMatch[1]);
|
|
58
|
+
if (!Number.isSafeInteger(iterations) || iterations <= 0) {
|
|
59
|
+
return "Loop count must be a positive integer.";
|
|
60
|
+
}
|
|
61
|
+
return { kind: "iterations", iterations };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const durationMatch = /^(\d+)([a-z]+)$/.exec(token);
|
|
65
|
+
if (durationMatch) {
|
|
66
|
+
return parseDurationParts(durationMatch[1], durationMatch[2]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return "Usage: /loop [count|duration]. Examples: /loop 10, /loop 10m, /loop 10min.";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseDurationParts(amountText: string, unitText: string): LoopLimitConfig | string {
|
|
73
|
+
if (!/^\d+$/.test(amountText)) {
|
|
74
|
+
return "Loop duration must use a positive integer amount.";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const amount = Number(amountText);
|
|
78
|
+
if (!Number.isSafeInteger(amount) || amount <= 0) {
|
|
79
|
+
return "Loop duration must be positive.";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const unitMs = TIME_UNITS_MS.get(unitText);
|
|
83
|
+
if (unitMs === undefined) {
|
|
84
|
+
return "Loop duration unit must be seconds, minutes, or hours.";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { kind: "duration", durationMs: amount * unitMs };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createLoopLimitRuntime(
|
|
91
|
+
config: LoopLimitConfig | undefined,
|
|
92
|
+
nowMs = Date.now(),
|
|
93
|
+
): LoopLimitRuntime | undefined {
|
|
94
|
+
if (!config) return undefined;
|
|
95
|
+
if (config.kind === "iterations") {
|
|
96
|
+
return { kind: "iterations", initial: config.iterations, remaining: config.iterations };
|
|
97
|
+
}
|
|
98
|
+
return { kind: "duration", durationMs: config.durationMs, deadlineMs: nowMs + config.durationMs };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function consumeLoopLimitIteration(limit: LoopLimitRuntime | undefined, nowMs = Date.now()): boolean {
|
|
102
|
+
if (!limit) return true;
|
|
103
|
+
if (limit.kind === "duration") {
|
|
104
|
+
return nowMs < limit.deadlineMs;
|
|
105
|
+
}
|
|
106
|
+
if (limit.remaining <= 0) return false;
|
|
107
|
+
limit.remaining -= 1;
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function isLoopDurationExpired(limit: LoopLimitRuntime | undefined, nowMs = Date.now()): boolean {
|
|
112
|
+
return limit?.kind === "duration" && nowMs >= limit.deadlineMs;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function describeLoopLimit(config: LoopLimitConfig): string {
|
|
116
|
+
if (config.kind === "iterations") {
|
|
117
|
+
return `${config.iterations} ${config.iterations === 1 ? "iteration" : "iterations"}`;
|
|
118
|
+
}
|
|
119
|
+
return formatDuration(config.durationMs);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function describeLoopLimitRuntime(limit: LoopLimitRuntime): string {
|
|
123
|
+
if (limit.kind === "iterations") {
|
|
124
|
+
return `${limit.remaining} of ${limit.initial} ${limit.initial === 1 ? "iteration" : "iterations"} remaining`;
|
|
125
|
+
}
|
|
126
|
+
return `${formatDuration(limit.durationMs)} limit`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function formatDuration(durationMs: number): string {
|
|
130
|
+
if (durationMs % 3_600_000 === 0) {
|
|
131
|
+
const hours = durationMs / 3_600_000;
|
|
132
|
+
return `${hours} ${hours === 1 ? "hour" : "hours"}`;
|
|
133
|
+
}
|
|
134
|
+
if (durationMs % 60_000 === 0) {
|
|
135
|
+
const minutes = durationMs / 60_000;
|
|
136
|
+
return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
|
|
137
|
+
}
|
|
138
|
+
const seconds = durationMs / 1_000;
|
|
139
|
+
return `${seconds} ${seconds === 1 ? "second" : "seconds"}`;
|
|
140
|
+
}
|
package/src/modes/types.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type { HookInputComponent } from "./components/hook-input";
|
|
|
24
24
|
import type { HookSelectorComponent } from "./components/hook-selector";
|
|
25
25
|
import type { StatusLineComponent } from "./components/status-line";
|
|
26
26
|
import type { ToolExecutionHandle } from "./components/tool-execution";
|
|
27
|
+
import type { LoopLimitRuntime } from "./loop-limit";
|
|
27
28
|
import type { OAuthManualInputManager } from "./oauth-manual-input";
|
|
28
29
|
import type { Theme } from "./theme/theme";
|
|
29
30
|
|
|
@@ -86,6 +87,7 @@ export interface InteractiveModeContext {
|
|
|
86
87
|
planModeEnabled: boolean;
|
|
87
88
|
loopModeEnabled: boolean;
|
|
88
89
|
loopPrompt?: string;
|
|
90
|
+
loopLimit?: LoopLimitRuntime;
|
|
89
91
|
planModePlanFilePath?: string;
|
|
90
92
|
hideThinkingBlock: boolean;
|
|
91
93
|
pendingImages: ImageContent[];
|
|
@@ -248,7 +250,7 @@ export interface InteractiveModeContext {
|
|
|
248
250
|
openExternalEditor(): void;
|
|
249
251
|
registerExtensionShortcuts(): void;
|
|
250
252
|
handlePlanModeCommand(initialPrompt?: string): Promise<void>;
|
|
251
|
-
handleLoopCommand(): Promise<void>;
|
|
253
|
+
handleLoopCommand(args?: string): Promise<void>;
|
|
252
254
|
disableLoopMode(): void;
|
|
253
255
|
pauseLoop(): void;
|
|
254
256
|
handleExitPlanModeTool(details: ExitPlanModeDetails): Promise<void>;
|
|
@@ -50,7 +50,7 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
|
|
|
50
50
|
|
|
51
51
|
# URLs
|
|
52
52
|
|
|
53
|
-
Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom feeds, JSON endpoints, PDFs at URLs, and similar text-based resources. Returns clean reader-mode text/markdown — no browser required. Use a `:raw` suffix for untouched HTML
|
|
53
|
+
Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom feeds, JSON endpoints, PDFs at URLs, and similar text-based resources. Returns clean reader-mode text/markdown — no browser required. Use a `:raw` suffix for untouched HTML. URL line selectors mirror the file form (`:50`, `:50-100`, `:50+150`, `:raw`). If a URL would otherwise look like `host:port`, add a trailing slash before the selector (e.g. `https://example.com/:80`).
|
|
54
54
|
</instruction>
|
|
55
55
|
|
|
56
56
|
<critical>
|
package/src/sdk.ts
CHANGED
|
@@ -201,6 +201,10 @@ export interface CreateAgentSessionOptions {
|
|
|
201
201
|
rules?: Rule[];
|
|
202
202
|
/** Context files (AGENTS.md content). Default: discovered walking up from cwd */
|
|
203
203
|
contextFiles?: Array<{ path: string; content: string }>;
|
|
204
|
+
/** Pre-built AGENTS.md search (skips re-scanning the workspace; passed by parents to subagents). */
|
|
205
|
+
agentsMdSearch?: AgentsMdSearch;
|
|
206
|
+
/** Pre-built workspace tree (skips re-scanning; passed by parents to subagents). */
|
|
207
|
+
workspaceTree?: WorkspaceTree;
|
|
204
208
|
/** Prompt templates. Default: discovered from cwd/.omp/prompts/ + agentDir/prompts/ */
|
|
205
209
|
promptTemplates?: PromptTemplate[];
|
|
206
210
|
/** File-based slash commands. Default: discovered from commands/ directories */
|
|
@@ -687,11 +691,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
687
691
|
if (!options.modelRegistry) {
|
|
688
692
|
modelRegistry.refreshInBackground();
|
|
689
693
|
}
|
|
690
|
-
// Kick off AGENTS.md filesystem search in parallel —
|
|
691
|
-
// (
|
|
692
|
-
|
|
694
|
+
// Kick off AGENTS.md filesystem search and workspace tree in parallel — they are the slowest pieces of
|
|
695
|
+
// buildSystemPrompt (can be many seconds on large repos) and only need `cwd`, so they overlap with
|
|
696
|
+
// everything that follows. Subagents inherit the parent's resolved values via options.
|
|
697
|
+
const agentsMdSearchPromise: Promise<AgentsMdSearch> = options.agentsMdSearch
|
|
698
|
+
? Promise.resolve(options.agentsMdSearch)
|
|
699
|
+
: logger.time("buildAgentsMdSearch", buildAgentsMdSearch, cwd);
|
|
693
700
|
agentsMdSearchPromise.catch(() => {});
|
|
694
|
-
const workspaceTreePromise: Promise<WorkspaceTree> =
|
|
701
|
+
const workspaceTreePromise: Promise<WorkspaceTree> = options.workspaceTree
|
|
702
|
+
? Promise.resolve(options.workspaceTree)
|
|
703
|
+
: logger.time("buildWorkspaceTree", buildWorkspaceTree, cwd);
|
|
695
704
|
workspaceTreePromise.catch(() => {});
|
|
696
705
|
|
|
697
706
|
// Independent discoveries that depend only on cwd/agentDir — kicked off in parallel and awaited
|
|
@@ -889,7 +898,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
889
898
|
return { ttsrManager, rulebookRules, alwaysApplyRules };
|
|
890
899
|
});
|
|
891
900
|
|
|
892
|
-
const contextFiles = await
|
|
901
|
+
const [contextFiles, resolvedAgentsMdSearch, resolvedWorkspaceTree] = await Promise.all([
|
|
902
|
+
contextFilesPromise,
|
|
903
|
+
agentsMdSearchPromise,
|
|
904
|
+
workspaceTreePromise,
|
|
905
|
+
]);
|
|
893
906
|
|
|
894
907
|
let agent: Agent;
|
|
895
908
|
let session!: AgentSession;
|
|
@@ -973,6 +986,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
973
986
|
},
|
|
974
987
|
skipPythonPreflight: options.skipPythonPreflight,
|
|
975
988
|
contextFiles,
|
|
989
|
+
agentsMdSearch: resolvedAgentsMdSearch,
|
|
990
|
+
workspaceTree: resolvedWorkspaceTree,
|
|
976
991
|
skills,
|
|
977
992
|
eventBus,
|
|
978
993
|
outputSchema: options.outputSchema,
|
|
@@ -1637,6 +1652,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1637
1652
|
presencePenalty: settings.get("presencePenalty") >= 0 ? settings.get("presencePenalty") : undefined,
|
|
1638
1653
|
repetitionPenalty: settings.get("repetitionPenalty") >= 0 ? settings.get("repetitionPenalty") : undefined,
|
|
1639
1654
|
serviceTier: initialServiceTier,
|
|
1655
|
+
hideThinkingSummary: settings.get("hideThinkingBlock"),
|
|
1640
1656
|
kimiApiFormat: settings.get("providers.kimiApiFormat") ?? "anthropic",
|
|
1641
1657
|
preferWebsockets: preferOpenAICodexWebsockets,
|
|
1642
1658
|
getToolContext: tc => toolContextStore.getContext(tc),
|
|
@@ -6610,6 +6610,7 @@ export class AgentSession {
|
|
|
6610
6610
|
apiKey,
|
|
6611
6611
|
sessionId: this.sessionId,
|
|
6612
6612
|
reasoning: toReasoningEffort(this.thinkingLevel),
|
|
6613
|
+
hideThinkingSummary: this.agent.hideThinkingSummary,
|
|
6613
6614
|
serviceTier: this.serviceTier,
|
|
6614
6615
|
signal: args.signal,
|
|
6615
6616
|
toolChoice: "none",
|
|
@@ -119,8 +119,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
119
119
|
name: "loop",
|
|
120
120
|
description:
|
|
121
121
|
"Toggle loop mode. While enabled, the next prompt you send re-submits after every yield. Esc cancels the current iteration; /loop again to disable.",
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
inlineHint: "[count|duration]",
|
|
123
|
+
allowArgs: true,
|
|
124
|
+
handle: async (command, runtime) => {
|
|
125
|
+
await runtime.ctx.handleLoopCommand(command.args);
|
|
124
126
|
runtime.ctx.editor.setText("");
|
|
125
127
|
},
|
|
126
128
|
},
|
package/src/system-prompt.ts
CHANGED
|
@@ -449,106 +449,121 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
449
449
|
} = options;
|
|
450
450
|
const resolvedCwd = cwd ?? getProjectDir();
|
|
451
451
|
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
:
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
452
|
+
const prepDefaults = {
|
|
453
|
+
resolvedCustomPrompt: undefined as string | undefined,
|
|
454
|
+
resolvedAppendPrompt: undefined as string | undefined,
|
|
455
|
+
systemPromptCustomization: null as string | null,
|
|
456
|
+
contextFiles: dedupeExactContextFiles(providedContextFiles ?? []),
|
|
457
|
+
agentsMdSearch: {
|
|
458
|
+
scopePath: ".",
|
|
459
|
+
limit: AGENTS_MD_LIMIT,
|
|
460
|
+
pattern: `AGENTS.md depth ${AGENTS_MD_MIN_DEPTH}-${AGENTS_MD_MAX_DEPTH}`,
|
|
461
|
+
files: [] as string[],
|
|
462
|
+
} satisfies AgentsMdSearch,
|
|
463
|
+
skills: providedSkills ?? ([] as Skill[]),
|
|
464
|
+
workspaceTree: {
|
|
465
|
+
rootPath: resolvedCwd,
|
|
466
|
+
rendered: "",
|
|
467
|
+
truncated: false,
|
|
468
|
+
totalLines: 0,
|
|
469
|
+
} satisfies WorkspaceTree,
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const deadline = Bun.sleep(SYSTEM_PROMPT_PREP_TIMEOUT_MS).then(() => "__timeout__" as const);
|
|
473
|
+
const timedOut: string[] = [];
|
|
474
|
+
const failed: Array<{ name: string; error: unknown }> = [];
|
|
475
|
+
|
|
476
|
+
async function withDeadline<T>(name: string, work: Promise<T>, fallback: T): Promise<T> {
|
|
477
|
+
const tagged = work
|
|
478
|
+
.then(value => ({ kind: "ok" as const, value }))
|
|
479
|
+
.catch(error => ({ kind: "err" as const, error }));
|
|
480
|
+
const result = await Promise.race([tagged, deadline]);
|
|
481
|
+
if (result === "__timeout__") {
|
|
482
|
+
timedOut.push(name);
|
|
483
|
+
// Let the work continue in the background so its caches still warm; just log on completion.
|
|
484
|
+
void tagged.then(r => {
|
|
485
|
+
if (r.kind === "err") {
|
|
486
|
+
logger.warn("Background system prompt preparation step failed", { name, error: String(r.error) });
|
|
487
|
+
} else {
|
|
488
|
+
logger.debug("Background system prompt preparation step completed after timeout", { name });
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
return fallback;
|
|
492
|
+
}
|
|
493
|
+
if (result.kind === "err") {
|
|
494
|
+
failed.push({ name, error: result.error });
|
|
495
|
+
return fallback;
|
|
496
|
+
}
|
|
497
|
+
return result.value;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const systemPromptCustomizationPromise = logger.time("loadSystemPromptFiles", loadSystemPromptFiles, {
|
|
501
|
+
cwd: resolvedCwd,
|
|
502
|
+
});
|
|
503
|
+
const contextFilesPromise = providedContextFiles
|
|
504
|
+
? Promise.resolve(providedContextFiles)
|
|
505
|
+
: logger.time("loadProjectContextFiles", loadProjectContextFiles, { cwd: resolvedCwd });
|
|
506
|
+
const agentsMdSearchPromise =
|
|
507
|
+
providedAgentsMdSearch !== undefined
|
|
508
|
+
? Promise.resolve(providedAgentsMdSearch)
|
|
509
|
+
: logger.time("buildAgentsMdSearch", buildAgentsMdSearch, resolvedCwd);
|
|
510
|
+
const workspaceTreePromise =
|
|
511
|
+
providedWorkspaceTree !== undefined
|
|
512
|
+
? Promise.resolve(providedWorkspaceTree)
|
|
513
|
+
: logger.time("buildWorkspaceTree", buildWorkspaceTree, resolvedCwd);
|
|
514
|
+
const skillsPromise: Promise<Skill[]> =
|
|
515
|
+
providedSkills !== undefined
|
|
516
|
+
? Promise.resolve(providedSkills)
|
|
517
|
+
: skillsSettings?.enabled !== false
|
|
518
|
+
? loadSkills({ ...skillsSettings, cwd: resolvedCwd }).then(result => result.skills)
|
|
519
|
+
: Promise.resolve([]);
|
|
520
|
+
|
|
521
|
+
const [
|
|
522
|
+
resolvedCustomPrompt,
|
|
523
|
+
resolvedAppendPrompt,
|
|
524
|
+
systemPromptCustomization,
|
|
525
|
+
contextFiles,
|
|
526
|
+
agentsMdSearch,
|
|
527
|
+
skills,
|
|
528
|
+
workspaceTree,
|
|
529
|
+
] = await Promise.all([
|
|
530
|
+
withDeadline(
|
|
531
|
+
"customPrompt",
|
|
475
532
|
resolvePromptInput(customPrompt, "system prompt"),
|
|
533
|
+
prepDefaults.resolvedCustomPrompt,
|
|
534
|
+
),
|
|
535
|
+
withDeadline(
|
|
536
|
+
"appendSystemPrompt",
|
|
476
537
|
resolvePromptInput(appendSystemPrompt, "append system prompt"),
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
systemPromptCustomization,
|
|
487
|
-
contextFiles,
|
|
488
|
-
agentsMdSearch,
|
|
489
|
-
skills,
|
|
490
|
-
workspaceTree,
|
|
491
|
-
]) => ({
|
|
492
|
-
resolvedCustomPrompt,
|
|
493
|
-
resolvedAppendPrompt,
|
|
494
|
-
systemPromptCustomization,
|
|
495
|
-
contextFiles,
|
|
496
|
-
agentsMdSearch,
|
|
497
|
-
skills,
|
|
498
|
-
workspaceTree,
|
|
499
|
-
}),
|
|
500
|
-
);
|
|
501
|
-
})();
|
|
502
|
-
|
|
503
|
-
const prepResult = await Promise.race([
|
|
504
|
-
prepPromise
|
|
505
|
-
.then(value => ({ type: "ready" as const, value }))
|
|
506
|
-
.catch(error => ({ type: "error" as const, error })),
|
|
507
|
-
Bun.sleep(SYSTEM_PROMPT_PREP_TIMEOUT_MS).then(() => ({ type: "timeout" as const })),
|
|
538
|
+
prepDefaults.resolvedAppendPrompt,
|
|
539
|
+
),
|
|
540
|
+
withDeadline("loadSystemPromptFiles", systemPromptCustomizationPromise, prepDefaults.systemPromptCustomization),
|
|
541
|
+
withDeadline("loadProjectContextFiles", contextFilesPromise, prepDefaults.contextFiles).then(
|
|
542
|
+
dedupeExactContextFiles,
|
|
543
|
+
),
|
|
544
|
+
withDeadline("buildAgentsMdSearch", agentsMdSearchPromise, prepDefaults.agentsMdSearch),
|
|
545
|
+
withDeadline("loadSkills", skillsPromise, prepDefaults.skills),
|
|
546
|
+
withDeadline("buildWorkspaceTree", workspaceTreePromise, prepDefaults.workspaceTree),
|
|
508
547
|
]);
|
|
509
548
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
let systemPromptCustomization: string | null = null;
|
|
513
|
-
let contextFiles: Array<{ path: string; content: string; depth?: number }> = dedupeExactContextFiles(
|
|
514
|
-
providedContextFiles ?? [],
|
|
515
|
-
);
|
|
516
|
-
let agentsMdSearch: AgentsMdSearch = {
|
|
517
|
-
scopePath: ".",
|
|
518
|
-
limit: AGENTS_MD_LIMIT,
|
|
519
|
-
pattern: `AGENTS.md depth ${AGENTS_MD_MIN_DEPTH}-${AGENTS_MD_MAX_DEPTH}`,
|
|
520
|
-
files: [],
|
|
521
|
-
};
|
|
522
|
-
let workspaceTree: WorkspaceTree = {
|
|
523
|
-
rootPath: resolvedCwd,
|
|
524
|
-
rendered: "",
|
|
525
|
-
truncated: false,
|
|
526
|
-
totalLines: 0,
|
|
527
|
-
};
|
|
528
|
-
let skills: Skill[] = providedSkills ?? [];
|
|
529
|
-
|
|
530
|
-
if (prepResult.type === "timeout") {
|
|
531
|
-
logger.warn("System prompt preparation timed out; using minimal startup context", {
|
|
549
|
+
if (timedOut.length > 0) {
|
|
550
|
+
logger.warn("System prompt preparation steps timed out; using minimal fallback for those steps", {
|
|
532
551
|
cwd: resolvedCwd,
|
|
533
552
|
timeoutMs: SYSTEM_PROMPT_PREP_TIMEOUT_MS,
|
|
553
|
+
steps: timedOut,
|
|
534
554
|
});
|
|
535
555
|
process.stderr.write(
|
|
536
|
-
`Warning: system prompt preparation timed out after ${SYSTEM_PROMPT_PREP_TIMEOUT_MS}ms; using minimal
|
|
556
|
+
`Warning: system prompt preparation steps timed out after ${SYSTEM_PROMPT_PREP_TIMEOUT_MS}ms (${timedOut.join(", ")}); using minimal fallback for those steps.\n`,
|
|
537
557
|
);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
systemPromptCustomization = prepResult.value.systemPromptCustomization;
|
|
548
|
-
contextFiles = dedupeExactContextFiles(prepResult.value.contextFiles);
|
|
549
|
-
agentsMdSearch = prepResult.value.agentsMdSearch;
|
|
550
|
-
skills = prepResult.value.skills;
|
|
551
|
-
workspaceTree = prepResult.value.workspaceTree;
|
|
558
|
+
}
|
|
559
|
+
if (failed.length > 0) {
|
|
560
|
+
for (const { name, error } of failed) {
|
|
561
|
+
logger.warn("System prompt preparation step failed; using minimal fallback", {
|
|
562
|
+
cwd: resolvedCwd,
|
|
563
|
+
step: name,
|
|
564
|
+
error: String(error),
|
|
565
|
+
});
|
|
566
|
+
}
|
|
552
567
|
}
|
|
553
568
|
|
|
554
569
|
const date = new Date().toISOString().slice(0, 10);
|
package/src/task/executor.ts
CHANGED
|
@@ -28,11 +28,13 @@ import { createAgentSession, discoverAuthStorage } from "../sdk";
|
|
|
28
28
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
29
29
|
import type { AuthStorage } from "../session/auth-storage";
|
|
30
30
|
import { SessionManager } from "../session/session-manager";
|
|
31
|
+
import type { AgentsMdSearch } from "../system-prompt";
|
|
31
32
|
import { type ContextFileEntry, truncateTail } from "../tools";
|
|
32
33
|
import { jtdToJsonSchema, normalizeSchema } from "../tools/jtd-to-json-schema";
|
|
33
34
|
import { ToolAbortError } from "../tools/tool-errors";
|
|
34
35
|
import type { EventBus } from "../utils/event-bus";
|
|
35
36
|
import { buildNamedToolChoice } from "../utils/tool-choice";
|
|
37
|
+
import type { WorkspaceTree } from "../workspace-tree";
|
|
36
38
|
import { subprocessToolRegistry } from "./subprocess-tool-registry";
|
|
37
39
|
import {
|
|
38
40
|
type AgentDefinition,
|
|
@@ -158,6 +160,8 @@ export interface ExecutorOptions {
|
|
|
158
160
|
contextFiles?: ContextFileEntry[];
|
|
159
161
|
skills?: Skill[];
|
|
160
162
|
promptTemplates?: PromptTemplate[];
|
|
163
|
+
agentsMdSearch?: AgentsMdSearch;
|
|
164
|
+
workspaceTree?: WorkspaceTree;
|
|
161
165
|
mcpManager?: MCPManager;
|
|
162
166
|
authStorage?: AuthStorage;
|
|
163
167
|
modelRegistry?: ModelRegistry;
|
|
@@ -967,6 +971,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
967
971
|
contextFiles: options.contextFiles,
|
|
968
972
|
skills: options.skills,
|
|
969
973
|
promptTemplates: options.promptTemplates,
|
|
974
|
+
agentsMdSearch: options.agentsMdSearch,
|
|
975
|
+
workspaceTree: options.workspaceTree,
|
|
970
976
|
systemPrompt: defaultPrompt => [
|
|
971
977
|
prompt.render(subagentSystemPromptTemplate, {
|
|
972
978
|
base: defaultPrompt.join("\n\n"),
|
package/src/task/index.ts
CHANGED
|
@@ -864,6 +864,8 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
864
864
|
mcpManager: this.session.mcpManager,
|
|
865
865
|
contextFiles,
|
|
866
866
|
skills: availableSkills,
|
|
867
|
+
agentsMdSearch: this.session.agentsMdSearch,
|
|
868
|
+
workspaceTree: this.session.workspaceTree,
|
|
867
869
|
promptTemplates,
|
|
868
870
|
localProtocolOptions,
|
|
869
871
|
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
|
@@ -919,6 +921,8 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
919
921
|
mcpManager: this.session.mcpManager,
|
|
920
922
|
contextFiles,
|
|
921
923
|
skills: availableSkills,
|
|
924
|
+
agentsMdSearch: this.session.agentsMdSearch,
|
|
925
|
+
workspaceTree: this.session.workspaceTree,
|
|
922
926
|
promptTemplates,
|
|
923
927
|
localProtocolOptions,
|
|
924
928
|
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
package/src/tools/fetch.ts
CHANGED
|
@@ -137,9 +137,10 @@ export function isReadableUrlPath(value: string): boolean {
|
|
|
137
137
|
return /^https?:\/\//i.test(value) || /^www\./i.test(value);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
//
|
|
142
|
-
|
|
140
|
+
// URL line selectors mirror the file form: `:50`, `:50-100`, `:50+150`, `:raw`.
|
|
141
|
+
// If a URL would otherwise look like `host:port`, add a trailing slash before the selector
|
|
142
|
+
// (e.g. `https://example.com/:80` to read line 80 of the document at `https://example.com/`).
|
|
143
|
+
const URL_LINE_RANGE_RE = /^(\d+)(?:([-+])(\d+))?$/;
|
|
143
144
|
|
|
144
145
|
export interface ParsedReadUrlTarget {
|
|
145
146
|
path: string;
|
|
@@ -157,11 +158,11 @@ export function parseReadUrlTarget(readPath: string): ParsedReadUrlTarget | null
|
|
|
157
158
|
|
|
158
159
|
const selector = embedded?.sel;
|
|
159
160
|
const raw = selector === "raw";
|
|
160
|
-
const lineMatch = selector ? URL_LINE_RANGE_RE.exec(selector) : null;
|
|
161
|
+
const lineMatch = selector && selector !== "raw" ? URL_LINE_RANGE_RE.exec(selector) : null;
|
|
161
162
|
if (lineMatch) {
|
|
162
163
|
const startLine = Number.parseInt(lineMatch[1]!, 10);
|
|
163
164
|
if (startLine < 1) {
|
|
164
|
-
throw new ToolError("URL line selector 0 is invalid; lines are 1-indexed. Use :
|
|
165
|
+
throw new ToolError("URL line selector 0 is invalid; lines are 1-indexed. Use :1.");
|
|
165
166
|
}
|
|
166
167
|
const sep = lineMatch[2];
|
|
167
168
|
const rhs = lineMatch[3] ? Number.parseInt(lineMatch[3], 10) : undefined;
|
|
@@ -195,13 +196,13 @@ function tryExtractEmbeddedUrlSelector(readPath: string): { path: string; sel?:
|
|
|
195
196
|
}
|
|
196
197
|
|
|
197
198
|
const candidateSelector = readPath.slice(lastColonIndex + 1);
|
|
198
|
-
const
|
|
199
|
-
if (!
|
|
199
|
+
const basePath = readPath.slice(0, lastColonIndex);
|
|
200
|
+
if (!isReadableUrlPath(basePath)) {
|
|
200
201
|
return null;
|
|
201
202
|
}
|
|
202
203
|
|
|
203
|
-
const
|
|
204
|
-
if (!
|
|
204
|
+
const isEmbeddedSelector = candidateSelector === "raw" || URL_LINE_RANGE_RE.test(candidateSelector);
|
|
205
|
+
if (!isEmbeddedSelector) {
|
|
205
206
|
return null;
|
|
206
207
|
}
|
|
207
208
|
|
package/src/tools/index.ts
CHANGED
|
@@ -14,11 +14,13 @@ import type { PlanModeState } from "../plan-mode/state";
|
|
|
14
14
|
import type { AgentRegistry } from "../registry/agent-registry";
|
|
15
15
|
import type { CustomMessage } from "../session/messages";
|
|
16
16
|
import type { ToolChoiceQueue } from "../session/tool-choice-queue";
|
|
17
|
+
import type { AgentsMdSearch } from "../system-prompt";
|
|
17
18
|
import { TaskTool } from "../task";
|
|
18
19
|
import type { AgentOutputManager } from "../task/output-manager";
|
|
19
20
|
import type { DiscoverableTool, DiscoverableToolSearchIndex } from "../tool-discovery/tool-index";
|
|
20
21
|
import type { EventBus } from "../utils/event-bus";
|
|
21
22
|
import { WebSearchTool } from "../web/search";
|
|
23
|
+
import type { WorkspaceTree } from "../workspace-tree";
|
|
22
24
|
import { AskTool } from "./ask";
|
|
23
25
|
import { AstEditTool } from "./ast-edit";
|
|
24
26
|
import { AstGrepTool } from "./ast-grep";
|
|
@@ -120,6 +122,10 @@ export interface ToolSession {
|
|
|
120
122
|
skipPythonPreflight?: boolean;
|
|
121
123
|
/** Pre-loaded context files (AGENTS.md, etc) */
|
|
122
124
|
contextFiles?: ContextFileEntry[];
|
|
125
|
+
/** Pre-loaded AGENTS.md search (forwarded to subagents to skip re-scanning) */
|
|
126
|
+
agentsMdSearch?: AgentsMdSearch;
|
|
127
|
+
/** Pre-loaded workspace tree (forwarded to subagents to skip re-scanning) */
|
|
128
|
+
workspaceTree?: WorkspaceTree;
|
|
123
129
|
/** Pre-loaded skills */
|
|
124
130
|
skills?: Skill[];
|
|
125
131
|
/** Pre-loaded prompt templates */
|