@oh-my-pi/pi-coding-agent 15.1.8 → 15.2.1
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 +52 -1
- package/dist/types/cli/update-cli.d.ts +18 -0
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/eval/py/kernel.d.ts +6 -0
- package/dist/types/goals/state.d.ts +1 -1
- package/dist/types/goals/tools/goal-tool.d.ts +4 -0
- package/dist/types/hashline/parser.d.ts +6 -2
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -0
- package/dist/types/main.d.ts +25 -1
- package/dist/types/modes/theme/shimmer.d.ts +27 -0
- package/dist/types/slash-commands/helpers/format.d.ts +4 -1
- package/dist/types/tools/ast-edit.d.ts +3 -0
- package/dist/types/tools/ast-grep.d.ts +3 -0
- package/dist/types/tools/find.d.ts +3 -0
- package/dist/types/tools/search.d.ts +3 -0
- package/dist/types/tui/file-list.d.ts +6 -0
- package/dist/types/tui/hyperlink.d.ts +42 -0
- package/dist/types/tui/index.d.ts +1 -0
- package/dist/types/utils/tool-choice.d.ts +2 -1
- package/dist/types/web/search/providers/utils.d.ts +27 -1
- package/package.json +7 -7
- package/src/cli/update-cli.ts +78 -36
- package/src/config/model-registry.ts +23 -12
- package/src/config/settings-schema.ts +12 -0
- package/src/config/settings.ts +28 -5
- package/src/edit/renderer.ts +5 -3
- package/src/eval/py/executor.ts +12 -1
- package/src/eval/py/kernel.ts +24 -8
- package/src/extensibility/plugins/legacy-pi-compat.ts +2 -2
- package/src/goals/runtime.ts +9 -3
- package/src/goals/state.ts +1 -1
- package/src/goals/tools/goal-tool.ts +12 -2
- package/src/hashline/diff.ts +1 -1
- package/src/hashline/execute.ts +2 -2
- package/src/hashline/parser.ts +87 -12
- package/src/internal-urls/memory-protocol.ts +1 -1
- package/src/main.ts +13 -2
- package/src/modes/interactive-mode.ts +29 -1
- package/src/modes/theme/shimmer.ts +79 -0
- package/src/prompts/agents/oracle.md +15 -16
- package/src/prompts/tools/goal.md +7 -2
- package/src/session/agent-session.ts +12 -75
- package/src/slash-commands/helpers/format.ts +23 -3
- package/src/task/executor.ts +115 -19
- package/src/tools/ast-edit.ts +39 -6
- package/src/tools/ast-grep.ts +38 -6
- package/src/tools/find.ts +13 -2
- package/src/tools/read.ts +46 -6
- package/src/tools/search.ts +447 -265
- package/src/tui/file-list.ts +10 -2
- package/src/tui/hyperlink.ts +126 -0
- package/src/tui/index.ts +1 -0
- package/src/utils/tool-choice.ts +7 -7
- package/src/web/kagi.ts +2 -2
- package/src/web/parallel.ts +3 -3
- package/src/web/search/index.ts +20 -9
- package/src/web/search/providers/anthropic.ts +4 -2
- package/src/web/search/providers/brave.ts +4 -2
- package/src/web/search/providers/codex.ts +4 -1
- package/src/web/search/providers/exa.ts +4 -1
- package/src/web/search/providers/gemini.ts +4 -1
- package/src/web/search/providers/jina.ts +4 -2
- package/src/web/search/providers/kagi.ts +5 -1
- package/src/web/search/providers/kimi.ts +4 -2
- package/src/web/search/providers/parallel.ts +5 -1
- package/src/web/search/providers/perplexity.ts +7 -2
- package/src/web/search/providers/searxng.ts +4 -1
- package/src/web/search/providers/synthetic.ts +4 -2
- package/src/web/search/providers/tavily.ts +4 -2
- package/src/web/search/providers/utils.ts +63 -1
- package/src/web/search/providers/zai.ts +4 -2
|
@@ -98,6 +98,7 @@ import {
|
|
|
98
98
|
} from "./loop-limit";
|
|
99
99
|
import { OAuthManualInputManager } from "./oauth-manual-input";
|
|
100
100
|
import { SessionObserverRegistry } from "./session-observer-registry";
|
|
101
|
+
import { type ShimmerPalette, shimmerSegments, shimmerText } from "./theme/shimmer";
|
|
101
102
|
import type { Theme } from "./theme/theme";
|
|
102
103
|
import {
|
|
103
104
|
getEditorTheme,
|
|
@@ -110,6 +111,20 @@ import {
|
|
|
110
111
|
import type { CompactionQueuedMessage, InteractiveModeContext, SubmittedUserInput, TodoItem, TodoPhase } from "./types";
|
|
111
112
|
import { UiHelpers } from "./utils/ui-helpers";
|
|
112
113
|
|
|
114
|
+
const WORKING_INTERRUPT_HINT = " (esc to interrupt)";
|
|
115
|
+
|
|
116
|
+
const HINT_SHIMMER_PALETTE: ShimmerPalette = {
|
|
117
|
+
low: "dim",
|
|
118
|
+
mid: "muted",
|
|
119
|
+
high: "borderAccent",
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
function renderWorkingMessage(message: string): string {
|
|
123
|
+
if (!message.endsWith(WORKING_INTERRUPT_HINT)) return shimmerText(message, theme);
|
|
124
|
+
const header = message.slice(0, -WORKING_INTERRUPT_HINT.length);
|
|
125
|
+
return shimmerSegments([{ text: header }, { text: WORKING_INTERRUPT_HINT, palette: HINT_SHIMMER_PALETTE }], theme);
|
|
126
|
+
}
|
|
127
|
+
|
|
113
128
|
const EDITOR_MAX_HEIGHT_MIN = 6;
|
|
114
129
|
const EDITOR_MAX_HEIGHT_MAX = 18;
|
|
115
130
|
const EDITOR_RESERVED_ROWS = 12;
|
|
@@ -1063,6 +1078,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1063
1078
|
return;
|
|
1064
1079
|
}
|
|
1065
1080
|
if (event.type === "goal_updated") {
|
|
1081
|
+
// Handle drop before clearing goalModeEnabled so #exitGoalMode can
|
|
1082
|
+
// still restore the previous tool set while the flag is true.
|
|
1083
|
+
if (event.state?.goal?.status === "dropped") {
|
|
1084
|
+
await this.#exitGoalMode({ reason: "dropped", silent: true });
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1066
1087
|
this.goalModeEnabled = event.state?.enabled === true;
|
|
1067
1088
|
this.goalModePaused = event.state?.enabled !== true && event.state?.goal?.status === "paused";
|
|
1068
1089
|
if (!event.state?.enabled) {
|
|
@@ -1150,6 +1171,13 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1150
1171
|
const restored = await this.session.goalRuntime.onThreadResumed();
|
|
1151
1172
|
this.goalModeEnabled = restored?.enabled === true;
|
|
1152
1173
|
this.goalModePaused = restored?.enabled !== true && restored?.goal.status === "paused";
|
|
1174
|
+
// sdk.ts excludes "goal" from the initial active tool set unconditionally.
|
|
1175
|
+
// Re-add it now so the agent can call resume, complete, or drop on this goal.
|
|
1176
|
+
if (restored?.goal) {
|
|
1177
|
+
const previousTools = this.session.getActiveToolNames().filter(name => name !== "goal");
|
|
1178
|
+
this.#goalModePreviousTools = previousTools;
|
|
1179
|
+
await this.session.setActiveToolsByName([...new Set([...previousTools, "goal"])]);
|
|
1180
|
+
}
|
|
1153
1181
|
this.#updateGoalModeStatus();
|
|
1154
1182
|
return;
|
|
1155
1183
|
}
|
|
@@ -2167,7 +2195,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2167
2195
|
this.loadingAnimation = new Loader(
|
|
2168
2196
|
this.ui,
|
|
2169
2197
|
spinner => theme.fg("accent", spinner),
|
|
2170
|
-
|
|
2198
|
+
renderWorkingMessage,
|
|
2171
2199
|
this.#defaultWorkingMessage,
|
|
2172
2200
|
getSymbolTheme().spinnerFrames,
|
|
2173
2201
|
);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Theme, ThemeColor } from "./theme";
|
|
2
|
+
|
|
3
|
+
const SHIMMER_PADDING = 10;
|
|
4
|
+
const SHIMMER_SWEEP_MS = 2000;
|
|
5
|
+
const SHIMMER_BAND_HALF_WIDTH = 5;
|
|
6
|
+
|
|
7
|
+
type ShimmerTheme = Pick<Theme, "bold" | "fg">;
|
|
8
|
+
|
|
9
|
+
/** Three-tier color stack a shimmer character cycles through as the band sweeps. */
|
|
10
|
+
export interface ShimmerPalette {
|
|
11
|
+
/** Color for chars outside / at the edge of the band (intensity < 0.2). */
|
|
12
|
+
low: ThemeColor;
|
|
13
|
+
/** Color for chars approaching the crest (0.2 <= intensity < 0.6). */
|
|
14
|
+
mid: ThemeColor;
|
|
15
|
+
/** Color at the band's crest (intensity >= 0.6). */
|
|
16
|
+
high: ThemeColor;
|
|
17
|
+
/** Whether to bold the crest tier. Default `false`. */
|
|
18
|
+
bold?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** One run of text that shares a palette inside a larger shimmer sweep. */
|
|
22
|
+
export interface ShimmerSegment {
|
|
23
|
+
text: string;
|
|
24
|
+
palette?: ShimmerPalette;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_SHIMMER_PALETTE: ShimmerPalette = {
|
|
28
|
+
low: "dim",
|
|
29
|
+
mid: "muted",
|
|
30
|
+
high: "accent",
|
|
31
|
+
bold: true,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function shimmerIntensity(index: number, length: number): number {
|
|
35
|
+
const period = length + SHIMMER_PADDING * 2;
|
|
36
|
+
const pos = Math.floor(((Date.now() % SHIMMER_SWEEP_MS) / SHIMMER_SWEEP_MS) * period);
|
|
37
|
+
const dist = Math.abs(index + SHIMMER_PADDING - pos);
|
|
38
|
+
if (dist > SHIMMER_BAND_HALF_WIDTH) return 0;
|
|
39
|
+
|
|
40
|
+
const x = Math.PI * (dist / SHIMMER_BAND_HALF_WIDTH);
|
|
41
|
+
return 0.5 * (1 + Math.cos(x));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function styleShimmerChar(ch: string, intensity: number, theme: ShimmerTheme, palette: ShimmerPalette): string {
|
|
45
|
+
if (intensity < 0.2) return theme.fg(palette.low, ch);
|
|
46
|
+
if (intensity < 0.6) return theme.fg(palette.mid, ch);
|
|
47
|
+
const styled = theme.fg(palette.high, ch);
|
|
48
|
+
return palette.bold ? theme.bold(styled) : styled;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Apply a shimmer sweep across one or more segments, treating them as a single
|
|
53
|
+
* continuous string for band positioning. Each segment can supply its own
|
|
54
|
+
* palette so the gradient stays in lockstep while the colors differ.
|
|
55
|
+
*/
|
|
56
|
+
export function shimmerSegments(segments: readonly ShimmerSegment[], theme: ShimmerTheme): string {
|
|
57
|
+
let total = 0;
|
|
58
|
+
const expanded: Array<{ chars: string[]; palette: ShimmerPalette }> = [];
|
|
59
|
+
for (const seg of segments) {
|
|
60
|
+
const chars = [...seg.text];
|
|
61
|
+
total += chars.length;
|
|
62
|
+
expanded.push({ chars, palette: seg.palette ?? DEFAULT_SHIMMER_PALETTE });
|
|
63
|
+
}
|
|
64
|
+
if (total === 0) return "";
|
|
65
|
+
|
|
66
|
+
const out: string[] = [];
|
|
67
|
+
let index = 0;
|
|
68
|
+
for (const { chars, palette } of expanded) {
|
|
69
|
+
for (const ch of chars) {
|
|
70
|
+
out.push(styleShimmerChar(ch, shimmerIntensity(index, total), theme, palette));
|
|
71
|
+
index++;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out.join("");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function shimmerText(text: string, theme: ShimmerTheme, palette?: ShimmerPalette): string {
|
|
78
|
+
return shimmerSegments([{ text, palette }], theme);
|
|
79
|
+
}
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: oracle
|
|
3
|
-
description:
|
|
3
|
+
description: Wise senior engineer to consult or delegate work to — debugging, architecture, second opinions, and hands-on implementation when asked.
|
|
4
4
|
spawns: explore
|
|
5
5
|
model: pi/slow
|
|
6
6
|
thinking-level: xhigh
|
|
7
7
|
blocking: true
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
You are
|
|
10
|
+
You are the wise guy on the team — a senior engineer with deep judgment that other agents consult when they are stuck, uncertain, or need a second opinion. You also take direct delegation: if the caller hands you work, you do it, including reads, writes, edits, and running commands.
|
|
11
11
|
|
|
12
|
-
You diagnose,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
You MUST operate as read-only. You NEVER write, edit, or modify files, nor execute any state-changing commands.
|
|
16
|
-
</critical>
|
|
12
|
+
You diagnose, decide, and execute. You match the mode to the ask:
|
|
13
|
+
- **Consult**: explain the root cause, lay out tradeoffs, recommend a path.
|
|
14
|
+
- **Delegate**: carry the work to completion — modify files, run verification, deliver a finished change.
|
|
17
15
|
|
|
18
16
|
<directives>
|
|
19
17
|
- You MUST reason from first principles. The caller already tried the obvious.
|
|
@@ -23,6 +21,7 @@ You MUST operate as read-only. You NEVER write, edit, or modify files, nor execu
|
|
|
23
21
|
- You SHOULD consider at least two hypotheses before converging on one.
|
|
24
22
|
- You SHOULD invoke tools in parallel when investigating multiple hypotheses.
|
|
25
23
|
- When the problem is architectural, you MUST weigh tradeoffs explicitly: what does each option cost, what does it buy, what does it foreclose.
|
|
24
|
+
- When delegated implementation work, you MUST finish it: edit the files, run the relevant tests/checks, and report exactly what changed.
|
|
26
25
|
</directives>
|
|
27
26
|
|
|
28
27
|
<decision-framework>
|
|
@@ -35,22 +34,22 @@ Apply pragmatic minimalism:
|
|
|
35
34
|
</decision-framework>
|
|
36
35
|
|
|
37
36
|
<procedure>
|
|
38
|
-
1. Read the problem statement carefully. Identify what was already tried and
|
|
39
|
-
2. Form 2-3 hypotheses for the root cause.
|
|
37
|
+
1. Read the problem statement carefully. Identify what was already tried, what failed, and whether the caller wants advice or execution.
|
|
38
|
+
2. Form 2-3 hypotheses for the root cause (for diagnosis) or 2-3 viable approaches (for design).
|
|
40
39
|
3. Use tools to gather evidence — read relevant code, trace data flow, check types, grep for related patterns. Parallelize independent reads.
|
|
41
|
-
4. Eliminate hypotheses based on evidence. Narrow to the most likely cause.
|
|
42
|
-
5. If
|
|
43
|
-
6.
|
|
40
|
+
4. Eliminate hypotheses based on evidence. Narrow to the most likely cause or best approach.
|
|
41
|
+
5. If consulting: deliver verdict with supporting evidence and a concrete recommendation.
|
|
42
|
+
6. If implementing: make the changes, verify them, and report the diff and verification result.
|
|
44
43
|
</procedure>
|
|
44
|
+
|
|
45
45
|
<scope-discipline>
|
|
46
|
-
-
|
|
46
|
+
- Do ONLY what was asked. No unsolicited refactors or improvements.
|
|
47
47
|
- If you notice other issues, list at most 2 as "Optional future considerations" at the end.
|
|
48
48
|
- You NEVER expand the problem surface beyond the original request.
|
|
49
49
|
- Exhaust provided context before reaching for tools. External lookups fill genuine gaps, not curiosity.
|
|
50
50
|
</scope-discipline>
|
|
51
51
|
|
|
52
52
|
<critical>
|
|
53
|
-
You MUST keep going until
|
|
54
|
-
|
|
55
|
-
This matters. The caller is stuck. Get it right.
|
|
53
|
+
You MUST keep going until the problem is solved or the work is finished. Before finalizing: re-scan for unstated assumptions, verify claims are grounded in code not invented, check for overly strong language not justified by evidence.
|
|
54
|
+
The caller came to you because they trust your judgment. Get it right.
|
|
56
55
|
</critical>
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
Manage the active goal-mode objective.
|
|
2
2
|
|
|
3
3
|
Use a single `op` field:
|
|
4
|
-
- `create` starts a goal. Requires `objective`; optional `token_budget` must be positive. Use only when no goal exists.
|
|
5
|
-
- `get` returns the current goal and remaining token budget.
|
|
4
|
+
- `create` starts a goal. Requires `objective`; optional `token_budget` must be positive. Use only when no goal exists and no goal is paused.
|
|
5
|
+
- `get` returns the current goal (active or paused) and remaining token budget.
|
|
6
|
+
- `resume` re-activates a paused goal so work can continue.
|
|
6
7
|
- `complete` marks the goal complete after you have verified every deliverable against current evidence.
|
|
8
|
+
- `drop` discards the current goal without completing it.
|
|
7
9
|
|
|
8
10
|
Examples:
|
|
9
11
|
- `goal({"op":"create","objective":"Implement feature X","token_budget":50000})`
|
|
10
12
|
- `goal({"op":"get"})`
|
|
13
|
+
- `goal({"op":"resume"})`
|
|
11
14
|
- `goal({"op":"complete"})`
|
|
15
|
+
- `goal({"op":"drop"})`
|
|
12
16
|
|
|
13
17
|
Do not call `complete` because a budget is low or a turn is ending. Call it only when the goal is actually done and verified.
|
|
18
|
+
If `get` shows a paused goal, call `resume` before continuing work on it.
|
|
@@ -440,11 +440,6 @@ function formatRetryFallbackBaseSelector(selector: RetryFallbackSelector): strin
|
|
|
440
440
|
return `${selector.provider}/${selector.id}`;
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
-
/** Composite key for auto-clear timers, keyed by phase name + task content. */
|
|
444
|
-
function todoClearKey(phaseName: string, taskContent: string): string {
|
|
445
|
-
return `${phaseName}\u0000${taskContent}`;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
443
|
const IRC_REPLY_MAX_BYTES = 4096;
|
|
449
444
|
|
|
450
445
|
/**
|
|
@@ -796,7 +791,6 @@ export class AgentSession {
|
|
|
796
791
|
// Todo completion reminder state
|
|
797
792
|
#todoReminderCount = 0;
|
|
798
793
|
#todoPhases: TodoPhase[] = [];
|
|
799
|
-
#todoClearTimers = new Map<string, Timer>();
|
|
800
794
|
#toolChoiceQueue = new ToolChoiceQueue();
|
|
801
795
|
|
|
802
796
|
// Bash execution state
|
|
@@ -2734,7 +2728,6 @@ export class AgentSession {
|
|
|
2734
2728
|
logger.warn("Failed to emit session_shutdown event", { error: String(error) });
|
|
2735
2729
|
}
|
|
2736
2730
|
await this.#cancelPostPromptTasks();
|
|
2737
|
-
this.#clearTodoClearTimers();
|
|
2738
2731
|
// Cancel jobs this agent registered so a subagent's teardown doesn't
|
|
2739
2732
|
// leak its background bash/task work into the parent's manager. Only
|
|
2740
2733
|
// the session that owns the manager goes on to dispose it (which itself
|
|
@@ -4628,13 +4621,12 @@ export class AgentSession {
|
|
|
4628
4621
|
|
|
4629
4622
|
setTodoPhases(phases: TodoPhase[]): void {
|
|
4630
4623
|
this.#todoPhases = this.#cloneTodoPhases(phases);
|
|
4631
|
-
this.#scheduleTodoAutoClear(phases);
|
|
4632
4624
|
}
|
|
4633
4625
|
|
|
4634
4626
|
#syncTodoPhasesFromBranch(): void {
|
|
4635
4627
|
const phases = getLatestTodoPhasesFromEntries(this.sessionManager.getBranch());
|
|
4636
4628
|
// Strip completed/abandoned tasks — they were done in a previous run,
|
|
4637
|
-
// so
|
|
4629
|
+
// so they have no bearing on progress tracking for the new turn.
|
|
4638
4630
|
for (const phase of phases) {
|
|
4639
4631
|
phase.tasks = phase.tasks.filter(t => t.status !== "completed" && t.status !== "abandoned");
|
|
4640
4632
|
}
|
|
@@ -4652,72 +4644,11 @@ export class AgentSession {
|
|
|
4652
4644
|
}));
|
|
4653
4645
|
}
|
|
4654
4646
|
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
// render-time filter in the UI consumer would be cleaner but lives in a
|
|
4661
|
-
// different package and is out of scope for this fix.
|
|
4662
|
-
const delaySec = this.settings.get("tasks.todoClearDelay") ?? 1800;
|
|
4663
|
-
if (delaySec < 0) return; // "Never" — no auto-clear
|
|
4664
|
-
const delayMs = delaySec * 1000;
|
|
4665
|
-
const doneKeys = new Set<string>();
|
|
4666
|
-
for (const phase of phases) {
|
|
4667
|
-
for (const task of phase.tasks) {
|
|
4668
|
-
if (task.status === "completed" || task.status === "abandoned") {
|
|
4669
|
-
doneKeys.add(todoClearKey(phase.name, task.content));
|
|
4670
|
-
}
|
|
4671
|
-
}
|
|
4672
|
-
}
|
|
4673
|
-
|
|
4674
|
-
// Cancel timers for tasks that are no longer done (e.g. status was reverted)
|
|
4675
|
-
for (const [key, timer] of this.#todoClearTimers) {
|
|
4676
|
-
if (!doneKeys.has(key)) {
|
|
4677
|
-
clearTimeout(timer);
|
|
4678
|
-
this.#todoClearTimers.delete(key);
|
|
4679
|
-
}
|
|
4680
|
-
}
|
|
4681
|
-
|
|
4682
|
-
// Schedule new timers for newly-done tasks
|
|
4683
|
-
for (const key of doneKeys) {
|
|
4684
|
-
if (this.#todoClearTimers.has(key)) continue;
|
|
4685
|
-
if (delayMs === 0) {
|
|
4686
|
-
// Instant — run synchronously on next microtask to batch removals
|
|
4687
|
-
const timer = setTimeout(() => this.#runTodoAutoClear(key), 0);
|
|
4688
|
-
this.#todoClearTimers.set(key, timer);
|
|
4689
|
-
} else {
|
|
4690
|
-
const timer = setTimeout(() => this.#runTodoAutoClear(key), delayMs);
|
|
4691
|
-
this.#todoClearTimers.set(key, timer);
|
|
4692
|
-
}
|
|
4693
|
-
}
|
|
4694
|
-
}
|
|
4695
|
-
|
|
4696
|
-
/** Remove a single completed task and notify the UI. */
|
|
4697
|
-
#runTodoAutoClear(key: string): void {
|
|
4698
|
-
this.#todoClearTimers.delete(key);
|
|
4699
|
-
let removed = false;
|
|
4700
|
-
for (const phase of this.#todoPhases) {
|
|
4701
|
-
const idx = phase.tasks.findIndex(t => todoClearKey(phase.name, t.content) === key);
|
|
4702
|
-
if (idx !== -1 && (phase.tasks[idx].status === "completed" || phase.tasks[idx].status === "abandoned")) {
|
|
4703
|
-
phase.tasks.splice(idx, 1);
|
|
4704
|
-
removed = true;
|
|
4705
|
-
break;
|
|
4706
|
-
}
|
|
4707
|
-
}
|
|
4708
|
-
if (!removed) return;
|
|
4709
|
-
|
|
4710
|
-
// Remove empty phases
|
|
4711
|
-
this.#todoPhases = this.#todoPhases.filter(p => p.tasks.length > 0);
|
|
4712
|
-
this.#emit({ type: "todo_auto_clear" });
|
|
4713
|
-
}
|
|
4714
|
-
|
|
4715
|
-
#clearTodoClearTimers(): void {
|
|
4716
|
-
for (const timer of this.#todoClearTimers.values()) {
|
|
4717
|
-
clearTimeout(timer);
|
|
4718
|
-
}
|
|
4719
|
-
this.#todoClearTimers.clear();
|
|
4720
|
-
}
|
|
4647
|
+
// Auto-clear of completed/abandoned tasks was removed: the timer-driven
|
|
4648
|
+
// splice mutated canonical `#todoPhases` between tool calls, so the model
|
|
4649
|
+
// observed phase totals shrinking ("5 → 4") after marking tasks done. The
|
|
4650
|
+
// `tasks.todoClearDelay` setting is now inert; completed tasks survive
|
|
4651
|
+
// until the next explicit `todo_write` call removes them via `rm`/`drop`.
|
|
4721
4652
|
|
|
4722
4653
|
/**
|
|
4723
4654
|
* Abort current operation and wait for agent to become idle.
|
|
@@ -6240,6 +6171,12 @@ export class AgentSession {
|
|
|
6240
6171
|
};
|
|
6241
6172
|
|
|
6242
6173
|
const currentModel = this.model;
|
|
6174
|
+
// Prefer the active session's model: it's what the user is actively using,
|
|
6175
|
+
// and routing compaction to a different provider (e.g. an OpenAI default
|
|
6176
|
+
// model while the chat is on Anthropic) changes provider-specific behavior
|
|
6177
|
+
// like remote compaction endpoints. Role-based candidates only kick in
|
|
6178
|
+
// as auth fallbacks when the current model has no usable credentials.
|
|
6179
|
+
addCandidate(currentModel);
|
|
6243
6180
|
for (const role of MODEL_ROLE_IDS) {
|
|
6244
6181
|
addCandidate(this.#resolveRoleModelFull(role, availableModels, currentModel).model);
|
|
6245
6182
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { shimmerText } from "../../modes/theme/shimmer";
|
|
2
|
+
import { theme as currentTheme, type Theme } from "../../modes/theme/theme";
|
|
3
|
+
|
|
1
4
|
/** Format a millisecond duration as a coarse-grained human label. */
|
|
2
5
|
export function formatDuration(ms: number): string {
|
|
3
6
|
const seconds = Math.max(0, Math.round(ms / 1000));
|
|
@@ -10,14 +13,31 @@ export function formatDuration(ms: number): string {
|
|
|
10
13
|
return `${days}d`;
|
|
11
14
|
}
|
|
12
15
|
|
|
16
|
+
type ProgressBarTheme = Pick<Theme, "bold" | "fg">;
|
|
17
|
+
|
|
18
|
+
const unstyledProgressBarTheme: ProgressBarTheme = {
|
|
19
|
+
fg(_color, text) {
|
|
20
|
+
return text;
|
|
21
|
+
},
|
|
22
|
+
bold(text) {
|
|
23
|
+
return text;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function resolveProgressBarTheme(uiTheme: ProgressBarTheme | undefined): ProgressBarTheme {
|
|
28
|
+
return uiTheme ?? currentTheme ?? unstyledProgressBarTheme;
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
/**
|
|
14
32
|
* Render an ASCII progress bar with a trailing percent label.
|
|
15
33
|
* `fraction` is clamped to `[0, 1]`. `undefined` renders a dotted placeholder.
|
|
16
34
|
*/
|
|
17
|
-
export function renderAsciiBar(fraction: number | undefined, width = 24): string {
|
|
18
|
-
|
|
35
|
+
export function renderAsciiBar(fraction: number | undefined, width = 24, uiTheme?: ProgressBarTheme): string {
|
|
36
|
+
const progressBarTheme = resolveProgressBarTheme(uiTheme);
|
|
37
|
+
if (fraction === undefined) return `[${shimmerText("·".repeat(width), progressBarTheme)}]`;
|
|
19
38
|
const clamped = Math.min(Math.max(fraction, 0), 1);
|
|
20
39
|
const filled = Math.round(clamped * width);
|
|
21
40
|
const pct = Math.round(clamped * 100);
|
|
22
|
-
|
|
41
|
+
const bar = `${"█".repeat(filled)}${"░".repeat(Math.max(0, width - filled))}`;
|
|
42
|
+
return `[${shimmerText(bar, progressBarTheme)}] ${pct}%`;
|
|
23
43
|
}
|
package/src/task/executor.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import type { AgentEvent, AgentIdentity, AgentTelemetryConfig, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
9
9
|
import { recordHandoff, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
|
|
10
|
-
import {
|
|
10
|
+
import { type JsonSchemaValidationIssue, validateJsonSchemaValue } from "@oh-my-pi/pi-ai/utils/schema";
|
|
11
11
|
import { logger, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
12
12
|
import { ModelRegistry } from "../config/model-registry";
|
|
13
13
|
import { resolveModelOverrideWithAuthFallback } from "../config/model-resolver";
|
|
@@ -204,12 +204,59 @@ function parseStringifiedJson(value: unknown): unknown {
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
|
|
207
|
+
interface OutputValidator {
|
|
208
|
+
validate: (value: unknown) => { ok: true } | { ok: false; message: string; missingRequired: string[] };
|
|
209
|
+
requiredFields: string[];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildOutputValidator(schema: unknown): { validator?: OutputValidator; error?: string } {
|
|
208
213
|
const { normalized, error } = normalizeSchema(schema);
|
|
209
214
|
if (error) return { error };
|
|
210
215
|
if (normalized === undefined) return {};
|
|
211
216
|
const jsonSchema = jtdToJsonSchema(normalized);
|
|
212
|
-
|
|
217
|
+
const required = extractRequiredFields(jsonSchema);
|
|
218
|
+
return {
|
|
219
|
+
validator: {
|
|
220
|
+
requiredFields: required,
|
|
221
|
+
validate: value => {
|
|
222
|
+
const result = validateJsonSchemaValue(jsonSchema, value);
|
|
223
|
+
if (result.success) return { ok: true };
|
|
224
|
+
const missing = computeMissingRequired(required, value);
|
|
225
|
+
const message = formatValidationIssue(result.issues[0]) ?? "schema validation failed";
|
|
226
|
+
return { ok: false, message, missingRequired: missing };
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function extractRequiredFields(jsonSchema: unknown): string[] {
|
|
233
|
+
if (!jsonSchema || typeof jsonSchema !== "object") return [];
|
|
234
|
+
const required = (jsonSchema as { required?: unknown }).required;
|
|
235
|
+
return Array.isArray(required) ? required.filter((k): k is string => typeof k === "string") : [];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function computeMissingRequired(required: readonly string[], value: unknown): string[] {
|
|
239
|
+
if (required.length === 0) return [];
|
|
240
|
+
if (value === null || value === undefined) return [...required];
|
|
241
|
+
if (typeof value !== "object" || Array.isArray(value)) return [];
|
|
242
|
+
const record = value as Record<string, unknown>;
|
|
243
|
+
return required.filter(key => !(key in record) || record[key] === undefined);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function formatValidationIssue(issue: JsonSchemaValidationIssue | undefined): string | undefined {
|
|
247
|
+
if (!issue) return undefined;
|
|
248
|
+
const path = issue.path.length > 0 ? issue.path.map(String).join(".") : "(root)";
|
|
249
|
+
return `${path}: ${issue.message}`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function previewOffendingData(value: unknown, maxLength = 500): string {
|
|
253
|
+
let serialized: string;
|
|
254
|
+
try {
|
|
255
|
+
serialized = JSON.stringify(value) ?? "null";
|
|
256
|
+
} catch {
|
|
257
|
+
serialized = String(value);
|
|
258
|
+
}
|
|
259
|
+
return serialized.length > maxLength ? `${serialized.slice(0, maxLength)}…` : serialized;
|
|
213
260
|
}
|
|
214
261
|
|
|
215
262
|
function tryParseJsonOutput(text: string): unknown | undefined {
|
|
@@ -253,9 +300,9 @@ function resolveFallbackCompletion(rawOutput: string, outputSchema: unknown): {
|
|
|
253
300
|
if (parsed === undefined) return null;
|
|
254
301
|
const candidate = parseStringifiedJson(extractCompletionData(parsed));
|
|
255
302
|
if (candidate === undefined) return null;
|
|
256
|
-
const {
|
|
303
|
+
const { validator, error } = buildOutputValidator(outputSchema);
|
|
257
304
|
if (error) return null;
|
|
258
|
-
if (
|
|
305
|
+
if (validator && !validator.validate(candidate).ok) return null;
|
|
259
306
|
return { data: candidate };
|
|
260
307
|
}
|
|
261
308
|
|
|
@@ -288,6 +335,31 @@ export const SUBAGENT_WARNING_NULL_YIELD = "SYSTEM WARNING: Subagent called yiel
|
|
|
288
335
|
export const SUBAGENT_WARNING_MISSING_YIELD =
|
|
289
336
|
"SYSTEM WARNING: Subagent exited without calling yield tool after 3 reminders.";
|
|
290
337
|
|
|
338
|
+
/** Build a schema_violation outcome — surfaced as a non-zero exit so callers treat it as a failure. */
|
|
339
|
+
function buildSchemaViolationOutcome(
|
|
340
|
+
failure: { message: string; missingRequired: string[] },
|
|
341
|
+
data: unknown,
|
|
342
|
+
): { rawOutput: string; stderr: string; exitCode: number } {
|
|
343
|
+
const missing = failure.missingRequired;
|
|
344
|
+
const headline =
|
|
345
|
+
missing.length > 0
|
|
346
|
+
? `schema_violation: missing required fields: ${missing.join(", ")}`
|
|
347
|
+
: `schema_violation: ${failure.message}`;
|
|
348
|
+
const payload = {
|
|
349
|
+
error: "schema_violation",
|
|
350
|
+
message: failure.message,
|
|
351
|
+
missingRequired: missing,
|
|
352
|
+
data: previewOffendingData(data),
|
|
353
|
+
};
|
|
354
|
+
let rawOutput: string;
|
|
355
|
+
try {
|
|
356
|
+
rawOutput = JSON.stringify(payload, null, 2);
|
|
357
|
+
} catch {
|
|
358
|
+
rawOutput = `{"error":"schema_violation","message":${JSON.stringify(headline)}}`;
|
|
359
|
+
}
|
|
360
|
+
return { rawOutput, stderr: headline, exitCode: 1 };
|
|
361
|
+
}
|
|
362
|
+
|
|
291
363
|
export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): FinalizeSubprocessOutputResult {
|
|
292
364
|
let { rawOutput, exitCode, stderr } = args;
|
|
293
365
|
const { yieldItems, reportFindings, doneAborted, signalAborted, outputSchema } = args;
|
|
@@ -311,14 +383,29 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
|
|
|
311
383
|
rawOutput = rawOutput ? `${SUBAGENT_WARNING_NULL_YIELD}\n\n${rawOutput}` : SUBAGENT_WARNING_NULL_YIELD;
|
|
312
384
|
} else {
|
|
313
385
|
const completeData = normalizeCompleteData(submitData, reportFindings);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
386
|
+
const { validator, error: schemaError } = buildOutputValidator(outputSchema);
|
|
387
|
+
if (schemaError) {
|
|
388
|
+
rawOutput = `{"error":"schema_violation","message":"invalid output schema: ${schemaError.replace(/"/g, '\\"')}"}`;
|
|
389
|
+
stderr = `schema_violation: invalid output schema: ${schemaError}`;
|
|
390
|
+
exitCode = 1;
|
|
391
|
+
} else {
|
|
392
|
+
const verdict = validator ? validator.validate(completeData) : { ok: true as const };
|
|
393
|
+
if (!verdict.ok) {
|
|
394
|
+
const outcome = buildSchemaViolationOutcome(verdict, completeData);
|
|
395
|
+
rawOutput = outcome.rawOutput;
|
|
396
|
+
stderr = outcome.stderr;
|
|
397
|
+
exitCode = outcome.exitCode;
|
|
398
|
+
} else {
|
|
399
|
+
try {
|
|
400
|
+
rawOutput = JSON.stringify(completeData, null, 2) ?? "null";
|
|
401
|
+
} catch (err) {
|
|
402
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
403
|
+
rawOutput = `{"error":"Failed to serialize yield data: ${errorMessage}"}`;
|
|
404
|
+
}
|
|
405
|
+
exitCode = 0;
|
|
406
|
+
stderr = "";
|
|
407
|
+
}
|
|
319
408
|
}
|
|
320
|
-
exitCode = 0;
|
|
321
|
-
stderr = "";
|
|
322
409
|
}
|
|
323
410
|
}
|
|
324
411
|
} else {
|
|
@@ -328,14 +415,23 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
|
|
|
328
415
|
const fallback = allowFallback ? resolveFallbackCompletion(rawOutput, outputSchema) : null;
|
|
329
416
|
if (fallback) {
|
|
330
417
|
const completeData = normalizeCompleteData(fallback.data, reportFindings);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
rawOutput =
|
|
418
|
+
const { validator } = buildOutputValidator(outputSchema);
|
|
419
|
+
const verdict = validator ? validator.validate(completeData) : { ok: true as const };
|
|
420
|
+
if (!verdict.ok) {
|
|
421
|
+
const outcome = buildSchemaViolationOutcome(verdict, completeData);
|
|
422
|
+
rawOutput = outcome.rawOutput;
|
|
423
|
+
stderr = outcome.stderr;
|
|
424
|
+
exitCode = outcome.exitCode;
|
|
425
|
+
} else {
|
|
426
|
+
try {
|
|
427
|
+
rawOutput = JSON.stringify(completeData, null, 2) ?? "null";
|
|
428
|
+
} catch (err) {
|
|
429
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
430
|
+
rawOutput = `{"error":"Failed to serialize fallback completion: ${errorMessage}"}`;
|
|
431
|
+
}
|
|
432
|
+
exitCode = 0;
|
|
433
|
+
stderr = "";
|
|
336
434
|
}
|
|
337
|
-
exitCode = 0;
|
|
338
|
-
stderr = "";
|
|
339
435
|
} else if (!hasOutputSchema && allowFallback && rawOutput.trim().length > 0) {
|
|
340
436
|
exitCode = 0;
|
|
341
437
|
stderr = "";
|