@oh-my-pi/pi-coding-agent 16.0.10 → 16.1.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 +57 -0
- package/dist/cli.js +3344 -3371
- 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 +14 -8
- 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/__tests__/skill-message.test.d.ts +1 -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/assistant-message.d.ts +8 -0
- package/dist/types/modes/components/cache-invalidation-marker.d.ts +34 -0
- package/dist/types/modes/components/chat-transcript-builder.d.ts +42 -0
- package/dist/types/modes/components/compaction-summary-message.d.ts +14 -1
- package/dist/types/modes/components/index.d.ts +0 -1
- package/dist/types/modes/components/message-frame.d.ts +6 -4
- package/dist/types/modes/controllers/command-controller.d.ts +3 -2
- package/dist/types/modes/interactive-mode.d.ts +4 -2
- package/dist/types/modes/theme/theme.d.ts +7 -1
- package/dist/types/modes/types.d.ts +9 -2
- package/dist/types/registry/agent-registry.d.ts +10 -3
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +20 -1
- package/dist/types/session/compact-modes.d.ts +60 -0
- package/dist/types/session/session-context.d.ts +7 -0
- package/dist/types/session/session-dump-format.d.ts +1 -0
- package/dist/types/session/streaming-output.d.ts +0 -2
- package/dist/types/session/tool-choice-queue.d.ts +14 -0
- package/dist/types/system-prompt.d.ts +3 -3
- package/dist/types/tools/__tests__/json-tree.test.d.ts +1 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/resolve.d.ts +15 -5
- 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 +16 -9
- package/src/config/settings.ts +0 -6
- package/src/debug/log-viewer.ts +4 -4
- package/src/debug/raw-sse.ts +4 -4
- package/src/edit/file-snapshot-store.ts +1 -1
- package/src/edit/renderer.ts +9 -9
- 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/lsp/render.ts +7 -7
- package/src/main.ts +4 -1
- package/src/modes/acp/acp-agent.ts +63 -0
- package/src/modes/components/__tests__/skill-message.test.ts +92 -0
- package/src/modes/components/agent-dashboard.ts +1 -1
- package/src/modes/components/agent-hub.ts +97 -920
- package/src/modes/components/agent-transcript-viewer.ts +461 -0
- package/src/modes/components/assistant-message.ts +21 -0
- package/src/modes/components/cache-invalidation-marker.ts +84 -0
- package/src/modes/components/chat-transcript-builder.ts +476 -0
- package/src/modes/components/compaction-summary-message.ts +29 -1
- package/src/modes/components/custom-message.ts +4 -1
- package/src/modes/components/diff.ts +12 -35
- package/src/modes/components/dynamic-border.ts +1 -1
- package/src/modes/components/extensions/extension-dashboard.ts +1 -1
- package/src/modes/components/extensions/inspector-panel.ts +5 -5
- package/src/modes/components/hook-selector.ts +2 -2
- package/src/modes/components/index.ts +0 -1
- package/src/modes/components/message-frame.ts +10 -6
- package/src/modes/components/model-selector.ts +2 -2
- package/src/modes/components/overlay-box.ts +10 -9
- package/src/modes/components/skill-message.ts +39 -19
- package/src/modes/components/tiny-title-download-progress.ts +1 -1
- package/src/modes/components/welcome.ts +1 -1
- package/src/modes/controllers/command-controller.ts +12 -2
- package/src/modes/controllers/event-controller.ts +15 -1
- package/src/modes/controllers/input-controller.ts +8 -1
- package/src/modes/controllers/selector-controller.ts +11 -1
- package/src/modes/interactive-mode.ts +13 -3
- package/src/modes/theme/theme.ts +14 -0
- package/src/modes/types.ts +9 -2
- package/src/modes/utils/ui-helpers.ts +20 -2
- package/src/prompts/steering/user-interjection.md +3 -4
- package/src/prompts/tools/read.md +1 -1
- package/src/registry/agent-registry.ts +13 -4
- package/src/sdk.ts +9 -7
- package/src/session/agent-session.ts +182 -16
- package/src/session/compact-modes.ts +105 -0
- package/src/session/messages.ts +7 -9
- package/src/session/session-context.ts +54 -7
- package/src/session/session-dump-format.ts +4 -2
- package/src/session/session-history-format.ts +1 -1
- package/src/session/snapcompact-inline.ts +2 -2
- package/src/session/streaming-output.ts +5 -5
- package/src/session/tool-choice-queue.ts +59 -0
- package/src/slash-commands/builtin-registry.ts +16 -4
- package/src/system-prompt.ts +10 -9
- 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-interactive.ts +4 -4
- 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/index.ts +4 -0
- package/src/tools/irc.ts +1 -1
- package/src/tools/json-tree.ts +22 -5
- package/src/tools/read.ts +5 -6
- package/src/tools/resolve.ts +66 -41
- package/src/tui/output-block.ts +9 -9
- 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
- package/dist/types/modes/components/branch-summary-message.d.ts +0 -13
- package/src/modes/components/branch-summary-message.ts +0 -46
|
@@ -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
|
+
};
|
|
@@ -17,6 +17,13 @@ export interface SessionContext {
|
|
|
17
17
|
mode: string;
|
|
18
18
|
/** Mode-specific data from the last mode_change entry */
|
|
19
19
|
modeData?: Record<string, unknown>;
|
|
20
|
+
/**
|
|
21
|
+
* Array parallel to messages, indicating which assistant turns should
|
|
22
|
+
* have their prompt-cache misses suppressed/explained (because a model,
|
|
23
|
+
* compaction, or plan-mode transition directly preceded them).
|
|
24
|
+
* Only populated in transcript mode.
|
|
25
|
+
*/
|
|
26
|
+
cacheMissExplainedAt?: boolean[];
|
|
20
27
|
}
|
|
21
28
|
/** Lists session model strings to try when restoring, in fallback order. */
|
|
22
29
|
export declare function getRestorableSessionModels(models: Readonly<Record<string, string>>, lastModelChangeRole: string | undefined): string[];
|
|
@@ -21,6 +21,7 @@ export interface FormatSessionDumpTextOptions {
|
|
|
21
21
|
model?: Model | null;
|
|
22
22
|
thinkingLevel?: ThinkingLevel | string | null;
|
|
23
23
|
tools?: readonly SessionDumpToolInfo[];
|
|
24
|
+
inlineToolDescriptors?: boolean;
|
|
24
25
|
}
|
|
25
26
|
/**
|
|
26
27
|
* Format messages and session metadata as markdown/plain text (same as
|
|
@@ -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
|
|
@@ -67,6 +67,20 @@ export declare class ToolChoiceQueue {
|
|
|
67
67
|
get hasInFlight(): boolean;
|
|
68
68
|
/** Return the in-flight directive's onInvoked handler and mark it when called. */
|
|
69
69
|
peekInFlightInvoker(): ((input: unknown) => Promise<unknown> | unknown) | undefined;
|
|
70
|
+
/** Register (or replace by exact id) a non-forcing pending preview invoker. */
|
|
71
|
+
registerPendingInvoker(id: string, sourceToolName: string, onInvoked: (input: unknown) => Promise<unknown> | unknown): void;
|
|
72
|
+
/** Drop the pending invoker with this id (e.g. after it resolves). */
|
|
73
|
+
removePendingInvoker(id: string): void;
|
|
74
|
+
/** True when at least one non-forcing pending preview is registered. */
|
|
75
|
+
get hasPendingInvoker(): boolean;
|
|
76
|
+
/** The head (most-recently registered) pending invoker's handler, for resolve dispatch. */
|
|
77
|
+
peekPendingInvoker(): ((input: unknown) => Promise<unknown> | unknown) | undefined;
|
|
78
|
+
/** The head pending preview's stable id + source tool, for building the agent-level
|
|
79
|
+
* SoftToolRequirement (the id drives reminder re-injection when the head changes). */
|
|
80
|
+
peekPendingHead(): {
|
|
81
|
+
id: string;
|
|
82
|
+
sourceToolName: string;
|
|
83
|
+
} | undefined;
|
|
70
84
|
/** Remove all directives with the given label. Rejects in-flight if it matches. */
|
|
71
85
|
removeByLabel(label: string): void;
|
|
72
86
|
/** Empty the queue and reject any in-flight yield. */
|
|
@@ -54,11 +54,11 @@ export interface BuildSystemPromptOptions {
|
|
|
54
54
|
toolNames?: string[];
|
|
55
55
|
/** Text to append to system prompt. */
|
|
56
56
|
appendSystemPrompt?: string;
|
|
57
|
-
/**
|
|
58
|
-
|
|
57
|
+
/** Inline full tool descriptors in the system prompt. Default: true */
|
|
58
|
+
inlineToolDescriptors?: boolean;
|
|
59
59
|
/**
|
|
60
60
|
* Whether provider-native tool calling is active (no owned/in-band syntax).
|
|
61
|
-
* When true and `
|
|
61
|
+
* When true and `inlineToolDescriptors` is false, the inventory renders as a
|
|
62
62
|
* compact tool-name list; otherwise it renders full `# Tool:` sections. Default: true
|
|
63
63
|
*/
|
|
64
64
|
nativeTools?: boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -272,6 +272,10 @@ export interface ToolSession {
|
|
|
272
272
|
}): void;
|
|
273
273
|
/** Peek the currently in-flight tool-choice queue directive's invocation handler. Used by the `resolve` tool to dispatch to the pending action. */
|
|
274
274
|
peekQueueInvoker?(): ((input: unknown) => Promise<unknown> | unknown) | undefined;
|
|
275
|
+
/** Peek the most-recently registered non-forcing pending preview invoker. The `resolve`
|
|
276
|
+
* tool dispatches to it so a staged preview resolves WITHOUT forcing tool_choice — the
|
|
277
|
+
* agent-loop's SoftToolRequirement lifecycle owns reminder injection and escalation. */
|
|
278
|
+
peekPendingInvoker?(): ((input: unknown) => Promise<unknown> | unknown) | undefined;
|
|
275
279
|
/** Peek the long-lived "standing" resolve handler registered by a mode (e.g. plan mode).
|
|
276
280
|
* Consulted by the `resolve` tool as a fallback when no queue invoker is in flight,
|
|
277
281
|
* letting modes accept `resolve` invocations without forcing the tool choice every turn. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
1
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback, CustomMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
3
3
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
4
4
|
import type { Theme } from "../modes/theme/theme";
|
|
@@ -18,10 +18,13 @@ export interface ResolveToolDetails {
|
|
|
18
18
|
sourceResultDetails?: unknown;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* Register a non-forcing resolve-protocol handler for a staged preview. Wraps the
|
|
22
|
+
* caller's apply/reject into an onInvoked closure (matching the resolve schema) and
|
|
23
|
+
* stores it on the tool-choice queue's pending-invoker registry under a UNIQUE id.
|
|
24
|
+
* The `resolve` tool dispatches to it; the agent-loop's SoftToolRequirement
|
|
25
|
+
* lifecycle injects the preview reminder and escalates to a forced `resolve` only
|
|
26
|
+
* if the model declines — so a compliant turn pays ZERO tool_choice change (no
|
|
27
|
+
* prompt-cache messages-cache invalidation).
|
|
25
28
|
*
|
|
26
29
|
* This is the canonical entry point for any tool that wants preview/apply
|
|
27
30
|
* semantics. No session-level abstraction is needed: callers pass their
|
|
@@ -33,6 +36,13 @@ export declare function queueResolveHandler(session: ToolSession, options: {
|
|
|
33
36
|
apply(reason: string, extra?: Record<string, unknown>): Promise<AgentToolResult<unknown>>;
|
|
34
37
|
reject?(reason: string, extra?: Record<string, unknown>): Promise<AgentToolResult<unknown> | undefined>;
|
|
35
38
|
}): void;
|
|
39
|
+
/**
|
|
40
|
+
* The canonical preview reminder. The resolve mechanism owns the wording; the
|
|
41
|
+
* agent-loop delivers it via the session's `SoftToolRequirement.reminder` (injected
|
|
42
|
+
* once per pending-preview head) instead of a host-side steer, so it lands as a
|
|
43
|
+
* stable mid-history append and never churns the cached prefix.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildResolveReminderMessage(sourceToolName: string): CustomMessage;
|
|
36
46
|
/**
|
|
37
47
|
* Shared invocation runner used by both queued (in-flight) handlers and
|
|
38
48
|
* standing handlers (e.g. plan-mode approval). Discriminates on action,
|
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.1.0",
|
|
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.1.0",
|
|
52
|
+
"@oh-my-pi/omp-stats": "16.1.0",
|
|
53
|
+
"@oh-my-pi/pi-agent-core": "16.1.0",
|
|
54
|
+
"@oh-my-pi/pi-ai": "16.1.0",
|
|
55
|
+
"@oh-my-pi/pi-catalog": "16.1.0",
|
|
56
|
+
"@oh-my-pi/pi-mnemopi": "16.1.0",
|
|
57
|
+
"@oh-my-pi/pi-natives": "16.1.0",
|
|
58
|
+
"@oh-my-pi/pi-tui": "16.1.0",
|
|
59
|
+
"@oh-my-pi/pi-utils": "16.1.0",
|
|
60
|
+
"@oh-my-pi/pi-wire": "16.1.0",
|
|
61
|
+
"@oh-my-pi/snapcompact": "16.1.0",
|
|
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);
|
|
@@ -1,7 +1,8 @@
|
|
|
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 { Markdown } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
|
|
5
6
|
import chalk from "chalk";
|
|
6
7
|
import typesDescriptionPrompt from "../../commit/prompts/types-description.md" with { type: "text" };
|
|
7
8
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
@@ -87,7 +87,7 @@ function truncateDiffContent(diff: string): { content: string; truncated: boolea
|
|
|
87
87
|
const truncatedCount = lines.length - KEEP_HEAD_LINES - KEEP_TAIL_LINES;
|
|
88
88
|
|
|
89
89
|
return {
|
|
90
|
-
content: [...head, `\n
|
|
90
|
+
content: [...head, `\n[…${truncatedCount}ln elided…]\n`, ...tail].join("\n"),
|
|
91
91
|
truncated: true,
|
|
92
92
|
};
|
|
93
93
|
}
|
|
@@ -117,7 +117,7 @@ function processDiffs(files: string[], diffs: Map<string, string>): { result: st
|
|
|
117
117
|
}
|
|
118
118
|
content = truncated;
|
|
119
119
|
if (content.length > remaining) {
|
|
120
|
-
content = `${content.slice(0, remaining)}\n
|
|
120
|
+
content = `${content.slice(0, remaining)}\n[…${content.length - remaining}ch elided…]`;
|
|
121
121
|
if (!truncatedFiles.includes(file)) {
|
|
122
122
|
truncatedFiles.push(file);
|
|
123
123
|
}
|
|
@@ -138,7 +138,7 @@ export async function applyChangelogProposals({
|
|
|
138
138
|
|
|
139
139
|
function truncateDiff(diff: string, maxChars: number): string {
|
|
140
140
|
if (diff.length <= maxChars) return diff;
|
|
141
|
-
return `${diff.slice(0, maxChars)}\n
|
|
141
|
+
return `${diff.slice(0, maxChars)}\n[…${diff.length - maxChars}ch elided…]`;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
function formatExistingEntries(entries: Record<string, string[]>): string {
|
|
@@ -126,7 +126,7 @@ function generateContextHeader(files: FileDiff[], currentFile: string): string {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
if (toShow.length < sorted.length) {
|
|
129
|
-
lines.push(
|
|
129
|
+
lines.push(`[…${sorted.length - toShow.length} files elided…]`);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
return lines.join("\n");
|
|
@@ -5,5 +5,5 @@ export function estimateTokens(text: string): number {
|
|
|
5
5
|
export function truncateToTokenLimit(text: string, maxTokens: number): string {
|
|
6
6
|
const maxChars = maxTokens * 4;
|
|
7
7
|
if (text.length <= maxChars) return text;
|
|
8
|
-
return `${text.slice(0, maxChars)}\n
|
|
8
|
+
return `${text.slice(0, maxChars)}\n[…${text.length - maxChars}ch elided…]`;
|
|
9
9
|
}
|
|
@@ -809,11 +809,6 @@ export const SETTINGS_SCHEMA = {
|
|
|
809
809
|
description: "Remove the 1-character horizontal padding from the left and right of the terminal output",
|
|
810
810
|
},
|
|
811
811
|
},
|
|
812
|
-
// Display rendering
|
|
813
|
-
"display.tabWidth": {
|
|
814
|
-
type: "number",
|
|
815
|
-
default: 3,
|
|
816
|
-
},
|
|
817
812
|
|
|
818
813
|
"display.shimmer": {
|
|
819
814
|
type: "enum",
|
|
@@ -854,6 +849,17 @@ export const SETTINGS_SCHEMA = {
|
|
|
854
849
|
},
|
|
855
850
|
},
|
|
856
851
|
|
|
852
|
+
"display.cacheMissMarker": {
|
|
853
|
+
type: "boolean",
|
|
854
|
+
default: true,
|
|
855
|
+
ui: {
|
|
856
|
+
tab: "appearance",
|
|
857
|
+
group: "Display",
|
|
858
|
+
label: "Cache Miss Marker",
|
|
859
|
+
description: "Show a divider above an assistant turn whose request lost (missed) the prompt cache",
|
|
860
|
+
},
|
|
861
|
+
},
|
|
862
|
+
|
|
857
863
|
showHardwareCursor: {
|
|
858
864
|
type: "boolean",
|
|
859
865
|
default: true, // will be computed based on platform if undefined
|
|
@@ -919,14 +925,15 @@ export const SETTINGS_SCHEMA = {
|
|
|
919
925
|
},
|
|
920
926
|
},
|
|
921
927
|
|
|
922
|
-
|
|
928
|
+
inlineToolDescriptors: {
|
|
923
929
|
type: "boolean",
|
|
924
|
-
default:
|
|
930
|
+
default: true,
|
|
925
931
|
ui: {
|
|
926
932
|
tab: "model",
|
|
927
933
|
group: "Prompt",
|
|
928
|
-
label: "
|
|
929
|
-
description:
|
|
934
|
+
label: "Inline Tool Descriptors",
|
|
935
|
+
description:
|
|
936
|
+
"Render full tool descriptors in the system prompt and strip top-level/nested descriptions from provider tool schemas so descriptor text is sent once",
|
|
930
937
|
},
|
|
931
938
|
},
|
|
932
939
|
|
package/src/config/settings.ts
CHANGED
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
isEnoent,
|
|
23
23
|
logger,
|
|
24
24
|
procmgr,
|
|
25
|
-
setDefaultTabWidth,
|
|
26
25
|
} from "@oh-my-pi/pi-utils";
|
|
27
26
|
import { JSONC, YAML } from "bun";
|
|
28
27
|
import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings";
|
|
@@ -1103,11 +1102,6 @@ const SETTING_HOOKS: Partial<Record<SettingPath, SettingHook<any>>> = {
|
|
|
1103
1102
|
});
|
|
1104
1103
|
}
|
|
1105
1104
|
},
|
|
1106
|
-
"display.tabWidth": value => {
|
|
1107
|
-
if (typeof value === "number") {
|
|
1108
|
-
setDefaultTabWidth(value);
|
|
1109
|
-
}
|
|
1110
|
-
},
|
|
1111
1105
|
"provider.appendOnlyContext": value => {
|
|
1112
1106
|
if (typeof value === "string") {
|
|
1113
1107
|
appendOnlyModeSignal.fire(value);
|
package/src/debug/log-viewer.ts
CHANGED
|
@@ -866,21 +866,21 @@ export class DebugLogViewerComponent implements Component {
|
|
|
866
866
|
}
|
|
867
867
|
|
|
868
868
|
#frameTop(innerWidth: number): string {
|
|
869
|
-
return `${theme.
|
|
869
|
+
return `${theme.boxRound.topLeft}${theme.boxRound.horizontal.repeat(innerWidth)}${theme.boxRound.topRight}`;
|
|
870
870
|
}
|
|
871
871
|
|
|
872
872
|
#frameSeparator(innerWidth: number): string {
|
|
873
|
-
return `${theme.
|
|
873
|
+
return `${theme.boxRound.teeRight}${theme.boxRound.horizontal.repeat(innerWidth)}${theme.boxRound.teeLeft}`;
|
|
874
874
|
}
|
|
875
875
|
|
|
876
876
|
#frameBottom(innerWidth: number): string {
|
|
877
|
-
return `${theme.
|
|
877
|
+
return `${theme.boxRound.bottomLeft}${theme.boxRound.horizontal.repeat(innerWidth)}${theme.boxRound.bottomRight}`;
|
|
878
878
|
}
|
|
879
879
|
|
|
880
880
|
#frameLine(content: string, innerWidth: number): string {
|
|
881
881
|
const truncated = truncateToWidth(content, innerWidth);
|
|
882
882
|
const remaining = Math.max(0, innerWidth - visibleWidth(truncated));
|
|
883
|
-
return `${theme.
|
|
883
|
+
return `${theme.boxRound.vertical}${truncated}${padding(remaining)}${theme.boxRound.vertical}`;
|
|
884
884
|
}
|
|
885
885
|
|
|
886
886
|
#copySelected() {
|
package/src/debug/raw-sse.ts
CHANGED
|
@@ -273,20 +273,20 @@ export class RawSseViewerComponent implements Component {
|
|
|
273
273
|
}
|
|
274
274
|
|
|
275
275
|
#frameTop(innerWidth: number): string {
|
|
276
|
-
return `${theme.
|
|
276
|
+
return `${theme.boxRound.topLeft}${theme.boxRound.horizontal.repeat(innerWidth)}${theme.boxRound.topRight}`;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
#frameSeparator(innerWidth: number): string {
|
|
280
|
-
return `${theme.
|
|
280
|
+
return `${theme.boxRound.teeRight}${theme.boxRound.horizontal.repeat(innerWidth)}${theme.boxRound.teeLeft}`;
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
#frameBottom(innerWidth: number): string {
|
|
284
|
-
return `${theme.
|
|
284
|
+
return `${theme.boxRound.bottomLeft}${theme.boxRound.horizontal.repeat(innerWidth)}${theme.boxRound.bottomRight}`;
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
#frameLine(content: string, innerWidth: number): string {
|
|
288
288
|
const truncated = truncateToWidth(content, innerWidth);
|
|
289
289
|
const remaining = Math.max(0, innerWidth - visibleWidth(truncated));
|
|
290
|
-
return `${theme.
|
|
290
|
+
return `${theme.boxRound.vertical}${truncated}${padding(remaining)}${theme.boxRound.vertical}`;
|
|
291
291
|
}
|
|
292
292
|
}
|