@oh-my-pi/pi-coding-agent 16.0.10 → 16.0.11
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 +35 -0
- package/dist/cli.js +3208 -3199
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/transcript-recorder.d.ts +52 -0
- package/dist/types/commit/agentic/agent.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +0 -4
- package/dist/types/edit/file-snapshot-store.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +7 -0
- package/dist/types/modes/components/agent-hub.d.ts +6 -1
- package/dist/types/modes/components/agent-transcript-viewer.d.ts +39 -0
- package/dist/types/modes/components/chat-transcript-builder.d.ts +42 -0
- package/dist/types/modes/controllers/command-controller.d.ts +3 -2
- package/dist/types/modes/interactive-mode.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/registry/agent-registry.d.ts +10 -3
- package/dist/types/session/compact-modes.d.ts +60 -0
- package/dist/types/session/streaming-output.d.ts +0 -2
- package/dist/types/tools/__tests__/json-tree.test.d.ts +1 -0
- package/package.json +12 -12
- package/src/advisor/index.ts +1 -0
- package/src/advisor/transcript-recorder.ts +136 -0
- package/src/cli/stats-cli.ts +2 -11
- package/src/collab/host.ts +25 -13
- package/src/commit/agentic/agent.ts +2 -1
- package/src/commit/agentic/tools/git-file-diff.ts +2 -2
- package/src/commit/changelog/index.ts +1 -1
- package/src/commit/map-reduce/map-phase.ts +1 -1
- package/src/commit/map-reduce/utils.ts +1 -1
- package/src/config/settings-schema.ts +0 -5
- package/src/config/settings.ts +0 -6
- package/src/edit/file-snapshot-store.ts +1 -1
- package/src/edit/renderer.ts +7 -7
- package/src/eval/js/tool-bridge.ts +3 -2
- package/src/eval/py/prelude.py +3 -2
- package/src/export/html/tool-views.generated.js +28 -28
- package/src/extensibility/extensions/types.ts +7 -0
- package/src/hindsight/mental-models.ts +1 -1
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/history-protocol.ts +8 -3
- package/src/irc/bus.ts +8 -0
- package/src/lsp/index.ts +2 -2
- package/src/main.ts +4 -1
- package/src/modes/acp/acp-agent.ts +63 -0
- package/src/modes/components/agent-hub.ts +97 -920
- package/src/modes/components/agent-transcript-viewer.ts +461 -0
- package/src/modes/components/chat-transcript-builder.ts +462 -0
- package/src/modes/components/diff.ts +12 -35
- package/src/modes/controllers/command-controller.ts +12 -2
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +8 -1
- package/src/modes/controllers/selector-controller.ts +4 -1
- package/src/modes/interactive-mode.ts +4 -2
- package/src/modes/types.ts +2 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/registry/agent-registry.ts +13 -4
- package/src/sdk.ts +1 -1
- package/src/session/agent-session.ts +92 -3
- package/src/session/compact-modes.ts +105 -0
- package/src/session/session-dump-format.ts +1 -1
- package/src/session/session-history-format.ts +1 -1
- package/src/session/streaming-output.ts +5 -5
- package/src/slash-commands/builtin-registry.ts +16 -4
- package/src/task/executor.ts +1 -1
- package/src/task/output-manager.ts +5 -0
- package/src/tools/__tests__/json-tree.test.ts +35 -0
- package/src/tools/approval.ts +1 -1
- package/src/tools/bash.ts +0 -1
- package/src/tools/browser.ts +0 -1
- package/src/tools/eval.ts +1 -1
- package/src/tools/gh.ts +1 -1
- package/src/tools/irc.ts +1 -1
- package/src/tools/json-tree.ts +22 -5
- package/src/tools/read.ts +5 -6
- package/src/web/scrapers/firefox-addons.ts +1 -1
- package/src/web/scrapers/github.ts +1 -1
- package/src/web/scrapers/go-pkg.ts +2 -2
- package/src/web/scrapers/metacpan.ts +2 -2
- package/src/web/scrapers/nvd.ts +2 -2
- package/src/web/scrapers/ollama.ts +1 -1
- package/src/web/scrapers/opencorporates.ts +1 -1
- package/src/web/scrapers/pub-dev.ts +1 -1
- package/src/web/scrapers/repology.ts +1 -1
- package/src/web/scrapers/sourcegraph.ts +1 -1
- package/src/web/scrapers/terraform.ts +6 -6
- package/src/web/scrapers/wikidata.ts +2 -2
- package/src/workspace-tree.ts +1 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
/**
|
|
3
|
+
* Reserved transcript stem for advisor session files. Chosen so it cannot
|
|
4
|
+
* collide with a task subagent's `<id>.jsonl` (task ids are reserved against
|
|
5
|
+
* this exact stem in {@link AgentOutputManager}).
|
|
6
|
+
*/
|
|
7
|
+
export declare const ADVISOR_TRANSCRIPT_STEM = "__advisor";
|
|
8
|
+
export declare const ADVISOR_TRANSCRIPT_FILENAME = "__advisor.jsonl";
|
|
9
|
+
/**
|
|
10
|
+
* Append-only persister for an advisor agent's transcript.
|
|
11
|
+
*
|
|
12
|
+
* The advisor is a passive reviewer with its own model usage, so — like a task
|
|
13
|
+
* subagent — its turns are written to a JSONL inside the owning session's
|
|
14
|
+
* artifacts dir (`<session>/__advisor.jsonl`, `<session>/<SubId>/__advisor.jsonl`
|
|
15
|
+
* for subagent advisors). That single file gives the advisor model proper usage
|
|
16
|
+
* attribution in `omp stats` (the stats parser scans the session dir
|
|
17
|
+
* recursively) and a read-only transcript in the Agent Hub, without making the
|
|
18
|
+
* advisor a registered, messageable peer.
|
|
19
|
+
*
|
|
20
|
+
* The target is derived from the *session file* (`getSessionFile()`), never
|
|
21
|
+
* `getArtifactsDir()` — subagents adopt the parent's artifact manager, so the
|
|
22
|
+
* artifacts dir points at the parent root and every subagent advisor would
|
|
23
|
+
* collide. The file path is resolved synchronously when a message finalizes and
|
|
24
|
+
* captured for the queued write, so a `/new`, resume, or session switch in
|
|
25
|
+
* flight can never misattribute an old advisor turn into the new session's file.
|
|
26
|
+
* On such a switch the previous writer is closed and the new file opened on the
|
|
27
|
+
* next recorded turn. The recorder never truncates: the advisor's in-memory
|
|
28
|
+
* context resets/compacts independently, but every billed turn is appended here.
|
|
29
|
+
*/
|
|
30
|
+
export declare class AdvisorTranscriptRecorder {
|
|
31
|
+
#private;
|
|
32
|
+
private readonly resolveSessionFile;
|
|
33
|
+
private readonly resolveCwd;
|
|
34
|
+
/**
|
|
35
|
+
* @param after Optional barrier the queue starts behind — used on the advisor
|
|
36
|
+
* on→off→on toggle so a fresh recorder's first `open` waits for the prior
|
|
37
|
+
* recorder's `close` and the two never hold the same `__advisor.jsonl` at once.
|
|
38
|
+
*/
|
|
39
|
+
constructor(resolveSessionFile: () => string | undefined, resolveCwd: () => string, after?: Promise<unknown>);
|
|
40
|
+
/**
|
|
41
|
+
* Persist one finalized advisor message. Assistant turns carry the usage the
|
|
42
|
+
* stats parser reads; tool results round out the Hub transcript; user deltas
|
|
43
|
+
* (the advisor's "session update" prompts) are persisted but flagged
|
|
44
|
+
* `synthetic`/agent-attributed so they never inflate user-message metrics.
|
|
45
|
+
* Non-conversational message kinds are skipped.
|
|
46
|
+
*/
|
|
47
|
+
record(message: AgentMessage): void;
|
|
48
|
+
/** Flush pending writes (best-effort). */
|
|
49
|
+
flush(): Promise<void>;
|
|
50
|
+
/** Flush and close the writer, releasing the session file. */
|
|
51
|
+
close(): Promise<void>;
|
|
52
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import type { Api, Model } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
4
4
|
import type { Settings } from "../../config/settings";
|
|
@@ -775,10 +775,6 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
775
775
|
readonly description: "Remove the 1-character horizontal padding from the left and right of the terminal output";
|
|
776
776
|
};
|
|
777
777
|
};
|
|
778
|
-
readonly "display.tabWidth": {
|
|
779
|
-
readonly type: "number";
|
|
780
|
-
readonly default: 3;
|
|
781
|
-
};
|
|
782
778
|
readonly "display.shimmer": {
|
|
783
779
|
readonly type: "enum";
|
|
784
780
|
readonly values: readonly ["classic", "kitt", "disabled"];
|
|
@@ -48,7 +48,7 @@ export declare function recordFileSnapshot(session: FileSnapshotStoreOwner, abso
|
|
|
48
48
|
/**
|
|
49
49
|
* The 1-indexed file lines a hashline-formatted body actually displayed.
|
|
50
50
|
* Single `NN:` rows contribute that line; a collapsed summary `NN-MM:` row
|
|
51
|
-
* (a `{
|
|
51
|
+
* (a `{ … }` brace pair) contributes only its boundary lines `NN` and `MM` —
|
|
52
52
|
* the elided interior was never shown, so editing inside it must be rejected.
|
|
53
53
|
*/
|
|
54
54
|
export declare function parseSeenLinesFromHashlineBody(body: string): number[];
|
|
@@ -25,6 +25,7 @@ import type * as PiCodingAgent from "../../index";
|
|
|
25
25
|
import type { MemoryRuntimeContext } from "../../memory-backend";
|
|
26
26
|
import type { CustomEditor } from "../../modes/components/custom-editor";
|
|
27
27
|
import type { Theme } from "../../modes/theme/theme";
|
|
28
|
+
import type { CompactMode } from "../../session/compact-modes";
|
|
28
29
|
import type { CustomMessage } from "../../session/messages";
|
|
29
30
|
import type { ReadonlySessionManager, SessionManager } from "../../session/session-manager";
|
|
30
31
|
import type { BashToolDetails, BashToolInput, FindToolDetails, FindToolInput, ReadToolDetails, ReadToolInput, SearchToolDetails, SearchToolInput, WriteToolInput } from "../../tools";
|
|
@@ -171,6 +172,12 @@ export interface ContextUsage {
|
|
|
171
172
|
export interface CompactOptions {
|
|
172
173
|
onComplete?: (result: CompactionResult) => void;
|
|
173
174
|
onError?: (error: Error) => void;
|
|
175
|
+
/**
|
|
176
|
+
* Force a one-off compaction mode for this invocation, overriding the
|
|
177
|
+
* configured `compaction.strategy` / `remoteEnabled` (the `/compact`
|
|
178
|
+
* subcommands: `soft` | `remote` | `snapcompact`). Omitted = configured behavior.
|
|
179
|
+
*/
|
|
180
|
+
mode?: CompactMode;
|
|
174
181
|
}
|
|
175
182
|
/**
|
|
176
183
|
* Context passed to extension event handlers.
|
|
@@ -62,6 +62,11 @@ export declare class AgentHubOverlayComponent extends Container {
|
|
|
62
62
|
dispose(): void;
|
|
63
63
|
render(width: number): readonly string[];
|
|
64
64
|
handleInput(keyData: string): void;
|
|
65
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Open the fullscreen transcript viewer for an agent id (public for table Enter
|
|
67
|
+
* and tests). Mounts {@link AgentTranscriptViewer} as a `fullscreen` overlay so it
|
|
68
|
+
* owns the alternate screen; the hub table stays mounted underneath and is
|
|
69
|
+
* restored when the viewer closes. No-op without a real TUI (render-only test stub).
|
|
70
|
+
*/
|
|
66
71
|
openChat(id: string): void;
|
|
67
72
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { type Component, type TUI } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import type { KeyId } from "../../config/keybindings";
|
|
4
|
+
import type { MessageRenderer } from "../../extensibility/extensions/types";
|
|
5
|
+
import type { AgentLifecycleManager } from "../../registry/agent-lifecycle";
|
|
6
|
+
import type { AgentRegistry } from "../../registry/agent-registry";
|
|
7
|
+
import type { SessionObserverRegistry } from "../session-observer-registry";
|
|
8
|
+
import type { AgentHubRemote } from "./agent-hub";
|
|
9
|
+
export interface AgentTranscriptViewerDeps {
|
|
10
|
+
agentId: string;
|
|
11
|
+
registry: AgentRegistry;
|
|
12
|
+
/** Collab guest: read transcript from the host instead of a local file. */
|
|
13
|
+
remote?: AgentHubRemote;
|
|
14
|
+
/** Progress/cost snapshot source for the stats line. */
|
|
15
|
+
observers?: SessionObserverRegistry;
|
|
16
|
+
/** Revive+prompt path for messageable local agents. Lazy to avoid touching the global. */
|
|
17
|
+
lifecycle?: () => AgentLifecycleManager;
|
|
18
|
+
ui: TUI;
|
|
19
|
+
getTool?: (name: string) => AgentTool | undefined;
|
|
20
|
+
getMessageRenderer?: (customType: string) => MessageRenderer | undefined;
|
|
21
|
+
cwd: string;
|
|
22
|
+
hideThinkingBlock?: () => boolean;
|
|
23
|
+
expandKeys: KeyId[];
|
|
24
|
+
/** Keys that toggle the whole hub closed (app.agents.hub + app.session.observe). */
|
|
25
|
+
hubKeys: KeyId[];
|
|
26
|
+
requestRender: () => void;
|
|
27
|
+
/** Close just this viewer (Esc), returning to the hub table. */
|
|
28
|
+
onClose: () => void;
|
|
29
|
+
/** Close this viewer AND the hub (hub-toggle keys). */
|
|
30
|
+
onHubClose: () => void;
|
|
31
|
+
}
|
|
32
|
+
export declare class AgentTranscriptViewer implements Component {
|
|
33
|
+
#private;
|
|
34
|
+
private readonly deps;
|
|
35
|
+
constructor(deps: AgentTranscriptViewerDeps);
|
|
36
|
+
dispose(): void;
|
|
37
|
+
handleInput(data: string): void;
|
|
38
|
+
render(width: number): readonly string[];
|
|
39
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds transcript components from persisted session message entries — the
|
|
3
|
+
* file/remote-backed counterpart to {@link UiHelpers.addMessageToChat} (which is
|
|
4
|
+
* bound to the live InteractiveModeContext). Used by the fullscreen transcript
|
|
5
|
+
* viewer ({@link AgentTranscriptViewer}) to render a parked subagent / advisor /
|
|
6
|
+
* collab-guest transcript that has no live session.
|
|
7
|
+
*
|
|
8
|
+
* Unlike the old incremental hub sync, {@link ChatTranscriptBuilder.rebuild}
|
|
9
|
+
* always discards prior components and rebuilds the whole transcript from the
|
|
10
|
+
* supplied entries. Re-rendering a growing transcript is therefore O(n) in the
|
|
11
|
+
* entry count, but it cannot duplicate or misorder rows the way incremental
|
|
12
|
+
* component reuse could.
|
|
13
|
+
*/
|
|
14
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
15
|
+
import { type TUI } from "@oh-my-pi/pi-tui";
|
|
16
|
+
import type { MessageRenderer } from "../../extensibility/extensions/types";
|
|
17
|
+
import type { SessionMessageEntry } from "../../session/session-entries";
|
|
18
|
+
import { TranscriptContainer } from "./transcript-container";
|
|
19
|
+
export interface ChatTranscriptBuilderDeps {
|
|
20
|
+
ui: TUI;
|
|
21
|
+
getTool?: (name: string) => AgentTool | undefined;
|
|
22
|
+
getMessageRenderer?: (customType: string) => MessageRenderer | undefined;
|
|
23
|
+
cwd: string;
|
|
24
|
+
hideThinkingBlock?: () => boolean;
|
|
25
|
+
requestRender: () => void;
|
|
26
|
+
}
|
|
27
|
+
export declare class ChatTranscriptBuilder {
|
|
28
|
+
#private;
|
|
29
|
+
private readonly deps;
|
|
30
|
+
readonly container: TranscriptContainer;
|
|
31
|
+
constructor(deps: ChatTranscriptBuilderDeps);
|
|
32
|
+
/** Whether the transcript currently holds any rendered rows. */
|
|
33
|
+
get isEmpty(): boolean;
|
|
34
|
+
/** Discard all components and rebuild the whole transcript from `entries`. */
|
|
35
|
+
rebuild(entries: SessionMessageEntry[]): void;
|
|
36
|
+
/** Toggle tool-output expansion across every expandable component. */
|
|
37
|
+
setExpanded(expanded: boolean): void;
|
|
38
|
+
get expanded(): boolean;
|
|
39
|
+
/** Tear down components (sealing pending spinners) and clear build state. */
|
|
40
|
+
reset(): void;
|
|
41
|
+
dispose(): void;
|
|
42
|
+
}
|
|
@@ -3,6 +3,7 @@ import { type ProviderDetails, type UsageReport } from "@oh-my-pi/pi-ai";
|
|
|
3
3
|
import type { CompactOptions } from "../../extensibility/extensions/types";
|
|
4
4
|
import { theme } from "../../modes/theme/theme";
|
|
5
5
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
6
|
+
import type { CompactMode } from "../../session/compact-modes";
|
|
6
7
|
import { type ShakeMode } from "../../session/shake-types";
|
|
7
8
|
export declare class CommandController {
|
|
8
9
|
#private;
|
|
@@ -31,14 +32,14 @@ export declare class CommandController {
|
|
|
31
32
|
handleRenameCommand(title: string): Promise<void>;
|
|
32
33
|
handleBashCommand(command: string, excludeFromContext?: boolean): Promise<void>;
|
|
33
34
|
handlePythonCommand(code: string, excludeFromContext?: boolean): Promise<void>;
|
|
34
|
-
handleCompactCommand(customInstructions?: string, beforeFlush?: (outcome: CompactionOutcome) => void | Promise<void>): Promise<CompactionOutcome>;
|
|
35
|
+
handleCompactCommand(customInstructions?: string, mode?: CompactMode, beforeFlush?: (outcome: CompactionOutcome) => void | Promise<void>): Promise<CompactionOutcome>;
|
|
35
36
|
/**
|
|
36
37
|
* TUI handler for `/shake`. `elide` drops heavy structural content and
|
|
37
38
|
* `images` strips image blocks. Rebuilds the chat and reports counts.
|
|
38
39
|
*/
|
|
39
40
|
handleShakeCommand(mode: ShakeMode): Promise<void>;
|
|
40
41
|
handleSkillCommand(skillPath: string, args: string): Promise<void>;
|
|
41
|
-
executeCompaction(customInstructionsOrOptions?: string | CompactOptions, isAuto?: boolean, beforeFlush?: (outcome: CompactionOutcome) => void | Promise<void
|
|
42
|
+
executeCompaction(customInstructionsOrOptions?: string | CompactOptions, isAuto?: boolean, beforeFlush?: (outcome: CompactionOutcome) => void | Promise<void>, mode?: CompactMode): Promise<CompactionOutcome>;
|
|
42
43
|
handleHandoffCommand(customInstructions?: string): Promise<void>;
|
|
43
44
|
}
|
|
44
45
|
export declare function renderProviderSection(details: ProviderDetails, uiTheme: Pick<typeof theme, "fg">): string;
|
|
@@ -12,6 +12,7 @@ import type { CompactOptions } from "../extensibility/extensions/types";
|
|
|
12
12
|
import type { MCPManager } from "../mcp";
|
|
13
13
|
import { type PlanApprovalDetails } from "../plan-mode/approved-plan";
|
|
14
14
|
import type { AgentSession } from "../session/agent-session";
|
|
15
|
+
import type { CompactMode } from "../session/compact-modes";
|
|
15
16
|
import { HistoryStorage } from "../session/history-storage";
|
|
16
17
|
import type { SessionContext } from "../session/session-context";
|
|
17
18
|
import type { SessionManager } from "../session/session-manager";
|
|
@@ -298,7 +299,7 @@ export declare class InteractiveMode implements InteractiveModeContext {
|
|
|
298
299
|
handlePythonCommand(code: string, excludeFromContext?: boolean): Promise<void>;
|
|
299
300
|
handleMCPCommand(text: string): Promise<void>;
|
|
300
301
|
handleSSHCommand(text: string): Promise<void>;
|
|
301
|
-
handleCompactCommand(customInstructions?: string, beforeFlush?: (outcome: CompactionOutcome) => void | Promise<void>): Promise<CompactionOutcome>;
|
|
302
|
+
handleCompactCommand(customInstructions?: string, mode?: CompactMode, beforeFlush?: (outcome: CompactionOutcome) => void | Promise<void>): Promise<CompactionOutcome>;
|
|
302
303
|
handleHandoffCommand(customInstructions?: string): Promise<void>;
|
|
303
304
|
handleShakeCommand(mode: ShakeMode): Promise<void>;
|
|
304
305
|
executeCompaction(customInstructionsOrOptions?: string | CompactOptions, isAuto?: boolean): Promise<CompactionOutcome>;
|
|
@@ -11,6 +11,7 @@ import type { CompactOptions } from "../extensibility/extensions/types";
|
|
|
11
11
|
import type { MCPManager } from "../mcp";
|
|
12
12
|
import type { PlanApprovalDetails } from "../plan-mode/approved-plan";
|
|
13
13
|
import type { AgentSession } from "../session/agent-session";
|
|
14
|
+
import type { CompactMode } from "../session/compact-modes";
|
|
14
15
|
import type { HistoryStorage } from "../session/history-storage";
|
|
15
16
|
import type { SessionContext } from "../session/session-context";
|
|
16
17
|
import type { SessionManager } from "../session/session-manager";
|
|
@@ -273,7 +274,7 @@ export interface InteractiveModeContext {
|
|
|
273
274
|
handlePythonCommand(code: string, excludeFromContext?: boolean): Promise<void>;
|
|
274
275
|
handleMCPCommand(text: string): Promise<void>;
|
|
275
276
|
handleSSHCommand(text: string): Promise<void>;
|
|
276
|
-
handleCompactCommand(customInstructions?: string): Promise<CompactionOutcome>;
|
|
277
|
+
handleCompactCommand(customInstructions?: string, mode?: CompactMode): Promise<CompactionOutcome>;
|
|
277
278
|
handleHandoffCommand(customInstructions?: string): Promise<void>;
|
|
278
279
|
handleShakeCommand(mode: ShakeMode): Promise<void>;
|
|
279
280
|
handleMoveCommand(targetPath: string): Promise<void>;
|
|
@@ -18,7 +18,13 @@ export declare const MAIN_AGENT_ID = "Main";
|
|
|
18
18
|
* - `aborted`: hard-killed, terminal.
|
|
19
19
|
*/
|
|
20
20
|
export type AgentStatus = "running" | "idle" | "parked" | "aborted";
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* - `main`/`sub`: the user-facing agent tree (driving agent + task subagents).
|
|
23
|
+
* - `advisor`: a passive review transcript persisted like a subagent for usage
|
|
24
|
+
* attribution and Agent Hub observability, but never a peer — hidden from
|
|
25
|
+
* agent-facing rosters (`irc`, `history://`) and not messageable/revivable.
|
|
26
|
+
*/
|
|
27
|
+
export type AgentKind = "main" | "sub" | "advisor";
|
|
22
28
|
export interface AgentRef {
|
|
23
29
|
id: string;
|
|
24
30
|
displayName: string;
|
|
@@ -81,8 +87,9 @@ export declare class AgentRegistry {
|
|
|
81
87
|
get(id: string): AgentRef | undefined;
|
|
82
88
|
list(): AgentRef[];
|
|
83
89
|
/**
|
|
84
|
-
* Returns every alive agent (running | idle) except the caller.
|
|
85
|
-
*
|
|
90
|
+
* Returns every alive agent (running | idle) except the caller. Advisor refs
|
|
91
|
+
* are observability-only transcripts, never peers, so they are excluded.
|
|
92
|
+
* Flat namespace: every other agent is visible.
|
|
86
93
|
*/
|
|
87
94
|
listVisibleTo(id: string): AgentRef[];
|
|
88
95
|
onChange(listener: RegistryListener): () => void;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manual `/compact` subcommands. Kept in a dependency-free leaf module so the
|
|
3
|
+
* slash-command registry, the interactive controllers, and `AgentSession`
|
|
4
|
+
* can all import the mode metadata + parser without pulling in the heavy
|
|
5
|
+
* `agent-session` module graph (which would form an import cycle through the
|
|
6
|
+
* slash-command registry) — same rationale as `shake-types.ts`.
|
|
7
|
+
*
|
|
8
|
+
* Each mode is a one-off override layered on top of the configured
|
|
9
|
+
* `compaction.*` settings for a single invocation; it never mutates settings.
|
|
10
|
+
* Adding a mode is a single entry here: the command surface (autocomplete +
|
|
11
|
+
* ACP hint), the parser, and the engine override all read this table.
|
|
12
|
+
*/
|
|
13
|
+
/** Subcommand selecting a one-off compaction mode for manual `/compact`. */
|
|
14
|
+
export type CompactMode = "soft" | "remote" | "snapcompact";
|
|
15
|
+
/**
|
|
16
|
+
* Per-invocation overrides merged over the configured `compaction.*` settings.
|
|
17
|
+
* Narrowed to the two knobs the modes actually flip; the result stays
|
|
18
|
+
* assignable to the full `CompactionSettings`.
|
|
19
|
+
*/
|
|
20
|
+
export interface CompactionOverride {
|
|
21
|
+
strategy?: "context-full" | "snapcompact";
|
|
22
|
+
remoteEnabled?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface CompactModeDef {
|
|
25
|
+
readonly name: CompactMode;
|
|
26
|
+
/** One-line description surfaced in autocomplete + help. */
|
|
27
|
+
readonly description: string;
|
|
28
|
+
/** Settings overrides applied on top of `compaction.*` for this run. */
|
|
29
|
+
readonly overrides: CompactionOverride;
|
|
30
|
+
/**
|
|
31
|
+
* When true, the mode produces no LLM summary, so trailing focus text is
|
|
32
|
+
* meaningless and rejected by the parser (snapcompact archives history into
|
|
33
|
+
* images without a directed summary).
|
|
34
|
+
*/
|
|
35
|
+
readonly rejectsFocus?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* When true, the mode explicitly demands a remote path; the engine warns and
|
|
38
|
+
* falls back to a local summary if neither a remote endpoint nor a
|
|
39
|
+
* provider-native compaction path is available.
|
|
40
|
+
*/
|
|
41
|
+
readonly requiresRemote?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export declare const COMPACT_MODES: readonly CompactModeDef[];
|
|
44
|
+
/** Resolve a subcommand token (case-insensitive) to its mode definition. */
|
|
45
|
+
export declare function findCompactMode(name: string): CompactModeDef | undefined;
|
|
46
|
+
/** Parsed `/compact` arguments: an optional mode plus optional focus text. */
|
|
47
|
+
export interface ParsedCompactArgs {
|
|
48
|
+
mode?: CompactMode;
|
|
49
|
+
instructions?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Split `/compact` args into a leading mode subcommand + focus instructions.
|
|
53
|
+
*
|
|
54
|
+
* Backward compatible: when the first token is not a known mode, the entire
|
|
55
|
+
* argument string is treated as focus instructions (the historical behavior).
|
|
56
|
+
* A recognized mode with `rejectsFocus` and trailing text is an error.
|
|
57
|
+
*/
|
|
58
|
+
export declare function parseCompactArgs(args: string): ParsedCompactArgs | {
|
|
59
|
+
error: string;
|
|
60
|
+
};
|
|
@@ -163,8 +163,6 @@ export declare function truncateMiddle(content: string, options?: TruncationOpti
|
|
|
163
163
|
export interface InlineByteCapOptions {
|
|
164
164
|
/** Inline byte budget. Defaults to {@link DEFAULT_MAX_BYTES}. */
|
|
165
165
|
maxBytes?: number;
|
|
166
|
-
/** What the text is, for the elision marker (e.g. "bash output"). */
|
|
167
|
-
label: string;
|
|
168
166
|
/**
|
|
169
167
|
* Persist the full text as a session artifact. When an artifact id is
|
|
170
168
|
* returned, a `[raw output: artifact://<id>]` footer is appended so the
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "16.0.
|
|
4
|
+
"version": "16.0.11",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -48,17 +48,17 @@
|
|
|
48
48
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
49
49
|
"@babel/parser": "^7.29.7",
|
|
50
50
|
"@mozilla/readability": "^0.6.0",
|
|
51
|
-
"@oh-my-pi/hashline": "16.0.
|
|
52
|
-
"@oh-my-pi/omp-stats": "16.0.
|
|
53
|
-
"@oh-my-pi/pi-agent-core": "16.0.
|
|
54
|
-
"@oh-my-pi/pi-ai": "16.0.
|
|
55
|
-
"@oh-my-pi/pi-catalog": "16.0.
|
|
56
|
-
"@oh-my-pi/pi-mnemopi": "16.0.
|
|
57
|
-
"@oh-my-pi/pi-natives": "16.0.
|
|
58
|
-
"@oh-my-pi/pi-tui": "16.0.
|
|
59
|
-
"@oh-my-pi/pi-utils": "16.0.
|
|
60
|
-
"@oh-my-pi/pi-wire": "16.0.
|
|
61
|
-
"@oh-my-pi/snapcompact": "16.0.
|
|
51
|
+
"@oh-my-pi/hashline": "16.0.11",
|
|
52
|
+
"@oh-my-pi/omp-stats": "16.0.11",
|
|
53
|
+
"@oh-my-pi/pi-agent-core": "16.0.11",
|
|
54
|
+
"@oh-my-pi/pi-ai": "16.0.11",
|
|
55
|
+
"@oh-my-pi/pi-catalog": "16.0.11",
|
|
56
|
+
"@oh-my-pi/pi-mnemopi": "16.0.11",
|
|
57
|
+
"@oh-my-pi/pi-natives": "16.0.11",
|
|
58
|
+
"@oh-my-pi/pi-tui": "16.0.11",
|
|
59
|
+
"@oh-my-pi/pi-utils": "16.0.11",
|
|
60
|
+
"@oh-my-pi/pi-wire": "16.0.11",
|
|
61
|
+
"@oh-my-pi/snapcompact": "16.0.11",
|
|
62
62
|
"@opentelemetry/api": "^1.9.1",
|
|
63
63
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
64
64
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
package/src/advisor/index.ts
CHANGED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
3
|
+
import type { Message, UserMessage } from "@oh-my-pi/pi-ai";
|
|
4
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { SessionManager } from "../session/session-manager";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Reserved transcript stem for advisor session files. Chosen so it cannot
|
|
9
|
+
* collide with a task subagent's `<id>.jsonl` (task ids are reserved against
|
|
10
|
+
* this exact stem in {@link AgentOutputManager}).
|
|
11
|
+
*/
|
|
12
|
+
export const ADVISOR_TRANSCRIPT_STEM = "__advisor";
|
|
13
|
+
export const ADVISOR_TRANSCRIPT_FILENAME = `${ADVISOR_TRANSCRIPT_STEM}.jsonl`;
|
|
14
|
+
|
|
15
|
+
const JSONL_SUFFIX = ".jsonl";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Append-only persister for an advisor agent's transcript.
|
|
19
|
+
*
|
|
20
|
+
* The advisor is a passive reviewer with its own model usage, so — like a task
|
|
21
|
+
* subagent — its turns are written to a JSONL inside the owning session's
|
|
22
|
+
* artifacts dir (`<session>/__advisor.jsonl`, `<session>/<SubId>/__advisor.jsonl`
|
|
23
|
+
* for subagent advisors). That single file gives the advisor model proper usage
|
|
24
|
+
* attribution in `omp stats` (the stats parser scans the session dir
|
|
25
|
+
* recursively) and a read-only transcript in the Agent Hub, without making the
|
|
26
|
+
* advisor a registered, messageable peer.
|
|
27
|
+
*
|
|
28
|
+
* The target is derived from the *session file* (`getSessionFile()`), never
|
|
29
|
+
* `getArtifactsDir()` — subagents adopt the parent's artifact manager, so the
|
|
30
|
+
* artifacts dir points at the parent root and every subagent advisor would
|
|
31
|
+
* collide. The file path is resolved synchronously when a message finalizes and
|
|
32
|
+
* captured for the queued write, so a `/new`, resume, or session switch in
|
|
33
|
+
* flight can never misattribute an old advisor turn into the new session's file.
|
|
34
|
+
* On such a switch the previous writer is closed and the new file opened on the
|
|
35
|
+
* next recorded turn. The recorder never truncates: the advisor's in-memory
|
|
36
|
+
* context resets/compacts independently, but every billed turn is appended here.
|
|
37
|
+
*/
|
|
38
|
+
export class AdvisorTranscriptRecorder {
|
|
39
|
+
#manager: SessionManager | undefined;
|
|
40
|
+
#file: string | undefined;
|
|
41
|
+
/** Serializes the async open/close against synchronous appends so records land in order. */
|
|
42
|
+
#queue: Promise<void>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param after Optional barrier the queue starts behind — used on the advisor
|
|
46
|
+
* on→off→on toggle so a fresh recorder's first `open` waits for the prior
|
|
47
|
+
* recorder's `close` and the two never hold the same `__advisor.jsonl` at once.
|
|
48
|
+
*/
|
|
49
|
+
constructor(
|
|
50
|
+
private readonly resolveSessionFile: () => string | undefined,
|
|
51
|
+
private readonly resolveCwd: () => string,
|
|
52
|
+
after?: Promise<unknown>,
|
|
53
|
+
) {
|
|
54
|
+
this.#queue = after
|
|
55
|
+
? after.then(
|
|
56
|
+
() => {},
|
|
57
|
+
() => {},
|
|
58
|
+
)
|
|
59
|
+
: Promise.resolve();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Persist one finalized advisor message. Assistant turns carry the usage the
|
|
64
|
+
* stats parser reads; tool results round out the Hub transcript; user deltas
|
|
65
|
+
* (the advisor's "session update" prompts) are persisted but flagged
|
|
66
|
+
* `synthetic`/agent-attributed so they never inflate user-message metrics.
|
|
67
|
+
* Non-conversational message kinds are skipped.
|
|
68
|
+
*/
|
|
69
|
+
record(message: AgentMessage): void {
|
|
70
|
+
let persisted: Message;
|
|
71
|
+
switch (message.role) {
|
|
72
|
+
case "assistant":
|
|
73
|
+
case "toolResult":
|
|
74
|
+
persisted = message;
|
|
75
|
+
break;
|
|
76
|
+
case "user":
|
|
77
|
+
// Clone so the live advisor message stays untouched; mark synthetic so
|
|
78
|
+
// stats' user-message metrics skip these agent-internal review prompts.
|
|
79
|
+
persisted = { ...(message as UserMessage), synthetic: true, attribution: "agent" };
|
|
80
|
+
break;
|
|
81
|
+
default:
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const sessionFile = this.resolveSessionFile();
|
|
85
|
+
if (!sessionFile?.endsWith(JSONL_SUFFIX)) return;
|
|
86
|
+
const file = path.join(sessionFile.slice(0, -JSONL_SUFFIX.length), ADVISOR_TRANSCRIPT_FILENAME);
|
|
87
|
+
const cwd = this.resolveCwd();
|
|
88
|
+
this.#enqueue(async () => {
|
|
89
|
+
if (file !== this.#file) {
|
|
90
|
+
await this.#closeManager();
|
|
91
|
+
this.#manager = await SessionManager.open(file, undefined, undefined, {
|
|
92
|
+
initialCwd: cwd,
|
|
93
|
+
suppressBreadcrumb: true,
|
|
94
|
+
});
|
|
95
|
+
this.#file = file;
|
|
96
|
+
}
|
|
97
|
+
this.#manager?.appendMessage(persisted);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Flush pending writes (best-effort). */
|
|
102
|
+
flush(): Promise<void> {
|
|
103
|
+
return this.#enqueueResult(async () => {
|
|
104
|
+
if (this.#manager) await this.#manager.flush();
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Flush and close the writer, releasing the session file. */
|
|
109
|
+
close(): Promise<void> {
|
|
110
|
+
return this.#enqueueResult(() => this.#closeManager());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async #closeManager(): Promise<void> {
|
|
114
|
+
const manager = this.#manager;
|
|
115
|
+
this.#manager = undefined;
|
|
116
|
+
this.#file = undefined;
|
|
117
|
+
if (!manager) return;
|
|
118
|
+
try {
|
|
119
|
+
await manager.close();
|
|
120
|
+
} catch (err) {
|
|
121
|
+
logger.debug("advisor transcript close failed", { err: String(err) });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#enqueue(work: () => Promise<void>): void {
|
|
126
|
+
this.#queue = this.#queue.then(work, work).catch(err => {
|
|
127
|
+
logger.debug("advisor transcript record failed", { err: String(err) });
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#enqueueResult(work: () => Promise<void>): Promise<void> {
|
|
132
|
+
const next = this.#queue.then(work, work);
|
|
133
|
+
this.#queue = next.catch(() => {});
|
|
134
|
+
return next;
|
|
135
|
+
}
|
|
136
|
+
}
|
package/src/cli/stats-cli.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Handles `omp stats` subcommand for viewing AI usage statistics.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { truncateToWidth } from "@oh-my-pi/pi-tui/utils";
|
|
7
8
|
import { APP_NAME, formatDuration, formatNumber, formatPercent } from "@oh-my-pi/pi-utils";
|
|
8
9
|
import chalk from "chalk";
|
|
9
10
|
import { openPath } from "../utils/open";
|
|
@@ -32,7 +33,7 @@ function createSyncProgressReporter(): {
|
|
|
32
33
|
const counter = chalk.cyan(`[${event.current}/${event.total}]`);
|
|
33
34
|
const line = `${counter} ${pct}% ${label}`;
|
|
34
35
|
const columns = stream.columns ?? 120;
|
|
35
|
-
const trimmed =
|
|
36
|
+
const trimmed = truncateToWidth(line, columns - 1);
|
|
36
37
|
stream.write(`\r${trimmed.padEnd(lastWidth)}`);
|
|
37
38
|
lastWidth = trimmed.length;
|
|
38
39
|
},
|
|
@@ -50,16 +51,6 @@ function shortenSessionFile(p: string): string {
|
|
|
50
51
|
return idx >= 0 ? p.slice(idx + marker.length) : p;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
function truncateToColumns(s: string, max: number): string {
|
|
54
|
-
if (max <= 0) return "";
|
|
55
|
-
const width = Bun.stringWidth(s, { countAnsiEscapeCodes: false });
|
|
56
|
-
if (width <= max) return s;
|
|
57
|
-
// Cheap right-trim with an ellipsis - we don't need ANSI-aware slicing
|
|
58
|
-
// because the colored prefix is short and the truncated tail is the
|
|
59
|
-
// dim filename, where dropping bytes is fine.
|
|
60
|
-
return `${s.slice(0, Math.max(0, max - 1))}\u2026`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
54
|
// =============================================================================
|
|
64
55
|
// Types
|
|
65
56
|
// =============================================================================
|
package/src/collab/host.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { logger } from "@oh-my-pi/pi-utils";
|
|
|
17
17
|
import type { BusChannel, AgentEvent as WireAgentEvent, SessionEntry as WireSessionEntry } from "@oh-my-pi/pi-wire";
|
|
18
18
|
import type { InteractiveModeContext } from "../modes/types";
|
|
19
19
|
import { AgentLifecycleManager } from "../registry/agent-lifecycle";
|
|
20
|
-
import { AgentRegistry } from "../registry/agent-registry";
|
|
20
|
+
import { type AgentRef, AgentRegistry } from "../registry/agent-registry";
|
|
21
21
|
import type { AgentSessionEvent } from "../session/agent-session";
|
|
22
22
|
import { stripImagesFromMessage, USER_INTERRUPT_LABEL } from "../session/messages";
|
|
23
23
|
import type { SessionEntry as StoredSessionEntry } from "../session/session-entries";
|
|
@@ -445,18 +445,24 @@ export class CollabHost {
|
|
|
445
445
|
}
|
|
446
446
|
|
|
447
447
|
#snapshotAgents(): AgentSnapshot[] {
|
|
448
|
-
return
|
|
449
|
-
.
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
448
|
+
return (
|
|
449
|
+
AgentRegistry.global()
|
|
450
|
+
.list()
|
|
451
|
+
// Advisor transcripts are local observability only; never mirror them to
|
|
452
|
+
// guests (the wire AgentSnapshot kind has no `advisor`, and guests must not
|
|
453
|
+
// be able to chat/kill/revive them).
|
|
454
|
+
.filter((ref): ref is AgentRef & { kind: "main" | "sub" } => ref.kind !== "advisor")
|
|
455
|
+
.map(ref => ({
|
|
456
|
+
id: ref.id,
|
|
457
|
+
displayName: ref.displayName,
|
|
458
|
+
kind: ref.kind,
|
|
459
|
+
parentId: ref.parentId,
|
|
460
|
+
status: ref.status,
|
|
461
|
+
hasSessionFile: !!ref.sessionFile,
|
|
462
|
+
createdAt: ref.createdAt,
|
|
463
|
+
lastActivity: ref.lastActivity,
|
|
464
|
+
}))
|
|
465
|
+
);
|
|
460
466
|
}
|
|
461
467
|
|
|
462
468
|
#scheduleAgentsBroadcast(): void {
|
|
@@ -472,6 +478,12 @@ export class CollabHost {
|
|
|
472
478
|
this.#rejectReadOnly("agent control", fromPeer);
|
|
473
479
|
return;
|
|
474
480
|
}
|
|
481
|
+
// Advisor refs are excluded from snapshots, but reject control by id defensively:
|
|
482
|
+
// a stale/malicious client must never chat/kill/revive a read-only advisor transcript.
|
|
483
|
+
if (AgentRegistry.global().get(agentId)?.kind === "advisor") {
|
|
484
|
+
this.#socket?.send({ t: "error", message: `agent ${agentId}: advisor transcripts are read-only` }, fromPeer);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
475
487
|
const fail = (err: unknown) => {
|
|
476
488
|
logger.warn("collab agent-cmd failed", { cmd, agentId, error: String(err) });
|
|
477
489
|
this.#socket?.send({ t: "error", message: `agent ${agentId}: ${String(err)}` }, fromPeer);
|