@oh-my-pi/pi-coding-agent 17.1.7 → 17.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +127 -0
- package/dist/{CHANGELOG-5k4dq4g6.md → CHANGELOG-be1f2t8h.md} +127 -0
- package/dist/cli.js +9554 -12379
- package/dist/template-8vdv6xb4.js +1671 -0
- package/dist/template-f8wx9vfn.css +1355 -0
- package/dist/template-qat058wr.html +55 -0
- package/dist/tool-views.generated-jdfmzwmn.js +35 -0
- package/dist/types/advisor/advise-tool.d.ts +0 -7
- package/dist/types/advisor/runtime.d.ts +8 -11
- package/dist/types/advisor/transcript-recorder.d.ts +15 -0
- package/dist/types/async/job-manager.d.ts +11 -0
- package/dist/types/auto-thinking/classifier.d.ts +9 -2
- package/dist/types/cleanse/agent.d.ts +19 -0
- package/dist/types/cleanse/balance.d.ts +7 -0
- package/dist/types/cleanse/checkers.d.ts +13 -0
- package/dist/types/cleanse/index.d.ts +16 -0
- package/dist/types/cleanse/loop.d.ts +16 -0
- package/dist/types/cleanse/parsers.d.ts +13 -0
- package/dist/types/cleanse/progress.d.ts +14 -0
- package/dist/types/cleanse/types.d.ts +64 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/session-picker.d.ts +14 -7
- package/dist/types/collab/guest.d.ts +21 -2
- package/dist/types/commands/cleanse.d.ts +23 -0
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +63 -8
- package/dist/types/cursor-bridge-tools.d.ts +54 -0
- package/dist/types/cursor.d.ts +193 -8
- package/dist/types/edit/edit-clipboard.d.ts +19 -0
- package/dist/types/edit/hashline/diff.d.ts +9 -1
- package/dist/types/edit/index.d.ts +8 -1
- package/dist/types/edit/streaming.d.ts +8 -1
- package/dist/types/export/html/index.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +19 -2
- package/dist/types/extensibility/extensions/types.d.ts +25 -1
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +27 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +2 -0
- package/dist/types/internal-urls/local-protocol.d.ts +3 -2
- package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
- package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
- package/dist/types/launch/terminal-output-worker.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +33 -4
- package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
- package/dist/types/modes/components/read-tool-group.d.ts +12 -0
- package/dist/types/modes/components/session-selector.d.ts +6 -0
- package/dist/types/modes/components/status-line/component.d.ts +23 -0
- package/dist/types/modes/components/tool-execution.d.ts +16 -7
- package/dist/types/modes/components/usage-row.d.ts +2 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +8 -5
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
- package/dist/types/modes/theme/theme.d.ts +12 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/sdk.d.ts +1 -0
- package/dist/types/session/agent-session-events.d.ts +2 -0
- package/dist/types/session/agent-session-types.d.ts +36 -1
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/async-job-delivery.d.ts +8 -0
- package/dist/types/session/claude-session-store.d.ts +13 -0
- package/dist/types/session/codex-session-store.d.ts +14 -0
- package/dist/types/session/credential-pin.d.ts +58 -0
- package/dist/types/session/foreign-session-import.d.ts +15 -0
- package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
- package/dist/types/session/foreign-session-store.d.ts +23 -0
- package/dist/types/session/model-controls.d.ts +3 -3
- package/dist/types/session/session-advisors.d.ts +47 -2
- package/dist/types/session/session-entries.d.ts +19 -1
- package/dist/types/session/session-handoff.d.ts +4 -1
- package/dist/types/session/session-loader.d.ts +2 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/session-tools.d.ts +25 -1
- package/dist/types/session/turn-recovery.d.ts +10 -4
- package/dist/types/system-prompt.d.ts +2 -1
- package/dist/types/task/executor.d.ts +11 -2
- package/dist/types/task/isolation-ownership.d.ts +34 -0
- package/dist/types/task/render.d.ts +7 -0
- package/dist/types/thinking.d.ts +18 -6
- package/dist/types/tools/acp-bridge.d.ts +28 -5
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +1 -2
- package/dist/types/tools/browser/tab-worker.d.ts +1 -4
- package/dist/types/tools/grep.d.ts +17 -1
- package/dist/types/tools/hub/launch.d.ts +5 -1
- package/dist/types/tools/index.d.ts +4 -1
- package/dist/types/tools/path-utils.d.ts +19 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/xdev.d.ts +2 -1
- package/dist/types/tui/output-block.d.ts +5 -5
- package/dist/types/utils/changelog.d.ts +19 -0
- package/dist/types/utils/git.d.ts +10 -0
- package/dist/types/utils/jj.d.ts +11 -4
- package/dist/types/web/search/types.d.ts +2 -2
- package/package.json +16 -12
- package/scripts/bundle-dist.ts +3 -1
- package/src/advisor/advise-tool.ts +0 -11
- package/src/advisor/runtime.ts +138 -24
- package/src/advisor/transcript-recorder.ts +56 -0
- package/src/async/job-manager.ts +30 -7
- package/src/auto-thinking/classifier.ts +48 -10
- package/src/cleanse/agent.ts +226 -0
- package/src/cleanse/balance.ts +79 -0
- package/src/cleanse/checkers.ts +996 -0
- package/src/cleanse/index.ts +190 -0
- package/src/cleanse/loop.ts +51 -0
- package/src/cleanse/parsers.ts +726 -0
- package/src/cleanse/progress.ts +50 -0
- package/src/cleanse/prompts/assignment.md +47 -0
- package/src/cleanse/types.ts +72 -0
- package/src/cli/args.ts +6 -0
- package/src/cli/flag-tables.ts +2 -0
- package/src/cli/gallery-fixtures/fs.ts +25 -0
- package/src/cli/session-picker.ts +34 -17
- package/src/cli/ttsr-cli.ts +19 -1
- package/src/cli/update-cli.ts +3 -0
- package/src/cli/usage-cli.ts +29 -4
- package/src/cli/worktree-cli.ts +28 -11
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +11 -1
- package/src/collab/guest.ts +37 -4
- package/src/collab/host.ts +1 -0
- package/src/commands/cleanse.ts +45 -0
- package/src/commands/launch.ts +6 -0
- package/src/config/model-registry.ts +103 -25
- package/src/config/settings-schema.ts +65 -6
- package/src/config/settings.ts +25 -0
- package/src/cursor-bridge-tools.ts +81 -0
- package/src/cursor.ts +463 -12
- package/src/discovery/claude-plugins.ts +144 -34
- package/src/edit/edit-clipboard.ts +23 -0
- package/src/edit/hashline/diff.ts +49 -10
- package/src/edit/hashline/execute.ts +38 -6
- package/src/edit/hashline/filesystem.ts +27 -3
- package/src/edit/index.ts +12 -2
- package/src/edit/renderer.ts +4 -4
- package/src/edit/streaming.ts +17 -2
- package/src/export/html/index.ts +17 -10
- package/src/export/html/template.js +1 -1
- package/src/extensibility/extensions/runner.ts +70 -0
- package/src/extensibility/extensions/types.ts +30 -0
- package/src/extensibility/legacy-pi-ai-shim.ts +46 -1
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +12 -13
- package/src/internal-urls/local-protocol.ts +3 -2
- package/src/internal-urls/mcp-protocol.ts +4 -1
- package/src/launch/broker.ts +14 -4
- package/src/launch/terminal-output-worker-client.ts +53 -0
- package/src/launch/terminal-output-worker-protocol.ts +11 -0
- package/src/launch/terminal-output-worker.ts +23 -0
- package/src/lsp/client.ts +18 -3
- package/src/lsp/index.ts +4 -0
- package/src/lsp/render.ts +1 -1
- package/src/main.ts +127 -56
- package/src/mcp/manager.ts +120 -18
- package/src/modes/acp/acp-agent.ts +27 -18
- package/src/modes/acp/acp-event-mapper.ts +38 -4
- package/src/modes/components/chat-transcript-builder.ts +30 -11
- package/src/modes/components/codex-reset-fireworks.ts +369 -0
- package/src/modes/components/read-tool-group.ts +159 -17
- package/src/modes/components/session-selector.ts +19 -3
- package/src/modes/components/settings-selector.ts +9 -1
- package/src/modes/components/status-line/component.ts +381 -54
- package/src/modes/components/tool-execution.ts +78 -31
- package/src/modes/components/usage-row.ts +10 -5
- package/src/modes/controllers/event-controller.ts +26 -4
- package/src/modes/controllers/mcp-command-controller.ts +68 -3
- package/src/modes/controllers/selector-controller.ts +94 -36
- package/src/modes/controllers/tan-command-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +67 -64
- package/src/modes/prompt-action-autocomplete.ts +5 -3
- package/src/modes/rpc/rpc-client.ts +18 -0
- package/src/modes/rpc/rpc-mode.ts +16 -1
- package/src/modes/rpc/rpc-types.ts +11 -0
- package/src/modes/theme/theme.ts +151 -6
- package/src/modes/types.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +38 -21
- package/src/prompts/goals/guided-goal-interview.md +41 -6
- package/src/prompts/system/auto-thinking-difficulty.md +4 -2
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +3 -0
- package/src/prompts/system/vibe-mode-active.md +4 -1
- package/src/prompts/system/xdev-mount-notice.md +1 -1
- package/src/prompts/tools/bash.md +15 -17
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/glob.md +7 -6
- package/src/prompts/tools/goal.md +1 -1
- package/src/prompts/tools/grep.md +7 -6
- package/src/sdk.ts +178 -15
- package/src/session/agent-session-events.ts +1 -0
- package/src/session/agent-session-types.ts +40 -1
- package/src/session/agent-session.ts +330 -179
- package/src/session/async-job-delivery.ts +8 -0
- package/src/session/claude-session-store.ts +426 -0
- package/src/session/codex-session-store.ts +673 -0
- package/src/session/credential-pin.ts +93 -0
- package/src/session/foreign-session-import.ts +52 -0
- package/src/session/foreign-session-jsonl.ts +29 -0
- package/src/session/foreign-session-store.ts +26 -0
- package/src/session/model-controls.ts +15 -7
- package/src/session/session-advisors.ts +142 -23
- package/src/session/session-entries.ts +21 -1
- package/src/session/session-handoff.ts +20 -3
- package/src/session/session-history-format.ts +31 -6
- package/src/session/session-loader.ts +25 -9
- package/src/session/session-maintenance.ts +59 -11
- package/src/session/session-manager.ts +61 -0
- package/src/session/session-tools.ts +107 -5
- package/src/session/settings-stream-fn.ts +1 -0
- package/src/session/turn-recovery.ts +97 -51
- package/src/slash-commands/builtin-registry.ts +136 -5
- package/src/system-prompt.ts +3 -2
- package/src/task/executor.ts +17 -4
- package/src/task/isolation-ownership.ts +106 -0
- package/src/task/render.ts +20 -4
- package/src/task/worktree.ts +8 -0
- package/src/thinking.ts +29 -10
- package/src/tools/acp-bridge.ts +52 -8
- package/src/tools/ask.ts +3 -3
- package/src/tools/ast-edit.ts +9 -2
- package/src/tools/browser/cmux/cmux-tab.ts +9 -14
- package/src/tools/browser/tab-worker.ts +12 -35
- package/src/tools/browser.ts +7 -2
- package/src/tools/checkpoint.ts +0 -13
- package/src/tools/gh-renderer.ts +3 -3
- package/src/tools/grep.ts +64 -8
- package/src/tools/hub/launch.ts +10 -15
- package/src/tools/index.ts +38 -5
- package/src/tools/path-utils.ts +88 -0
- package/src/tools/read.ts +3 -0
- package/src/tools/write.ts +44 -3
- package/src/tools/xdev.ts +39 -14
- package/src/tui/code-cell.ts +4 -4
- package/src/tui/output-block.ts +25 -8
- package/src/utils/changelog.ts +144 -4
- package/src/utils/git.ts +61 -14
- package/src/utils/jj.ts +19 -13
- package/src/utils/shell-snapshot-fn-env.sh +5 -2
- package/src/web/search/providers/codex.ts +34 -0
- package/src/web/search/render.ts +2 -2
- package/src/web/search/types.ts +1 -1
- package/dist/types/goals/guided-setup.d.ts +0 -30
- package/src/goals/guided-setup.ts +0 -171
- package/src/prompts/goals/guided-goal-system.md +0 -33
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
3
4
|
import type { Message, UserMessage } from "@oh-my-pi/pi-ai";
|
|
4
5
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { visitEntriesFromFileStream } from "../session/session-loader";
|
|
5
7
|
import { SessionManager } from "../session/session-manager";
|
|
6
8
|
|
|
7
9
|
/**
|
|
@@ -31,6 +33,60 @@ export function isAdvisorTranscriptName(name: string): boolean {
|
|
|
31
33
|
);
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Sum the advisor spend already persisted next to a primary session transcript,
|
|
38
|
+
* keyed by advisor slug.
|
|
39
|
+
*
|
|
40
|
+
* The ledger a session keeps in memory only covers the current process, so a
|
|
41
|
+
* resumed session would report zero until the next advisor turn. The recorded
|
|
42
|
+
* transcripts are the durable copy of exactly the same finalized messages, so
|
|
43
|
+
* they are read back through the shared loader - no lock, no writer, and no
|
|
44
|
+
* second parser to keep in step with the session format.
|
|
45
|
+
*
|
|
46
|
+
* Only the session's own advisors count: subagent advisors write to
|
|
47
|
+
* `<session>/<SubId>/__advisor.jsonl`, and their spend belongs to the subagent,
|
|
48
|
+
* not to this roster. Hence the scan stays at the top level of the directory.
|
|
49
|
+
*/
|
|
50
|
+
export async function loadAdvisorTranscriptCosts(sessionFile: string | undefined): Promise<Map<string, number>> {
|
|
51
|
+
const costs = new Map<string, number>();
|
|
52
|
+
if (!sessionFile?.endsWith(JSONL_SUFFIX)) return costs;
|
|
53
|
+
const directory = sessionFile.slice(0, -JSONL_SUFFIX.length);
|
|
54
|
+
const dirents = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
55
|
+
for (const dirent of dirents) {
|
|
56
|
+
if (!dirent.isFile() || !isAdvisorTranscriptName(dirent.name)) continue;
|
|
57
|
+
const slug =
|
|
58
|
+
dirent.name === ADVISOR_TRANSCRIPT_FILENAME
|
|
59
|
+
? ""
|
|
60
|
+
: dirent.name.slice(`${ADVISOR_TRANSCRIPT_STEM}.`.length, -JSONL_SUFFIX.length);
|
|
61
|
+
let total = 0;
|
|
62
|
+
let validHeader: boolean | undefined;
|
|
63
|
+
try {
|
|
64
|
+
await visitEntriesFromFileStream(path.join(directory, dirent.name), entry => {
|
|
65
|
+
const isObject = typeof entry === "object" && entry !== null;
|
|
66
|
+
if (validHeader === undefined) {
|
|
67
|
+
validHeader = isObject && entry.type === "session" && typeof entry.id === "string";
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// A syntactically valid but non-object entry (e.g. a bare `null`
|
|
71
|
+
// line) must cost only itself, not crash entry.type access and
|
|
72
|
+
// discard everything accumulated for this transcript.
|
|
73
|
+
if (!validHeader || !isObject || entry.type !== "message") return;
|
|
74
|
+
const message = entry.message;
|
|
75
|
+
if (!message || typeof message !== "object" || message.role !== "assistant") return;
|
|
76
|
+
// One malformed usage block must cost that entry only, not the
|
|
77
|
+
// whole transcript's total.
|
|
78
|
+
const total_ = message.usage?.cost?.total;
|
|
79
|
+
if (typeof total_ === "number" && Number.isFinite(total_)) total += total_;
|
|
80
|
+
});
|
|
81
|
+
} catch (err) {
|
|
82
|
+
logger.debug("advisor transcript cost read failed", { file: dirent.name, err: String(err) });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (total > 0) costs.set(slug, total);
|
|
86
|
+
}
|
|
87
|
+
return costs;
|
|
88
|
+
}
|
|
89
|
+
|
|
34
90
|
/**
|
|
35
91
|
* Append-only persister for an advisor agent's transcript.
|
|
36
92
|
*
|
package/src/async/job-manager.ts
CHANGED
|
@@ -417,6 +417,26 @@ export class AsyncJobManager {
|
|
|
417
417
|
}
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
+
/**
|
|
421
|
+
* Immediately evict completed and failed jobs matching the filter instead of
|
|
422
|
+
* waiting for retention expiry, dropping every queued delivery so a prior
|
|
423
|
+
* session's result can never be injected into a later transcript. Returns the
|
|
424
|
+
* number of jobs evicted.
|
|
425
|
+
*
|
|
426
|
+
* A delivery whose sink call is already in flight (or drained onto a caller's
|
|
427
|
+
* yield queue) is guarded by the owner's delivery generation, not the per-id
|
|
428
|
+
* suppression marker — that marker is cleared when the id is reused.
|
|
429
|
+
*/
|
|
430
|
+
evictCompletedJobs(filter?: AsyncJobFilter): number {
|
|
431
|
+
let evicted = 0;
|
|
432
|
+
for (const job of this.#filterJobs(this.#jobs.values(), filter)) {
|
|
433
|
+
if (job.status !== "completed" && job.status !== "failed") continue;
|
|
434
|
+
this.acknowledgeDeliveries([job.id]);
|
|
435
|
+
if (this.#evictJob(job.id)) evicted += 1;
|
|
436
|
+
}
|
|
437
|
+
return evicted;
|
|
438
|
+
}
|
|
439
|
+
|
|
420
440
|
async waitForAll(): Promise<void> {
|
|
421
441
|
await Promise.all(Array.from(this.#jobs.values()).map(job => job.promise));
|
|
422
442
|
}
|
|
@@ -579,12 +599,18 @@ export class AsyncJobManager {
|
|
|
579
599
|
return candidate;
|
|
580
600
|
}
|
|
581
601
|
|
|
602
|
+
#evictJob(jobId: string): boolean {
|
|
603
|
+
clearTimeout(this.#evictionTimers.get(jobId));
|
|
604
|
+
this.#evictionTimers.delete(jobId);
|
|
605
|
+
this.#suppressedDeliveries.delete(jobId);
|
|
606
|
+
this.#watchedJobs.delete(jobId);
|
|
607
|
+
return this.#jobs.delete(jobId);
|
|
608
|
+
}
|
|
609
|
+
|
|
582
610
|
#scheduleEviction(jobId: string): void {
|
|
583
611
|
if (this.#disposed) return;
|
|
584
612
|
if (this.#retentionMs <= 0) {
|
|
585
|
-
this.#
|
|
586
|
-
this.#suppressedDeliveries.delete(jobId);
|
|
587
|
-
this.#watchedJobs.delete(jobId);
|
|
613
|
+
this.#evictJob(jobId);
|
|
588
614
|
return;
|
|
589
615
|
}
|
|
590
616
|
const existing = this.#evictionTimers.get(jobId);
|
|
@@ -592,10 +618,7 @@ export class AsyncJobManager {
|
|
|
592
618
|
clearTimeout(existing);
|
|
593
619
|
}
|
|
594
620
|
const timer = setTimeout(() => {
|
|
595
|
-
this.#
|
|
596
|
-
this.#jobs.delete(jobId);
|
|
597
|
-
this.#suppressedDeliveries.delete(jobId);
|
|
598
|
-
this.#watchedJobs.delete(jobId);
|
|
621
|
+
this.#evictJob(jobId);
|
|
599
622
|
}, this.#retentionMs);
|
|
600
623
|
timer.unref();
|
|
601
624
|
this.#evictionTimers.set(jobId, timer);
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* {@link Effort}, clamped into the active model's supported range (never below
|
|
6
6
|
* {@link Effort.Low}). Two backends, selected by `providers.autoThinkingModel`:
|
|
7
7
|
*
|
|
8
|
-
* - `online` (default): a smol model classifies into `low|medium|high|xhigh
|
|
8
|
+
* - `online` (default): a smol model classifies into `low|medium|high|xhigh`,
|
|
9
|
+
* plus `max` when the target model exposes that tier.
|
|
9
10
|
* - a local key: an on-device memory model classifies into the coarser
|
|
10
11
|
* `trivial|moderate|hard` scheme (3-class is more reliable than 4-way ordinal
|
|
11
12
|
* on sub-2B models), mapped to `low|high|xhigh`.
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
* the caller falls back to a concrete level and continues the turn.
|
|
15
16
|
*/
|
|
16
17
|
import { type AssistantMessage, completeSimple, Effort, type Model } from "@oh-my-pi/pi-ai";
|
|
18
|
+
import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
17
19
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
18
20
|
|
|
19
21
|
import type { ModelRegistry } from "../config/model-registry";
|
|
@@ -30,7 +32,31 @@ import {
|
|
|
30
32
|
} from "../tiny/models";
|
|
31
33
|
import { tinyModelClient } from "../tiny/title-client";
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Rendered classifier prompts, keyed by whether `max` is offered as a label.
|
|
37
|
+
* Two variants only, so both are memoized on first use.
|
|
38
|
+
*/
|
|
39
|
+
const DIFFICULTY_SYSTEM_PROMPTS: Partial<Record<"max" | "xhigh", string>> = {};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Highest effort this turn's classification may resolve to: the configured
|
|
43
|
+
* ceiling, further limited by what the target model actually exposes. The
|
|
44
|
+
* default keeps `auto` one tier below the top, so only an explicit
|
|
45
|
+
* `ultrathink` reaches {@link Effort.Max}.
|
|
46
|
+
*/
|
|
47
|
+
function autoEffortCeiling(deps: ClassifyDifficultyDeps): Effort {
|
|
48
|
+
if (deps.settings.get("providers.autoThinkingMaxEffort") !== Effort.Max) return Effort.XHigh;
|
|
49
|
+
return getSupportedEfforts(deps.model).includes(Effort.Max) ? Effort.Max : Effort.XHigh;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function difficultySystemPromptFor(ceiling: Effort): string {
|
|
53
|
+
const key = ceiling === Effort.Max ? "max" : "xhigh";
|
|
54
|
+
const cached = DIFFICULTY_SYSTEM_PROMPTS[key];
|
|
55
|
+
if (cached !== undefined) return cached;
|
|
56
|
+
const rendered = prompt.render(difficultySystemPrompt, { allowMax: key === "max" });
|
|
57
|
+
DIFFICULTY_SYSTEM_PROMPTS[key] = rendered;
|
|
58
|
+
return rendered;
|
|
59
|
+
}
|
|
34
60
|
|
|
35
61
|
/** Local classifiers occasionally need more room for chat-template boilerplate. */
|
|
36
62
|
const LOCAL_ANSWER_MAX_TOKENS = 16;
|
|
@@ -64,14 +90,18 @@ export async function classifyDifficulty(
|
|
|
64
90
|
): Promise<Effort | undefined> {
|
|
65
91
|
const backend = deps.settings.get("providers.autoThinkingModel");
|
|
66
92
|
const input = preprocessTinyMessage(promptText);
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
93
|
+
const online = backend === ONLINE_AUTO_THINKING_MODEL_KEY;
|
|
94
|
+
// The 3-bucket local classifier cannot select `max`, so its ceiling stays at
|
|
95
|
+
// XHigh whatever the setting says — otherwise a sparse ladder would snap its
|
|
96
|
+
// `hard` bucket up to a tier it never chose.
|
|
97
|
+
const ceiling = online ? autoEffortCeiling(deps) : Effort.XHigh;
|
|
98
|
+
const effort = online ? await classifyOnline(input, deps, ceiling) : await classifyLocal(input, backend, deps);
|
|
99
|
+
// The ceiling goes into the clamp itself: capping the request alone is not
|
|
100
|
+
// enough, because a sparse ladder snaps an excluded request back up.
|
|
101
|
+
return clampAutoThinkingEffort(deps.model, effort, ceiling);
|
|
72
102
|
}
|
|
73
103
|
|
|
74
|
-
async function classifyOnline(input: string, deps: ClassifyDifficultyDeps): Promise<Effort> {
|
|
104
|
+
async function classifyOnline(input: string, deps: ClassifyDifficultyDeps, ceiling: Effort): Promise<Effort> {
|
|
75
105
|
const resolved = resolveRoleSelection(["tiny", "smol"], deps.settings, deps.registry.getAvailable());
|
|
76
106
|
const model = resolved?.model;
|
|
77
107
|
if (!model) {
|
|
@@ -88,7 +118,7 @@ async function classifyOnline(input: string, deps: ClassifyDifficultyDeps): Prom
|
|
|
88
118
|
const response = await completeSimple(
|
|
89
119
|
model,
|
|
90
120
|
{
|
|
91
|
-
systemPrompt: [
|
|
121
|
+
systemPrompt: [difficultySystemPromptFor(ceiling)],
|
|
92
122
|
messages: [{ role: "user", content: input, timestamp: Date.now() }],
|
|
93
123
|
},
|
|
94
124
|
{
|
|
@@ -134,7 +164,13 @@ async function classifyLocal(input: string, modelKey: string, deps: ClassifyDiff
|
|
|
134
164
|
return effort;
|
|
135
165
|
}
|
|
136
166
|
|
|
137
|
-
/**
|
|
167
|
+
/**
|
|
168
|
+
* Map an online level keyword to an {@link Effort}; earliest match wins.
|
|
169
|
+
*
|
|
170
|
+
* `max` is only offered to the classifier when the target model exposes that
|
|
171
|
+
* tier, but it is always parsed: an unsupported `max` is snapped back down by
|
|
172
|
+
* {@link clampAutoThinkingEffort} rather than failing the turn.
|
|
173
|
+
*/
|
|
138
174
|
export function parseDifficultyLevel(text: string): Effort | undefined {
|
|
139
175
|
const lower = text.toLowerCase();
|
|
140
176
|
const candidates: Array<[number, Effort]> = [];
|
|
@@ -142,6 +178,8 @@ export function parseDifficultyLevel(text: string): Effort | undefined {
|
|
|
142
178
|
// inside "xhigh" (no word boundary between `x` and `h`), so the two never collide.
|
|
143
179
|
const xhigh = lower.search(/x[\s_-]?high/);
|
|
144
180
|
if (xhigh >= 0) candidates.push([xhigh, Effort.XHigh]);
|
|
181
|
+
const max = lower.search(/\bmax\b/);
|
|
182
|
+
if (max >= 0) candidates.push([max, Effort.Max]);
|
|
145
183
|
const high = lower.search(/\bhigh\b/);
|
|
146
184
|
if (high >= 0) candidates.push([high, Effort.High]);
|
|
147
185
|
const medium = lower.search(/\bmed(?:ium)?\b/);
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { getProjectDir, prompt } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { ModelRegistry } from "../config/model-registry";
|
|
3
|
+
import { formatModelString, resolveCliModel } from "../config/model-resolver";
|
|
4
|
+
import { Settings } from "../config/settings";
|
|
5
|
+
import { MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
6
|
+
import { discoverAuthStorage } from "../sdk";
|
|
7
|
+
import { SessionManager } from "../session/session-manager";
|
|
8
|
+
import { mapWithConcurrencyLimitAllSettled } from "../task/parallel";
|
|
9
|
+
import { runStructuredSubagent } from "../task/structured-subagent";
|
|
10
|
+
import type { ToolSession } from "../tools";
|
|
11
|
+
import { EventBus } from "../utils/event-bus";
|
|
12
|
+
import assignmentPrompt from "./prompts/assignment.md" with { type: "text" };
|
|
13
|
+
import type {
|
|
14
|
+
CleanseAgentOutcome,
|
|
15
|
+
CleanseAssignment,
|
|
16
|
+
CleanseCheckResult,
|
|
17
|
+
CleanseDiagnostic,
|
|
18
|
+
CleanseDiagnosticReport,
|
|
19
|
+
CleanseLoopResult,
|
|
20
|
+
} from "./types";
|
|
21
|
+
|
|
22
|
+
const MAX_DIAGNOSTIC_MESSAGE = 4_000;
|
|
23
|
+
|
|
24
|
+
/** Hooks used by the standalone command to render subagent lifecycle progress. */
|
|
25
|
+
export interface CleanseAgentHooks {
|
|
26
|
+
onStart?(name: string, assignment: CleanseAssignment): void;
|
|
27
|
+
onFinish?(outcome: CleanseAgentOutcome, assignment: CleanseAssignment): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Persisted parent session that dispatches file-disjoint cleanse workers. */
|
|
31
|
+
export interface CleanseAgentRuntime {
|
|
32
|
+
readonly model: string;
|
|
33
|
+
readonly sessionFile: string;
|
|
34
|
+
dispatch(
|
|
35
|
+
assignments: CleanseAssignment[],
|
|
36
|
+
wave: number,
|
|
37
|
+
report: CleanseDiagnosticReport,
|
|
38
|
+
signal?: AbortSignal,
|
|
39
|
+
): Promise<CleanseAgentOutcome[]>;
|
|
40
|
+
close(result?: CleanseLoopResult): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Resolve the requested model and create a fresh persisted cleanse session. */
|
|
44
|
+
export async function createCleanseAgentRuntime(options: {
|
|
45
|
+
cwd?: string;
|
|
46
|
+
model: string;
|
|
47
|
+
hooks?: CleanseAgentHooks;
|
|
48
|
+
}): Promise<CleanseAgentRuntime> {
|
|
49
|
+
const cwd = options.cwd ?? getProjectDir();
|
|
50
|
+
const [settings, authStorage] = await Promise.all([Settings.init({ cwd }), discoverAuthStorage()]);
|
|
51
|
+
const modelRegistry = new ModelRegistry(authStorage);
|
|
52
|
+
await modelRegistry.refresh();
|
|
53
|
+
const resolved = resolveCliModel({ cliModel: options.model, modelRegistry, settings });
|
|
54
|
+
if (resolved.error || !resolved.model) {
|
|
55
|
+
throw new Error(resolved.error ?? `Model "${options.model}" not found`);
|
|
56
|
+
}
|
|
57
|
+
const modelSelector = resolved.selector ?? formatModelString(resolved.model);
|
|
58
|
+
const modelDisplay = formatModelString(resolved.model);
|
|
59
|
+
const sessionManager = SessionManager.create(cwd);
|
|
60
|
+
await sessionManager.setSessionName("Cleanse", "auto");
|
|
61
|
+
sessionManager.appendCustomEntry("cleanse", {
|
|
62
|
+
status: "running",
|
|
63
|
+
model: modelDisplay,
|
|
64
|
+
selector: options.model,
|
|
65
|
+
});
|
|
66
|
+
await sessionManager.ensureOnDisk();
|
|
67
|
+
const sessionFile = sessionManager.getSessionFile();
|
|
68
|
+
if (!sessionFile) throw new Error("Cleanse session could not be persisted");
|
|
69
|
+
const eventBus = new EventBus();
|
|
70
|
+
const toolSession: ToolSession = {
|
|
71
|
+
cwd,
|
|
72
|
+
hasUI: false,
|
|
73
|
+
suppressSpawnAdvisory: true,
|
|
74
|
+
enableLsp: true,
|
|
75
|
+
enableIrc: true,
|
|
76
|
+
enableMCP: false,
|
|
77
|
+
eventBus,
|
|
78
|
+
getSessionFile: () => sessionFile,
|
|
79
|
+
getSessionId: () => sessionManager.getSessionId(),
|
|
80
|
+
getArtifactsDir: () => sessionManager.getArtifactsDir(),
|
|
81
|
+
getArtifactManager: () => sessionManager.getArtifactManager(),
|
|
82
|
+
getAgentId: () => MAIN_AGENT_ID,
|
|
83
|
+
getSessionSpawns: () => "sonic",
|
|
84
|
+
getModelString: () => modelSelector,
|
|
85
|
+
getActiveModelString: () => modelSelector,
|
|
86
|
+
getActiveModel: () => resolved.model,
|
|
87
|
+
sessionManager,
|
|
88
|
+
settings,
|
|
89
|
+
authStorage,
|
|
90
|
+
modelRegistry,
|
|
91
|
+
};
|
|
92
|
+
let closed = false;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
model: modelDisplay,
|
|
96
|
+
sessionFile,
|
|
97
|
+
async dispatch(
|
|
98
|
+
assignments: CleanseAssignment[],
|
|
99
|
+
wave: number,
|
|
100
|
+
report: CleanseDiagnosticReport,
|
|
101
|
+
signal?: AbortSignal,
|
|
102
|
+
): Promise<CleanseAgentOutcome[]> {
|
|
103
|
+
sessionManager.appendCustomEntry("cleanse_wave", {
|
|
104
|
+
wave,
|
|
105
|
+
assignments: assignments.map(assignment => ({
|
|
106
|
+
weight: assignment.weight,
|
|
107
|
+
files: assignment.groups.map(group => group.file ?? "<project>"),
|
|
108
|
+
})),
|
|
109
|
+
});
|
|
110
|
+
const settled = await mapWithConcurrencyLimitAllSettled(
|
|
111
|
+
assignments,
|
|
112
|
+
assignments.length,
|
|
113
|
+
async (assignment, index, workerSignal) => {
|
|
114
|
+
const name = `CleanseW${wave}A${index + 1}`;
|
|
115
|
+
options.hooks?.onStart?.(name, assignment);
|
|
116
|
+
const result = await runStructuredSubagent({
|
|
117
|
+
session: toolSession,
|
|
118
|
+
invocationKind: "task",
|
|
119
|
+
assignment: renderAssignment(assignment, assignments, wave, index + 1, report.checks),
|
|
120
|
+
agent: "sonic",
|
|
121
|
+
model: modelSelector,
|
|
122
|
+
identity: { label: name },
|
|
123
|
+
index,
|
|
124
|
+
enableLsp: true,
|
|
125
|
+
enableIrc: true,
|
|
126
|
+
signal: workerSignal,
|
|
127
|
+
});
|
|
128
|
+
const outcome: CleanseAgentOutcome = {
|
|
129
|
+
name,
|
|
130
|
+
success: result.result.exitCode === 0 && !result.result.error && result.result.aborted !== true,
|
|
131
|
+
output: result.result.output,
|
|
132
|
+
error: result.result.error ?? (result.result.stderr || undefined),
|
|
133
|
+
resolvedModel: result.result.resolvedModel,
|
|
134
|
+
};
|
|
135
|
+
options.hooks?.onFinish?.(outcome, assignment);
|
|
136
|
+
return outcome;
|
|
137
|
+
},
|
|
138
|
+
signal,
|
|
139
|
+
);
|
|
140
|
+
const outcomes: CleanseAgentOutcome[] = [];
|
|
141
|
+
for (let index = 0; index < settled.results.length; index += 1) {
|
|
142
|
+
const result = settled.results[index];
|
|
143
|
+
if (!result) {
|
|
144
|
+
outcomes.push({ name: `CleanseW${wave}A${index + 1}`, success: false, output: "", error: "Cancelled" });
|
|
145
|
+
} else if (result.status === "fulfilled") {
|
|
146
|
+
outcomes.push(result.value);
|
|
147
|
+
} else {
|
|
148
|
+
const error = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
149
|
+
const outcome = { name: `CleanseW${wave}A${index + 1}`, success: false, output: "", error };
|
|
150
|
+
options.hooks?.onFinish?.(outcome, assignments[index]);
|
|
151
|
+
outcomes.push(outcome);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return outcomes;
|
|
155
|
+
},
|
|
156
|
+
async close(result?: CleanseLoopResult): Promise<void> {
|
|
157
|
+
if (closed) return;
|
|
158
|
+
closed = true;
|
|
159
|
+
sessionManager.appendCustomEntry("cleanse", {
|
|
160
|
+
status: result?.status ?? "interrupted",
|
|
161
|
+
waves: result?.waves ?? 0,
|
|
162
|
+
remaining: result?.report.diagnostics.length,
|
|
163
|
+
});
|
|
164
|
+
await sessionManager.close();
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function renderAssignment(
|
|
170
|
+
assignment: CleanseAssignment,
|
|
171
|
+
allAssignments: readonly CleanseAssignment[],
|
|
172
|
+
wave: number,
|
|
173
|
+
worker: number,
|
|
174
|
+
checks: readonly CleanseCheckResult[],
|
|
175
|
+
): string {
|
|
176
|
+
const hasProjectIssues = assignment.groups.some(group => group.file === undefined);
|
|
177
|
+
const files = assignment.groups.flatMap(group => (group.file ? [group.file] : []));
|
|
178
|
+
const writeScope = [
|
|
179
|
+
...(files.length > 0 ? files.map(file => `- ${file}`) : ["- No file is named by the project-level diagnostic."]),
|
|
180
|
+
...(hasProjectIssues ? ["- Minimal additional files strictly required by project-level diagnostics."] : []),
|
|
181
|
+
].join("\n");
|
|
182
|
+
return prompt.render(assignmentPrompt, {
|
|
183
|
+
wave,
|
|
184
|
+
worker,
|
|
185
|
+
write_scope: writeScope,
|
|
186
|
+
diagnostics: formatDiagnostics(assignment.groups.flatMap(group => group.diagnostics)),
|
|
187
|
+
checker_commands: formatCheckerCommands(checks),
|
|
188
|
+
peer_assignments: formatPeerAssignments(assignment, allAssignments),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function formatDiagnostics(diagnostics: readonly CleanseDiagnostic[]): string {
|
|
193
|
+
return diagnostics
|
|
194
|
+
.map(diagnostic => {
|
|
195
|
+
const location = diagnostic.file
|
|
196
|
+
? `${diagnostic.file}${diagnostic.line ? `:${diagnostic.line}${diagnostic.column ? `:${diagnostic.column}` : ""}` : ""}`
|
|
197
|
+
: "<project>";
|
|
198
|
+
const code = diagnostic.code ? ` ${diagnostic.code}` : "";
|
|
199
|
+
const message = diagnostic.message.slice(0, MAX_DIAGNOSTIC_MESSAGE);
|
|
200
|
+
const suggestion = diagnostic.suggestion ? `\n Suggested fix: ${diagnostic.suggestion}` : "";
|
|
201
|
+
return `- [${diagnostic.severity}] ${location} — ${diagnostic.checker}${code}: ${message}${suggestion}`;
|
|
202
|
+
})
|
|
203
|
+
.join("\n");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function formatCheckerCommands(checks: readonly CleanseCheckResult[]): string {
|
|
207
|
+
const seen = new Set<string>();
|
|
208
|
+
const lines: string[] = [];
|
|
209
|
+
for (const check of checks) {
|
|
210
|
+
const key = `${check.cwd}\u0000${check.command}`;
|
|
211
|
+
if (seen.has(key)) continue;
|
|
212
|
+
seen.add(key);
|
|
213
|
+
lines.push(`- [${check.cwd}] ${check.command}`);
|
|
214
|
+
}
|
|
215
|
+
return lines.join("\n") || "- No command metadata available.";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function formatPeerAssignments(current: CleanseAssignment, assignments: readonly CleanseAssignment[]): string {
|
|
219
|
+
const lines: string[] = [];
|
|
220
|
+
for (const assignment of assignments) {
|
|
221
|
+
if (assignment.index === current.index) continue;
|
|
222
|
+
const files = assignment.groups.map(group => group.file ?? "<project-level>").join(", ");
|
|
223
|
+
lines.push(`- Worker ${assignment.index + 1}: ${files}`);
|
|
224
|
+
}
|
|
225
|
+
return lines.join("\n") || "- None.";
|
|
226
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { CleanseAssignment, CleanseDiagnostic, CleanseFileIssues, CleanseSeverity } from "./types";
|
|
2
|
+
|
|
3
|
+
const SEVERITY_WEIGHT: Record<CleanseSeverity, number> = {
|
|
4
|
+
error: 5,
|
|
5
|
+
warning: 3,
|
|
6
|
+
info: 1,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/** Estimate repair burden from severity and available location/fix evidence. */
|
|
10
|
+
export function diagnosticWeight(diagnostic: CleanseDiagnostic): number {
|
|
11
|
+
let weight = SEVERITY_WEIGHT[diagnostic.severity];
|
|
12
|
+
if (diagnostic.line === undefined) weight += 2;
|
|
13
|
+
if (!diagnostic.code) weight += 1;
|
|
14
|
+
if (!diagnostic.suggestion) weight += 1;
|
|
15
|
+
return weight;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Group diagnostics by file while keeping project-level failures together. */
|
|
19
|
+
export function groupDiagnosticsByFile(diagnostics: readonly CleanseDiagnostic[]): CleanseFileIssues[] {
|
|
20
|
+
const groups = new Map<string, CleanseDiagnostic[]>();
|
|
21
|
+
for (const diagnostic of diagnostics) {
|
|
22
|
+
const key = diagnostic.file ?? "";
|
|
23
|
+
const existing = groups.get(key);
|
|
24
|
+
if (existing) existing.push(diagnostic);
|
|
25
|
+
else groups.set(key, [diagnostic]);
|
|
26
|
+
}
|
|
27
|
+
return [...groups.entries()]
|
|
28
|
+
.map(([file, entries]) => ({
|
|
29
|
+
file: file || undefined,
|
|
30
|
+
diagnostics: entries.sort(compareDiagnostics),
|
|
31
|
+
weight: entries.reduce((sum, diagnostic) => sum + diagnosticWeight(diagnostic), 0),
|
|
32
|
+
}))
|
|
33
|
+
.sort((left, right) => right.weight - left.weight || (left.file ?? "").localeCompare(right.file ?? ""));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Balance whole-file workloads with longest-processing-time bin packing. */
|
|
37
|
+
export function balanceDiagnostics(diagnostics: readonly CleanseDiagnostic[], maxAgents: number): CleanseAssignment[] {
|
|
38
|
+
if (!Number.isInteger(maxAgents) || maxAgents <= 0) {
|
|
39
|
+
throw new Error("maxAgents must be a positive integer");
|
|
40
|
+
}
|
|
41
|
+
const groups = groupDiagnosticsByFile(diagnostics);
|
|
42
|
+
if (groups.length === 0) return [];
|
|
43
|
+
const count = Math.min(maxAgents, groups.length);
|
|
44
|
+
const assignments: CleanseAssignment[] = Array.from({ length: count }, (_, index) => ({
|
|
45
|
+
index,
|
|
46
|
+
groups: [],
|
|
47
|
+
weight: 0,
|
|
48
|
+
}));
|
|
49
|
+
for (const group of groups) {
|
|
50
|
+
let lightest = assignments[0];
|
|
51
|
+
for (let index = 1; index < assignments.length; index += 1) {
|
|
52
|
+
const candidate = assignments[index];
|
|
53
|
+
if (
|
|
54
|
+
candidate.weight < lightest.weight ||
|
|
55
|
+
(candidate.weight === lightest.weight && candidate.groups.length < lightest.groups.length) ||
|
|
56
|
+
(candidate.weight === lightest.weight &&
|
|
57
|
+
candidate.groups.length === lightest.groups.length &&
|
|
58
|
+
candidate.index < lightest.index)
|
|
59
|
+
) {
|
|
60
|
+
lightest = candidate;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
lightest.groups.push(group);
|
|
64
|
+
lightest.weight += group.weight;
|
|
65
|
+
}
|
|
66
|
+
for (const assignment of assignments) {
|
|
67
|
+
assignment.groups.sort((left, right) => (left.file ?? "").localeCompare(right.file ?? ""));
|
|
68
|
+
}
|
|
69
|
+
return assignments;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function compareDiagnostics(left: CleanseDiagnostic, right: CleanseDiagnostic): number {
|
|
73
|
+
return (
|
|
74
|
+
(left.line ?? 0) - (right.line ?? 0) ||
|
|
75
|
+
(left.column ?? 0) - (right.column ?? 0) ||
|
|
76
|
+
left.checker.localeCompare(right.checker) ||
|
|
77
|
+
left.message.localeCompare(right.message)
|
|
78
|
+
);
|
|
79
|
+
}
|