@oh-my-pi/pi-coding-agent 16.2.9 → 16.2.12
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 +60 -0
- package/dist/cli.js +3464 -3306
- package/dist/types/cli/bench-cli.d.ts +0 -5
- package/dist/types/cli/dry-balance-cli.d.ts +0 -5
- package/dist/types/cli/models-cli.d.ts +1 -1
- package/dist/types/config/inline-tool-descriptors-mode.d.ts +1 -2
- package/dist/types/config/model-registry.d.ts +1 -23
- package/dist/types/config/model-resolver.d.ts +10 -15
- package/dist/types/config/models-config-schema.d.ts +0 -6
- package/dist/types/config/models-config.d.ts +0 -6
- package/dist/types/config/settings-schema.d.ts +31 -1
- package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +1 -0
- package/dist/types/extensibility/extensions/runner.d.ts +15 -0
- package/dist/types/extensibility/skills.d.ts +29 -0
- package/dist/types/lsp/client.d.ts +9 -3
- package/dist/types/modes/components/todo-reminder.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +0 -1
- package/dist/types/modes/skill-command.d.ts +1 -1
- package/dist/types/modes/types.d.ts +0 -1
- package/dist/types/stt/asr-client.d.ts +7 -3
- package/dist/types/task/spawn-policy.d.ts +17 -0
- package/dist/types/task/spawn-policy.test.d.ts +1 -0
- package/dist/types/task/types.d.ts +8 -2
- package/dist/types/tools/__tests__/eval-description.test.d.ts +1 -0
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +2 -2
- package/dist/types/tools/browser/registry.d.ts +2 -0
- package/dist/types/tools/browser/run-cancellation.d.ts +4 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +30 -1
- package/dist/types/tools/eval.d.ts +3 -6
- package/dist/types/tools/write.d.ts +1 -1
- package/dist/types/utils/image-vision-fallback.d.ts +1 -1
- package/package.json +14 -13
- package/scripts/bundle-dist.ts +23 -4
- package/scripts/generate-docs-index.ts +116 -24
- package/src/advisor/__tests__/advisor.test.ts +1 -1
- package/src/async/job-manager.ts +27 -3
- package/src/auto-thinking/classifier.ts +1 -1
- package/src/cli/bench-cli.ts +3 -12
- package/src/cli/dry-balance-cli.ts +1 -6
- package/src/cli/grep-cli.ts +1 -1
- package/src/cli/models-cli.ts +3 -81
- package/src/commands/models.ts +1 -2
- package/src/commit/model-selection.ts +4 -4
- package/src/config/inline-tool-descriptors-mode.ts +1 -2
- package/src/config/model-discovery.ts +150 -82
- package/src/config/model-registry.ts +11 -225
- package/src/config/model-resolver.ts +23 -135
- package/src/config/models-config-schema.ts +0 -22
- package/src/config/prompt-templates.ts +20 -0
- package/src/config/settings-schema.ts +35 -1
- package/src/config/settings.ts +60 -24
- package/src/eval/__tests__/agent-bridge.test.ts +17 -3
- package/src/eval/agent-bridge.ts +7 -9
- package/src/eval/completion-bridge.ts +1 -1
- package/src/eval/py/__tests__/runner-shell-output.test.ts +157 -0
- package/src/eval/py/runner.py +169 -18
- package/src/extensibility/extensions/model-api.ts +1 -3
- package/src/extensibility/extensions/runner.ts +62 -5
- package/src/extensibility/skills.ts +77 -0
- package/src/internal-urls/docs-index.generated.txt +2 -2
- package/src/lsp/client.ts +162 -45
- package/src/lsp/config.ts +17 -3
- package/src/lsp/index.ts +31 -21
- package/src/main.ts +0 -1
- package/src/mcp/transports/http.ts +19 -17
- package/src/mcp/transports/stdio.test.ts +51 -1
- package/src/mcp/transports/stdio.ts +19 -9
- package/src/memories/index.ts +0 -1
- package/src/mnemopi/backend.ts +1 -1
- package/src/modes/acp/acp-agent.ts +6 -9
- package/src/modes/components/model-selector.ts +32 -186
- package/src/modes/components/todo-reminder.ts +5 -1
- package/src/modes/controllers/event-controller.ts +1 -16
- package/src/modes/controllers/selector-controller.ts +57 -36
- package/src/modes/interactive-mode.ts +0 -6
- package/src/modes/rpc/rpc-mode.ts +5 -8
- package/src/modes/skill-command.ts +8 -20
- package/src/modes/types.ts +0 -1
- package/src/prompts/system/subagent-system-prompt.md +2 -2
- package/src/prompts/system/tool-call-loop-redirect.md +8 -0
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/task.md +1 -1
- package/src/sdk.ts +2 -9
- package/src/session/agent-session.ts +89 -8
- package/src/session/session-context.ts +2 -1
- package/src/session/session-manager.ts +2 -1
- package/src/session/unexpected-stop-classifier.ts +1 -1
- package/src/stt/asr-client.ts +87 -27
- package/src/stt/downloader.ts +8 -2
- package/src/task/executor.ts +1 -3
- package/src/task/index.ts +37 -32
- package/src/task/spawn-policy.test.ts +63 -0
- package/src/task/spawn-policy.ts +58 -0
- package/src/task/types.ts +77 -6
- package/src/tools/__tests__/eval-description.test.ts +19 -0
- package/src/tools/ast-grep.ts +34 -12
- package/src/tools/browser/cmux/cmux-tab.ts +66 -24
- package/src/tools/browser/registry.ts +25 -0
- package/src/tools/browser/run-cancellation.ts +47 -0
- package/src/tools/browser/tab-supervisor.ts +120 -7
- package/src/tools/browser/tab-worker.ts +22 -10
- package/src/tools/browser.ts +1 -0
- package/src/tools/eval.ts +15 -10
- package/src/tools/grep.ts +11 -8
- package/src/tools/inspect-image.ts +1 -1
- package/src/tools/write.ts +49 -9
- package/src/utils/commit-message-generator.ts +0 -1
- package/src/utils/git.ts +22 -1
- package/src/utils/image-vision-fallback.ts +2 -3
- package/src/utils/title-generator.ts +1 -1
|
@@ -248,7 +248,6 @@ export class SelectorController {
|
|
|
248
248
|
const advisorRoleSel = resolveAdvisorRoleSelection(
|
|
249
249
|
this.ctx.settings,
|
|
250
250
|
this.ctx.session.modelRegistry.getAvailable(),
|
|
251
|
-
this.ctx.session.modelRegistry,
|
|
252
251
|
);
|
|
253
252
|
const defaultAdvisorModel = advisorRoleSel?.model;
|
|
254
253
|
const deps: AdvisorConfigDeps = {
|
|
@@ -962,43 +961,65 @@ export class SelectorController {
|
|
|
962
961
|
// every project's history when the cwd has nothing to resume. See #3099.
|
|
963
962
|
const historyStorage = this.ctx.historyStorage;
|
|
964
963
|
const historyMatcher = historyStorage ? (query: string) => historyStorage.matchingSessionIds(query) : undefined;
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
964
|
+
// Fullscreen session picker on the alternate screen (the /settings idiom):
|
|
965
|
+
// the overlay borrows the alt buffer and enables mouse tracking (wheel
|
|
966
|
+
// scroll + click-to-resume) for its lifetime, leaving the transcript
|
|
967
|
+
// untouched underneath. Anchored top-left at full size so a mouse row maps
|
|
968
|
+
// directly to a rendered line (the overlay paints from screen row 0), and
|
|
969
|
+
// `fillHeight` pads the body so the footer pins to the screen bottom.
|
|
970
|
+
let overlayHandle: OverlayHandle | undefined;
|
|
971
|
+
const done = () => {
|
|
972
|
+
overlayHandle?.hide();
|
|
973
|
+
this.focusActiveEditorArea();
|
|
974
|
+
this.ctx.ui.requestRender();
|
|
975
|
+
};
|
|
976
|
+
const selector = new SessionSelectorComponent(
|
|
977
|
+
sessions,
|
|
978
|
+
async (session: SessionInfo) => {
|
|
979
|
+
done();
|
|
980
|
+
await this.handleResumeSession(session.path);
|
|
981
|
+
},
|
|
982
|
+
() => {
|
|
983
|
+
done();
|
|
984
|
+
},
|
|
985
|
+
() => {
|
|
986
|
+
// Release the alt buffer before teardown: shutdown() awaits flush/save/
|
|
987
|
+
// dispose/drain before stop() leaves the alt screen, so without this the
|
|
988
|
+
// fullscreen picker would freeze on screen for that window on Ctrl+C.
|
|
989
|
+
done();
|
|
990
|
+
void this.ctx.shutdown();
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
onDelete: async (session: SessionInfo) => {
|
|
994
|
+
if (!(await this.#detachActiveSessionBeforeDeletion(session.path))) {
|
|
995
|
+
return false;
|
|
996
|
+
}
|
|
997
|
+
const storage = new FileSessionStorage();
|
|
998
|
+
try {
|
|
999
|
+
await storage.deleteSessionWithArtifacts(session.path);
|
|
1000
|
+
return true;
|
|
1001
|
+
} catch (err) {
|
|
1002
|
+
throw new Error(`Failed to delete session: ${err instanceof Error ? err.message : String(err)}`, {
|
|
1003
|
+
cause: err,
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
997
1006
|
},
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1007
|
+
historyMatcher,
|
|
1008
|
+
loadAllSessions: () => SessionManager.listAll(),
|
|
1009
|
+
getTerminalRows: () => this.ctx.ui.terminal.rows,
|
|
1010
|
+
fillHeight: true,
|
|
1011
|
+
},
|
|
1012
|
+
);
|
|
1013
|
+
selector.setOnRequestRender(() => this.ctx.ui.requestRender());
|
|
1014
|
+
overlayHandle = this.ctx.ui.showOverlay(selector, {
|
|
1015
|
+
anchor: "top-left",
|
|
1016
|
+
width: "100%",
|
|
1017
|
+
maxHeight: "100%",
|
|
1018
|
+
margin: 0,
|
|
1019
|
+
fullscreen: true,
|
|
1001
1020
|
});
|
|
1021
|
+
this.ctx.ui.setFocus(selector);
|
|
1022
|
+
this.ctx.ui.requestRender();
|
|
1002
1023
|
}
|
|
1003
1024
|
|
|
1004
1025
|
#refreshSessionTerminalTitle(): void {
|
|
@@ -379,7 +379,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
379
379
|
chatContainer: TranscriptContainer;
|
|
380
380
|
pendingMessagesContainer: Container;
|
|
381
381
|
statusContainer: Container;
|
|
382
|
-
todoReminderContainer: Container;
|
|
383
382
|
todoContainer: Container;
|
|
384
383
|
subagentContainer: Container;
|
|
385
384
|
btwContainer: Container;
|
|
@@ -548,7 +547,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
548
547
|
this.retryLoader = undefined;
|
|
549
548
|
}
|
|
550
549
|
this.statusContainer.clear();
|
|
551
|
-
this.todoReminderContainer.clear();
|
|
552
550
|
this.pendingMessagesContainer.clear();
|
|
553
551
|
this.#cancelModelCycleClearTimer();
|
|
554
552
|
this.modelCycleContainer.clear();
|
|
@@ -628,7 +626,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
628
626
|
this.chatContainer = new TranscriptContainer();
|
|
629
627
|
this.pendingMessagesContainer = new Container();
|
|
630
628
|
this.statusContainer = new AnchoredLiveContainer();
|
|
631
|
-
this.todoReminderContainer = new AnchoredLiveContainer();
|
|
632
629
|
this.todoContainer = new AnchoredLiveContainer();
|
|
633
630
|
this.subagentContainer = new AnchoredLiveContainer();
|
|
634
631
|
this.btwContainer = new AnchoredLiveContainer();
|
|
@@ -848,7 +845,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
848
845
|
|
|
849
846
|
this.ui.addChild(this.chatContainer);
|
|
850
847
|
this.ui.addChild(this.pendingMessagesContainer);
|
|
851
|
-
this.ui.addChild(this.todoReminderContainer);
|
|
852
848
|
this.ui.addChild(this.todoContainer);
|
|
853
849
|
this.ui.addChild(this.subagentContainer);
|
|
854
850
|
this.ui.addChild(this.btwContainer);
|
|
@@ -4077,14 +4073,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4077
4073
|
},
|
|
4078
4074
|
];
|
|
4079
4075
|
}
|
|
4080
|
-
this.todoReminderContainer.clear();
|
|
4081
4076
|
this.#syncTodoAutoClearTimer();
|
|
4082
4077
|
this.#renderTodoList();
|
|
4083
4078
|
this.ui.requestRender();
|
|
4084
4079
|
}
|
|
4085
4080
|
|
|
4086
4081
|
async reloadTodos(): Promise<void> {
|
|
4087
|
-
this.todoReminderContainer.clear();
|
|
4088
4082
|
await this.#loadTodoList();
|
|
4089
4083
|
this.ui.requestRender();
|
|
4090
4084
|
}
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
type ExtensionWidgetOptions,
|
|
23
23
|
getExtensionUISelectOptionLabel,
|
|
24
24
|
} from "../../extensibility/extensions";
|
|
25
|
-
import { buildSkillPromptMessage } from "../../extensibility/skills";
|
|
25
|
+
import { buildSkillPromptMessage, parseSkillInvocation } from "../../extensibility/skills";
|
|
26
26
|
import { loadSlashCommands } from "../../extensibility/slash-commands";
|
|
27
27
|
import { type Theme, theme } from "../../modes/theme/theme";
|
|
28
28
|
import type { AgentSession } from "../../session/agent-session";
|
|
@@ -86,15 +86,12 @@ export async function tryRunRpcSkillCommand(
|
|
|
86
86
|
session: RpcSkillCommandSession,
|
|
87
87
|
text: string,
|
|
88
88
|
): Promise<RpcSkillCommandResult | false> {
|
|
89
|
-
if (!text.startsWith("/skill:")) return false;
|
|
90
89
|
if (!session.skillsSettings?.enableSkillCommands) return false;
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
const skillName = commandName.slice("skill:".length);
|
|
95
|
-
const skill = session.skills.find(candidate => candidate.name === skillName);
|
|
90
|
+
const parsed = parseSkillInvocation(text);
|
|
91
|
+
if (!parsed) return false;
|
|
92
|
+
const skill = session.skills.find(candidate => candidate.name === parsed.name);
|
|
96
93
|
if (!skill) return false;
|
|
97
|
-
const built = await buildSkillPromptMessage(skill, args);
|
|
94
|
+
const built = await buildSkillPromptMessage(skill, parsed.args);
|
|
98
95
|
await session.promptCustomMessage({
|
|
99
96
|
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
100
97
|
content: built.message,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { getSkillSlashCommandName, parseSkillInvocation } from "../extensibility/skills";
|
|
2
3
|
import { type CustomMessage, SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../session/messages";
|
|
3
4
|
import type { InteractiveModeContext } from "./types";
|
|
4
5
|
|
|
@@ -20,11 +21,6 @@ type SkillPromptOptions = {
|
|
|
20
21
|
queueChipText: string;
|
|
21
22
|
};
|
|
22
23
|
|
|
23
|
-
interface ParsedSkillCommand {
|
|
24
|
-
commandName: string;
|
|
25
|
-
args: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
24
|
interface InvokeSkillCommandOptions {
|
|
29
25
|
propagateErrors?: boolean;
|
|
30
26
|
queueOnly?: boolean;
|
|
@@ -37,19 +33,11 @@ export interface BuiltSkillCommandPrompt {
|
|
|
37
33
|
options: SkillPromptOptions;
|
|
38
34
|
}
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
if (!text.startsWith("/skill:")) return undefined;
|
|
42
|
-
const spaceIndex = text.indexOf(" ");
|
|
43
|
-
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
|
44
|
-
const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
|
|
45
|
-
return { commandName, args };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Return true when `text` names a registered `/skill:<name>` command. */
|
|
36
|
+
/** Return true when `text` invokes a registered `/skill:<name>` command. */
|
|
49
37
|
export function isKnownSkillCommand(ctx: SkillCommandHost, text: string): boolean {
|
|
50
|
-
const parsed =
|
|
38
|
+
const parsed = parseSkillInvocation(text);
|
|
51
39
|
if (!parsed) return false;
|
|
52
|
-
return ctx.skillCommands.has(parsed.
|
|
40
|
+
return ctx.skillCommands.has(getSkillSlashCommandName({ name: parsed.name }));
|
|
53
41
|
}
|
|
54
42
|
|
|
55
43
|
/** Build the user-attributed custom message for a registered `/skill:<name>` command. */
|
|
@@ -59,9 +47,10 @@ export async function buildSkillCommandPrompt(
|
|
|
59
47
|
streamingBehavior: "steer" | "followUp",
|
|
60
48
|
images?: ImageContent[],
|
|
61
49
|
): Promise<BuiltSkillCommandPrompt | undefined> {
|
|
62
|
-
const parsed =
|
|
50
|
+
const parsed = parseSkillInvocation(text);
|
|
63
51
|
if (!parsed) return undefined;
|
|
64
|
-
const
|
|
52
|
+
const commandName = getSkillSlashCommandName({ name: parsed.name });
|
|
53
|
+
const skillPath = ctx.skillCommands.get(commandName);
|
|
65
54
|
if (!skillPath) return undefined;
|
|
66
55
|
|
|
67
56
|
const content = await Bun.file(skillPath).text();
|
|
@@ -73,9 +62,8 @@ export async function buildSkillCommandPrompt(
|
|
|
73
62
|
const message = `${body}\n\n---\n\n${metaLines.join("\n")}`;
|
|
74
63
|
const textBlock: TextContent = { type: "text", text: message };
|
|
75
64
|
const promptContent = images && images.length > 0 ? [textBlock, ...images] : message;
|
|
76
|
-
const skillName = parsed.commandName.slice("skill:".length);
|
|
77
65
|
const details: SkillPromptDetails = {
|
|
78
|
-
name:
|
|
66
|
+
name: parsed.name,
|
|
79
67
|
path: skillPath,
|
|
80
68
|
args: parsed.args || undefined,
|
|
81
69
|
lineCount: body ? body.split("\n").length : 0,
|
package/src/modes/types.ts
CHANGED
|
@@ -96,7 +96,6 @@ export interface InteractiveModeContext {
|
|
|
96
96
|
chatContainer: TranscriptContainer;
|
|
97
97
|
pendingMessagesContainer: Container;
|
|
98
98
|
statusContainer: Container;
|
|
99
|
-
todoReminderContainer: Container;
|
|
100
99
|
todoContainer: Container;
|
|
101
100
|
subagentContainer: Container;
|
|
102
101
|
btwContainer: Container;
|
|
@@ -62,9 +62,9 @@ Yield protocol:
|
|
|
62
62
|
This is your only way to return a final result. For structured results, you NEVER put JSON in plain text or substitute a text summary for `result.data`.
|
|
63
63
|
|
|
64
64
|
{{#if outputSchema}}
|
|
65
|
-
Your
|
|
65
|
+
Your terminal `yield` MUST use exactly this shape — the schema fields go inside `result.data`, NEVER at the top level and NEVER as a stringified summary:
|
|
66
66
|
```ts
|
|
67
|
-
{{
|
|
67
|
+
{{renderYieldSchema outputSchema}}
|
|
68
68
|
```
|
|
69
69
|
{{/if}}
|
|
70
70
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<system-interrupt reason="tool_call_loop_detected">
|
|
2
|
+
You called `{{tool_name}}` {{count}} consecutive times with identical arguments:
|
|
3
|
+
`{{arguments_summary}}`
|
|
4
|
+
|
|
5
|
+
Last result (truncated): `{{result_summary}}`
|
|
6
|
+
|
|
7
|
+
NEVER call `{{tool_name}}` with those arguments again this turn. Use different arguments, choose another tool, or summarize findings and yield if complete.
|
|
8
|
+
</system-interrupt>
|
|
@@ -39,8 +39,8 @@ tool.<name>(args) → unknown
|
|
|
39
39
|
Invoke any session tool; `args` = its parameter object.
|
|
40
40
|
completion(prompt, model?="default", system?=None, schema?=None) → str | dict
|
|
41
41
|
Oneshot, stateless (no history/tools). `model`: "smol" fast | "default" session | "slow" most capable. `schema` (JSON-Schema) → structured output, parsed object.
|
|
42
|
-
{{#if spawns}}agent(prompt, agent?="
|
|
43
|
-
Run a subagent → final output. `agent` picks another discovered agent; `schema` as in completion(). Background via `local://` files named in the prompt. `handle` → DAG node dict { text, output, handle: "agent://<id>", id, agent } (parsed under `data` when `schema` set).
|
|
42
|
+
{{#if spawns}}agent(prompt, agent?="{{spawnDefaultAgent}}", model?=None, label?=None, schema?=None, handle?=False) → str | dict
|
|
43
|
+
Run a subagent → final output. `agent` picks another discovered agent; omit it to use `{{spawnDefaultAgent}}`.{{#if spawnAllowedAgentsText}} Allowed agents: {{spawnAllowedAgentsText}}.{{/if}} `schema` as in completion(). Background via `local://` files named in the prompt. `handle` → DAG node dict { text, output, handle: "agent://<id>", id, agent } (parsed under `data` when `schema` set).
|
|
44
44
|
{{#if js}} JS: options are ONE trailing object — agent(prompt, { agent, schema, handle }).
|
|
45
45
|
{{/if}}
|
|
46
46
|
{{/if}}
|
|
@@ -10,7 +10,7 @@ Execution blocks your turn: the call only returns once the work is completely fi
|
|
|
10
10
|
- **One-pass agents:** Prefer agents that investigate **and** edit in a single pass; only spin a read-only discovery step (e.g. `explore`) when the affected files are genuinely unknown.
|
|
11
11
|
|
|
12
12
|
# Inputs
|
|
13
|
-
- `agent` (optional): The base agent type to use (e.g., `explore`, `reviewer`). Defaults to `
|
|
13
|
+
- `agent` (optional): The base agent type to use (e.g., `explore`, `reviewer`). Defaults to `{{defaultAgent}}`{{#if defaultAgentIsGeneric}} (the general-purpose worker){{/if}} — omit it for the default worker instead of passing `agent: "{{defaultAgent}}"`.{{#if allowedAgentsText}} Current spawn policy allows: {{allowedAgentsText}}.{{/if}}
|
|
14
14
|
{{#if batchEnabled}}
|
|
15
15
|
- `context`: Shared project state, constraints, and contracts. Applies to the entire batch; do not duplicate this background into individual tasks.
|
|
16
16
|
- `tasks[]`: Array of subagents to spawn.
|
package/src/sdk.ts
CHANGED
|
@@ -1244,7 +1244,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1244
1244
|
resolveModelRoleValue(settings.getModelRole("default"), allowedModels, {
|
|
1245
1245
|
settings,
|
|
1246
1246
|
matchPreferences: modelMatchPreferences,
|
|
1247
|
-
modelRegistry,
|
|
1248
1247
|
}),
|
|
1249
1248
|
);
|
|
1250
1249
|
let model = options.model;
|
|
@@ -1946,9 +1945,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1946
1945
|
if (!model && options.modelPattern) {
|
|
1947
1946
|
const availableModels = modelRegistry.getAll();
|
|
1948
1947
|
const matchPreferences = getModelMatchPreferences(settings);
|
|
1949
|
-
const { model: resolved } = parseModelPattern(options.modelPattern, availableModels, matchPreferences
|
|
1950
|
-
modelRegistry,
|
|
1951
|
-
});
|
|
1948
|
+
const { model: resolved } = parseModelPattern(options.modelPattern, availableModels, matchPreferences);
|
|
1952
1949
|
if (resolved) {
|
|
1953
1950
|
model = resolved;
|
|
1954
1951
|
modelFallbackMessage = undefined;
|
|
@@ -1978,7 +1975,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1978
1975
|
const reResolvedRoleSpec = resolveModelRoleValue(settings.getModelRole("default"), fallbackCandidates, {
|
|
1979
1976
|
settings,
|
|
1980
1977
|
matchPreferences: modelMatchPreferences,
|
|
1981
|
-
modelRegistry,
|
|
1982
1978
|
});
|
|
1983
1979
|
if (reResolvedRoleSpec.model) {
|
|
1984
1980
|
defaultRoleSpec = reResolvedRoleSpec;
|
|
@@ -2179,10 +2175,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2179
2175
|
// `auto` enforces the per-model policy (inline for Gemini, off otherwise);
|
|
2180
2176
|
// like the rest of the prune machinery this is fixed for the session, so a
|
|
2181
2177
|
// mid-session model switch keeps the start-time decision.
|
|
2182
|
-
const inlineToolDescriptors = shouldInlineToolDescriptors(
|
|
2183
|
-
settings.get("inlineToolDescriptors"),
|
|
2184
|
-
model ? (modelRegistry.getCanonicalId(model) ?? model.id) : undefined,
|
|
2185
|
-
);
|
|
2178
|
+
const inlineToolDescriptors = shouldInlineToolDescriptors(settings.get("inlineToolDescriptors"), model?.id);
|
|
2186
2179
|
const eagerTasks = settings.get("task.eager") !== "default";
|
|
2187
2180
|
const eagerTasksAlways = settings.get("task.eager") === "always";
|
|
2188
2181
|
const intentField = $flag("PI_INTENT_TRACING", settings.get("tools.intentTracing")) ? INTENT_FIELD : undefined;
|
|
@@ -115,6 +115,7 @@ import {
|
|
|
115
115
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
116
116
|
import { toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
|
|
117
117
|
import { GeminiHeaderRunDetector, isGeminiThinkingModel } from "@oh-my-pi/pi-ai/utils/thinking-loop";
|
|
118
|
+
import { type RepeatedToolCallDetection, ToolCallLoopGuard } from "@oh-my-pi/pi-ai/utils/tool-call-loop-guard";
|
|
118
119
|
import { isFireworksFastModelId, toFireworksBaseModelId } from "@oh-my-pi/pi-catalog/fireworks-model-id";
|
|
119
120
|
import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
120
121
|
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
@@ -251,6 +252,7 @@ import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool
|
|
|
251
252
|
};
|
|
252
253
|
import sideChannelNoToolsReminder from "../prompts/system/side-channel-no-tools.md" with { type: "text" };
|
|
253
254
|
import thinkingLoopRedirectTemplate from "../prompts/system/thinking-loop-redirect.md" with { type: "text" };
|
|
255
|
+
import toolCallLoopRedirectTemplate from "../prompts/system/tool-call-loop-redirect.md" with { type: "text" };
|
|
254
256
|
import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
|
|
255
257
|
import ttsrToolReminderTemplate from "../prompts/system/ttsr-tool-reminder.md" with { type: "text" };
|
|
256
258
|
import unexpectedStopRetryTemplate from "../prompts/system/unexpected-stop-retry.md" with { type: "text" };
|
|
@@ -285,6 +287,7 @@ import {
|
|
|
285
287
|
selectDiscoverableToolNamesByServer,
|
|
286
288
|
} from "../tool-discovery/tool-index";
|
|
287
289
|
import { assertEditableFile } from "../tools/auto-generated-guard";
|
|
290
|
+
import { releaseTabsForOwner } from "../tools/browser/tab-supervisor";
|
|
288
291
|
import { normalizeToolNames } from "../tools/builtin-names";
|
|
289
292
|
import type { CheckpointState } from "../tools/checkpoint";
|
|
290
293
|
import { outputMeta, wrapToolWithMetaNotice } from "../tools/output-meta";
|
|
@@ -350,6 +353,7 @@ const GEMINI_TOOL_REMINDER_TYPE = "gemini-tool-call-reminder";
|
|
|
350
353
|
/** `customType` for the hidden redirect notice injected into a turn retried after a
|
|
351
354
|
* thinking/response loop. Steers the model off the repeated content; never displayed. */
|
|
352
355
|
const THINKING_LOOP_REDIRECT_TYPE = "thinking-loop-redirect";
|
|
356
|
+
const TOOL_CALL_LOOP_REDIRECT_TYPE = "tool-call-loop-redirect";
|
|
353
357
|
|
|
354
358
|
// A side-channel assistant response is signed for the hidden prompt/history that
|
|
355
359
|
// produced it. If we persist that response under a different user turn, native
|
|
@@ -1554,6 +1558,8 @@ export class AgentSession {
|
|
|
1554
1558
|
* `#geminiHeaderGuardActive`); undefined for non-Gemini models or when the
|
|
1555
1559
|
* guard is off. Fed thinking deltas in the assistant-message interceptor. */
|
|
1556
1560
|
#geminiHeaderDetector: GeminiHeaderRunDetector | undefined;
|
|
1561
|
+
#toolCallLoopGuard: ToolCallLoopGuard | undefined;
|
|
1562
|
+
#toolCallLoopGuardSettingsKey: string | undefined;
|
|
1557
1563
|
#promptInFlightCount = 0;
|
|
1558
1564
|
#abortInProgress = false;
|
|
1559
1565
|
// Wire-level agent_end emission deferred until #promptInFlightCount drops to 0.
|
|
@@ -1848,6 +1854,13 @@ export class AgentSession {
|
|
|
1848
1854
|
this.#pendingRewindReport = undefined;
|
|
1849
1855
|
await this.#applyRewind(rewindReport, messages);
|
|
1850
1856
|
}
|
|
1857
|
+
if (context?.message.role === "assistant") {
|
|
1858
|
+
const detection = this.#activeToolCallLoopGuard()?.recordTurn({
|
|
1859
|
+
message: context.message,
|
|
1860
|
+
toolResults: context.toolResults,
|
|
1861
|
+
});
|
|
1862
|
+
if (detection) this.#maybeInjectToolCallLoopRedirect(messages, detection);
|
|
1863
|
+
}
|
|
1851
1864
|
this.#advisorPrimaryTurnsCompleted++;
|
|
1852
1865
|
if (this.#advisors.length > 0) {
|
|
1853
1866
|
for (const a of this.#advisors) {
|
|
@@ -2095,11 +2108,7 @@ export class AgentSession {
|
|
|
2095
2108
|
continue;
|
|
2096
2109
|
}
|
|
2097
2110
|
} else {
|
|
2098
|
-
const sel = resolveAdvisorRoleSelection(
|
|
2099
|
-
this.settings,
|
|
2100
|
-
this.#modelRegistry.getAvailable(),
|
|
2101
|
-
this.#modelRegistry,
|
|
2102
|
-
);
|
|
2111
|
+
const sel = resolveAdvisorRoleSelection(this.settings, this.#modelRegistry.getAvailable());
|
|
2103
2112
|
if (!sel) {
|
|
2104
2113
|
logger.debug("advisor enabled but no model assigned to the 'advisor' role; advisor inactive", {
|
|
2105
2114
|
advisor: config.name,
|
|
@@ -4299,6 +4308,58 @@ export class AgentSession {
|
|
|
4299
4308
|
this.#streamingEditFileCache.clear();
|
|
4300
4309
|
}
|
|
4301
4310
|
|
|
4311
|
+
#activeToolCallLoopGuard(): ToolCallLoopGuard | undefined {
|
|
4312
|
+
if (this.settings.get("model.toolCallLoopGuard.enabled") !== true) {
|
|
4313
|
+
this.#toolCallLoopGuard = undefined;
|
|
4314
|
+
this.#toolCallLoopGuardSettingsKey = undefined;
|
|
4315
|
+
return undefined;
|
|
4316
|
+
}
|
|
4317
|
+
|
|
4318
|
+
const threshold = this.settings.get("model.toolCallLoopGuard.threshold");
|
|
4319
|
+
const exemptTools = this.settings
|
|
4320
|
+
.get("model.toolCallLoopGuard.exemptTools")
|
|
4321
|
+
.filter((tool): tool is string => typeof tool === "string" && tool.length > 0);
|
|
4322
|
+
const settingsKey = `${threshold}:${JSON.stringify(exemptTools)}`;
|
|
4323
|
+
if (!this.#toolCallLoopGuard || this.#toolCallLoopGuardSettingsKey !== settingsKey) {
|
|
4324
|
+
this.#toolCallLoopGuard = new ToolCallLoopGuard({ threshold, exemptTools });
|
|
4325
|
+
this.#toolCallLoopGuardSettingsKey = settingsKey;
|
|
4326
|
+
}
|
|
4327
|
+
return this.#toolCallLoopGuard;
|
|
4328
|
+
}
|
|
4329
|
+
|
|
4330
|
+
#maybeInjectToolCallLoopRedirect(messages: AgentMessage[], detection: RepeatedToolCallDetection): void {
|
|
4331
|
+
const content = prompt.render(toolCallLoopRedirectTemplate, {
|
|
4332
|
+
tool_name: detection.toolName,
|
|
4333
|
+
count: detection.count,
|
|
4334
|
+
arguments_summary: detection.argumentsSummary,
|
|
4335
|
+
result_summary: detection.resultSummary || "(no text result)",
|
|
4336
|
+
});
|
|
4337
|
+
const details = {
|
|
4338
|
+
toolName: detection.toolName,
|
|
4339
|
+
count: detection.count,
|
|
4340
|
+
argumentsSummary: detection.argumentsSummary,
|
|
4341
|
+
resultSummary: detection.resultSummary,
|
|
4342
|
+
};
|
|
4343
|
+
logger.warn("cross-turn tool-call loop detected", {
|
|
4344
|
+
toolName: detection.toolName,
|
|
4345
|
+
count: detection.count,
|
|
4346
|
+
});
|
|
4347
|
+
const redirectMessage: CustomMessage = {
|
|
4348
|
+
role: "custom",
|
|
4349
|
+
customType: TOOL_CALL_LOOP_REDIRECT_TYPE,
|
|
4350
|
+
content,
|
|
4351
|
+
display: false,
|
|
4352
|
+
details,
|
|
4353
|
+
attribution: "agent",
|
|
4354
|
+
timestamp: Date.now(),
|
|
4355
|
+
};
|
|
4356
|
+
messages.push(redirectMessage);
|
|
4357
|
+
if (this.agent.state.messages !== messages) {
|
|
4358
|
+
this.agent.appendMessage(redirectMessage);
|
|
4359
|
+
}
|
|
4360
|
+
this.sessionManager.appendCustomMessageEntry(TOOL_CALL_LOOP_REDIRECT_TYPE, content, false, details, "agent");
|
|
4361
|
+
}
|
|
4362
|
+
|
|
4302
4363
|
/**
|
|
4303
4364
|
* Whether the Gemini header-runaway guard applies to the current model: the loop
|
|
4304
4365
|
* guard is on (settings + `PI_NO_THINKING_LOOP_GUARD`), the tool-call reminder is
|
|
@@ -5093,6 +5154,28 @@ export class AgentSession {
|
|
|
5093
5154
|
await disposeKernelSessionsByOwner(this.#evalKernelOwnerId);
|
|
5094
5155
|
await disposeRubyKernelSessionsByOwner(this.#evalKernelOwnerId);
|
|
5095
5156
|
await disposeJuliaKernelSessionsByOwner(this.#evalKernelOwnerId);
|
|
5157
|
+
// Release headless / spawned Chromium and worker tabs this session
|
|
5158
|
+
// opened via the browser tool. The tool's `tabs`/`browsers` maps are
|
|
5159
|
+
// module-global — subagents and future sessions share them — so we
|
|
5160
|
+
// walk by `ownerSessionId` (assigned at `acquireTab` creation, never on
|
|
5161
|
+
// reuse) and touch only what THIS session created. Bounded so a broken
|
|
5162
|
+
// CDP close cannot stall `/exit`; mirrors the async-job/MCP pattern.
|
|
5163
|
+
// (Issue #3963.)
|
|
5164
|
+
const browserOwnerId = this.sessionManager.getSessionId();
|
|
5165
|
+
if (browserOwnerId) {
|
|
5166
|
+
try {
|
|
5167
|
+
const released = await withTimeout(
|
|
5168
|
+
releaseTabsForOwner(browserOwnerId, { kill: true }),
|
|
5169
|
+
3_000,
|
|
5170
|
+
"Timed out releasing owned browser tabs during dispose",
|
|
5171
|
+
);
|
|
5172
|
+
if (released > 0) {
|
|
5173
|
+
logger.debug("Released owned browser tabs during dispose", { ownerId: browserOwnerId, released });
|
|
5174
|
+
}
|
|
5175
|
+
} catch (error) {
|
|
5176
|
+
logger.warn("Failed to release owned browser tabs during dispose", { error: String(error) });
|
|
5177
|
+
}
|
|
5178
|
+
}
|
|
5096
5179
|
await shutdownTinyTitleClient();
|
|
5097
5180
|
this.#releasePowerAssertion();
|
|
5098
5181
|
// Clean up an empty session created by this session's /move so it doesn't accumulate.
|
|
@@ -8158,7 +8241,6 @@ export class AgentSession {
|
|
|
8158
8241
|
const resolved = resolveModelRoleValue(roleModelStr, availableModels, {
|
|
8159
8242
|
settings: this.settings,
|
|
8160
8243
|
matchPreferences,
|
|
8161
|
-
modelRegistry: this.#modelRegistry,
|
|
8162
8244
|
});
|
|
8163
8245
|
if (!resolved.model) continue;
|
|
8164
8246
|
|
|
@@ -8299,7 +8381,7 @@ export class AgentSession {
|
|
|
8299
8381
|
const all = this.#modelRegistry.getAvailable();
|
|
8300
8382
|
const patterns = this.settings.get("enabledModels");
|
|
8301
8383
|
if (!patterns || patterns.length === 0) return all;
|
|
8302
|
-
return filterAvailableModelsByEnabledPatterns(all, patterns
|
|
8384
|
+
return filterAvailableModelsByEnabledPatterns(all, patterns);
|
|
8303
8385
|
}
|
|
8304
8386
|
|
|
8305
8387
|
// =========================================================================
|
|
@@ -10713,7 +10795,6 @@ export class AgentSession {
|
|
|
10713
10795
|
return resolveModelRoleValue(roleModelStr, availableModels, {
|
|
10714
10796
|
settings: this.settings,
|
|
10715
10797
|
matchPreferences: getModelMatchPreferences(this.settings),
|
|
10716
|
-
modelRegistry: this.#modelRegistry,
|
|
10717
10798
|
});
|
|
10718
10799
|
}
|
|
10719
10800
|
|
|
@@ -142,9 +142,10 @@ export function buildSessionContext(
|
|
|
142
142
|
const path: SessionEntry[] = [];
|
|
143
143
|
let current: SessionEntry | undefined = leaf;
|
|
144
144
|
while (current) {
|
|
145
|
-
path.
|
|
145
|
+
path.push(current);
|
|
146
146
|
current = current.parentId ? byId.get(current.parentId) : undefined;
|
|
147
147
|
}
|
|
148
|
+
path.reverse();
|
|
148
149
|
|
|
149
150
|
// Extract settings and find compaction
|
|
150
151
|
let thinkingLevel: string | undefined = "off";
|
|
@@ -242,9 +242,10 @@ class SessionEntryIndex {
|
|
|
242
242
|
|
|
243
243
|
while (cursor && !seen.has(cursor.id)) {
|
|
244
244
|
seen.add(cursor.id);
|
|
245
|
-
branch.
|
|
245
|
+
branch.push(cursor);
|
|
246
246
|
cursor = cursor.parentId ? this.#entriesById.get(cursor.parentId) : undefined;
|
|
247
247
|
}
|
|
248
|
+
branch.reverse();
|
|
248
249
|
|
|
249
250
|
return branch;
|
|
250
251
|
}
|
|
@@ -64,7 +64,7 @@ export async function classifyUnexpectedStop(
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
async function classifyOnline(text: string, deps: ClassifyUnexpectedStopDeps): Promise<boolean | undefined> {
|
|
67
|
-
const resolved = resolveRoleSelection(["tiny", "smol"], deps.settings, deps.registry.getAvailable()
|
|
67
|
+
const resolved = resolveRoleSelection(["tiny", "smol"], deps.settings, deps.registry.getAvailable());
|
|
68
68
|
const model = resolved?.model;
|
|
69
69
|
if (!model) {
|
|
70
70
|
throw new Error("unexpected-stop: no tiny/smol model available for classification");
|