@oh-my-pi/pi-coding-agent 17.2.8 → 17.2.10
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 +94 -1
- package/dist/{CHANGELOG-0b0w17se.md → CHANGELOG-rhwzpexa.md} +94 -1
- package/dist/cli.js +13216 -15614
- package/dist/types/async/job-manager.d.ts +12 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +13 -0
- package/dist/types/discovery/agents.d.ts +11 -0
- package/dist/types/extensibility/custom-commands/types.d.ts +5 -5
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
- package/dist/types/extensibility/extensions/loader.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +34 -8
- package/dist/types/extensibility/hooks/types.d.ts +5 -5
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +10 -0
- package/dist/types/hindsight/state.d.ts +0 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/lsp/utils.d.ts +8 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/modes/acp/acp-agent.d.ts +1 -1
- package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
- package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -1
- package/dist/types/modes/acp/acp-mode.d.ts +1 -1
- package/dist/types/modes/components/agent-hub-projection.d.ts +38 -0
- package/dist/types/modes/components/agent-hub-renderer.d.ts +41 -0
- package/dist/types/modes/components/agent-hub.d.ts +12 -5
- package/dist/types/modes/components/model-browser.d.ts +9 -1
- package/dist/types/modes/interactive-mode.d.ts +8 -1
- package/dist/types/modes/session-observer-registry.d.ts +2 -0
- package/dist/types/registry/agent-lifecycle.d.ts +1 -1
- package/dist/types/registry/agent-registry.d.ts +47 -0
- package/dist/types/registry/persisted-agents.d.ts +3 -1
- package/dist/types/session/agent-session-types.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +8 -4
- package/dist/types/session/session-entries.d.ts +10 -0
- package/dist/types/session/session-loader.d.ts +11 -1
- package/dist/types/session/session-manager.d.ts +9 -1
- package/dist/types/session/session-tools.d.ts +10 -1
- package/dist/types/session/turn-recovery.d.ts +6 -2
- package/dist/types/slash-commands/acp-builtins.d.ts +1 -1
- package/dist/types/slash-commands/available-commands.d.ts +1 -1
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +2 -3
- package/dist/types/task/read-only-policy.d.ts +3 -0
- package/dist/types/task/structured-subagent.d.ts +2 -0
- package/dist/types/task/types.d.ts +4 -0
- package/dist/types/tools/bash.d.ts +3 -3
- package/dist/types/tools/browser/launch.d.ts +3 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -2
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/read.d.ts +3 -5
- package/dist/types/tools/run-scope.d.ts +27 -1
- package/dist/types/tools/shell-tokenize.d.ts +21 -3
- package/dist/types/tools/terminal-output.d.ts +1 -1
- package/dist/types/utils/changelog.d.ts +3 -2
- package/dist/types/utils/late-cleanup.d.ts +2 -0
- package/dist/types/utils/turndown.d.ts +1 -1
- package/dist/types/vibe/runtime.d.ts +4 -3
- package/dist/types/web/scrapers/readthedocs.d.ts +0 -3
- package/dist/types/web/search/providers/browser-headers.d.ts +2 -3
- package/examples/custom-tools/README.md +1 -1
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/api-demo.ts +5 -6
- package/examples/extensions/chalk-logger.ts +1 -1
- package/examples/extensions/hello.ts +2 -2
- package/examples/extensions/reload-runtime.ts +1 -1
- package/examples/extensions/tools.ts +2 -2
- package/examples/extensions/with-deps/index.ts +1 -1
- package/examples/sdk/06-extensions.ts +1 -1
- package/package.json +15 -31
- package/scripts/bundle-dist.ts +1 -11
- package/scripts/omp +11 -2
- package/src/advisor/runtime.ts +54 -0
- package/src/async/job-manager.ts +26 -0
- package/src/cli/agents-cli.ts +1 -1
- package/src/cli/args.ts +24 -2
- package/src/cli/auth-broker-cli.ts +1 -1
- package/src/cli/auth-gateway-cli.ts +1 -1
- package/src/cli/bench-cli.ts +1 -1
- package/src/cli/claude-trace-cli.ts +1 -1
- package/src/cli/config-cli.ts +1 -1
- package/src/cli/dry-balance-cli.ts +1 -1
- package/src/cli/file-processor.ts +1 -1
- package/src/cli/flag-tables.ts +4 -0
- package/src/cli/grep-cli.ts +1 -1
- package/src/cli/grievances-cli.ts +1 -1
- package/src/cli/help-extra.ts +1 -1
- package/src/cli/models-cli.ts +1 -1
- package/src/cli/plugin-cli.ts +1 -1
- package/src/cli/read-cli.ts +1 -1
- package/src/cli/setup-cli.ts +1 -1
- package/src/cli/shell-cli.ts +1 -1
- package/src/cli/ssh-cli.ts +1 -1
- package/src/cli/stats-cli.ts +4 -4
- package/src/cli/tiny-models-cli.ts +1 -1
- package/src/cli/ttsr-cli.ts +1 -1
- package/src/cli/update-cli.ts +3 -57
- package/src/cli/usage-cli.ts +1 -1
- package/src/cli/web-search-cli.ts +1 -1
- package/src/cli/worktree-cli.ts +1 -1
- package/src/commands/say.ts +1 -1
- package/src/commands/token.ts +1 -1
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/cli.ts +1 -1
- package/src/config/config-file.ts +2 -2
- package/src/config/keybindings.ts +3 -3
- package/src/config/model-discovery.ts +36 -2
- package/src/config/model-registry.ts +4 -1
- package/src/config/model-resolver.ts +54 -7
- package/src/discovery/agents.ts +73 -3
- package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +5 -4
- package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -1
- package/src/discovery/claude.ts +8 -5
- package/src/discovery/cursor.ts +8 -5
- package/src/discovery/gemini.ts +11 -8
- package/src/discovery/vscode.ts +1 -0
- package/src/discovery/windsurf.ts +4 -2
- package/src/edit/hashline/filesystem.ts +7 -7
- package/src/eval/agent-bridge.ts +1 -3
- package/src/eval/jl/prelude.jl +4 -4
- package/src/eval/js/shared/prelude.txt +2 -2
- package/src/eval/py/prelude.py +0 -3
- package/src/eval/py/runner.py +38 -1
- package/src/eval/rb/prelude.rb +1 -2
- package/src/extensibility/custom-commands/loader.ts +4 -4
- package/src/extensibility/custom-commands/types.ts +5 -5
- package/src/extensibility/custom-tools/loader.ts +4 -4
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/loader.ts +7 -6
- package/src/extensibility/extensions/types.ts +39 -8
- package/src/extensibility/hooks/loader.ts +4 -5
- package/src/extensibility/hooks/types.ts +5 -5
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +23 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +53 -0
- package/src/hindsight/state.ts +2 -22
- package/src/index.ts +2 -2
- package/src/internal-urls/memory-protocol.ts +33 -0
- package/src/launch/terminal-output.ts +1 -1
- package/src/lsp/client.ts +2 -2
- package/src/lsp/utils.ts +29 -0
- package/src/main.ts +85 -21
- package/src/markit/converters/docx.ts +1 -1
- package/src/markit/converters/epub.ts +1 -1
- package/src/markit/converters/pptx.ts +1 -1
- package/src/markit/converters/xlsx.ts +1 -1
- package/src/modes/acp/acp-agent.ts +7 -6
- package/src/modes/acp/acp-client-bridge.ts +1 -1
- package/src/modes/acp/acp-event-mapper.ts +1 -1
- package/src/modes/acp/acp-mode.ts +1 -1
- package/src/modes/components/agent-hub-projection.ts +248 -0
- package/src/modes/components/agent-hub-renderer.ts +194 -0
- package/src/modes/components/agent-hub.ts +670 -192
- package/src/modes/components/agent-transcript-viewer.ts +1 -3
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/components/bordered-loader.ts +1 -1
- package/src/modes/components/custom-editor.ts +2 -2
- package/src/modes/components/extensions/inspector-panel.ts +11 -6
- package/src/modes/components/footer.ts +1 -3
- package/src/modes/components/hook-input.ts +1 -1
- package/src/modes/components/hook-selector.ts +2 -2
- package/src/modes/components/model-browser.ts +11 -3
- package/src/modes/components/model-hub.ts +4 -1
- package/src/modes/components/session-selector.ts +2 -2
- package/src/modes/components/status-line/segments.ts +4 -3
- package/src/modes/controllers/event-controller.ts +188 -7
- package/src/modes/controllers/extension-ui-controller.ts +4 -3
- package/src/modes/controllers/mcp-command-controller.ts +81 -20
- package/src/modes/controllers/selector-controller.ts +40 -49
- package/src/modes/controllers/streaming-reveal.ts +1 -1
- package/src/modes/interactive-mode.ts +13 -2
- package/src/modes/print-mode.ts +29 -12
- package/src/modes/rpc/rpc-mode.ts +2 -2
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/session-observer-registry.ts +5 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/prompts/tools/computer.md +1 -1
- package/src/registry/agent-lifecycle.ts +35 -10
- package/src/registry/agent-registry.ts +65 -2
- package/src/registry/persisted-agents.ts +341 -16
- package/src/sdk.ts +11 -5
- package/src/session/agent-session-types.ts +8 -0
- package/src/session/agent-session.ts +177 -188
- package/src/session/session-entries.ts +10 -0
- package/src/session/session-listing.ts +1 -1
- package/src/session/session-loader.ts +74 -8
- package/src/session/session-manager.ts +22 -2
- package/src/session/session-paths.ts +105 -110
- package/src/session/session-tools.ts +28 -1
- package/src/session/settings-stream-fn.ts +7 -3
- package/src/session/turn-recovery.ts +236 -18
- package/src/session/unexpected-stop-classifier.ts +12 -3
- package/src/slash-commands/acp-builtins.ts +1 -1
- package/src/slash-commands/available-commands.ts +1 -1
- package/src/slash-commands/helpers/stats-dashboard.ts +2 -1
- package/src/task/agents.ts +1 -1
- package/src/task/executor.ts +173 -47
- package/src/task/index.ts +9 -28
- package/src/task/isolation-runner.ts +43 -12
- package/src/task/persisted-revive.ts +12 -5
- package/src/task/read-only-policy.ts +27 -0
- package/src/task/structured-subagent.ts +31 -6
- package/src/task/types.ts +4 -0
- package/src/tiny/worker.ts +1 -1
- package/src/tools/auto-generated-guard.ts +1 -1
- package/src/tools/bash-interactive.ts +4 -4
- package/src/tools/bash-interceptor.ts +8 -4
- package/src/tools/bash.ts +6 -6
- package/src/tools/browser/cmux/cmux-tab.ts +66 -18
- package/src/tools/browser/launch.ts +76 -19
- package/src/tools/browser/readable.ts +9 -9
- package/src/tools/browser/tab-supervisor.ts +12 -3
- package/src/tools/browser/tab-worker-entry.ts +1 -1
- package/src/tools/browser/tab-worker.ts +101 -12
- package/src/tools/fetch.ts +1 -1
- package/src/tools/glob.ts +4 -2
- package/src/tools/path-utils.ts +22 -5
- package/src/tools/read.ts +22 -5
- package/src/tools/run-scope.ts +290 -4
- package/src/tools/shell-tokenize.ts +38 -9
- package/src/tools/terminal-output.ts +1 -1
- package/src/tools/todo.ts +1 -1
- package/src/utils/changelog.ts +5 -4
- package/src/utils/clipboard.ts +10 -2
- package/src/utils/external-editor.ts +4 -2
- package/src/utils/jj.ts +1 -1
- package/src/utils/late-cleanup.ts +17 -0
- package/src/utils/turndown.ts +1 -2
- package/src/vibe/runtime.ts +202 -58
- package/src/web/scrapers/arxiv.ts +1 -1
- package/src/web/scrapers/go-pkg.ts +1 -1
- package/src/web/scrapers/hackage.ts +1 -12
- package/src/web/scrapers/iacr.ts +1 -1
- package/src/web/scrapers/readthedocs.ts +2 -1
- package/src/web/scrapers/twitter.ts +2 -2
- package/src/web/scrapers/types.ts +1 -1
- package/src/web/scrapers/wikipedia.ts +1 -1
- package/src/web/search/providers/browser-headers.ts +12 -39
- package/src/web/search/providers/codex.ts +3 -26
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/providers/ecosia.ts +2 -2
- package/src/web/search/providers/exa.ts +1 -1
- package/src/web/search/providers/firecrawl.ts +1 -1
- package/src/web/search/providers/google.ts +1 -1
- package/src/web/search/providers/mojeek.ts +2 -2
- package/src/web/search/providers/perplexity.ts +1 -1
- package/src/web/search/providers/public.ts +1 -1
- package/src/web/search/providers/startpage.ts +2 -2
- package/src/markit/converters/mammoth.d.ts +0 -24
package/src/index.ts
CHANGED
|
@@ -2,12 +2,12 @@ import { HookEditorComponent, HookInputComponent, HookSelectorComponent } from "
|
|
|
2
2
|
|
|
3
3
|
// Core session management
|
|
4
4
|
|
|
5
|
+
export * as zod from "@oh-my-pi/omptype/zod";
|
|
6
|
+
export { z } from "@oh-my-pi/omptype/zod";
|
|
5
7
|
// Re-export TUI components for custom tool rendering
|
|
6
8
|
export { Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
7
9
|
// Logging
|
|
8
10
|
export { getAgentDir, logger, VERSION } from "@oh-my-pi/pi-utils";
|
|
9
|
-
export * as zod from "zod/v4";
|
|
10
|
-
export { z } from "zod/v4";
|
|
11
11
|
export * from "./config/keybindings";
|
|
12
12
|
export * from "./config/model-registry";
|
|
13
13
|
// Prompt templates
|
|
@@ -222,6 +222,18 @@ function mnemopiSessionStatesFromRegistry(): MnemopiSessionState[] {
|
|
|
222
222
|
return states;
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
function memoryBackendFromContext(context?: ResolveContext): string | undefined {
|
|
226
|
+
if (!context?.settings || typeof context.settings !== "object") return undefined;
|
|
227
|
+
try {
|
|
228
|
+
const get = Reflect.get(context.settings, "get");
|
|
229
|
+
if (typeof get !== "function") return undefined;
|
|
230
|
+
const backend = Reflect.apply(get, context.settings, ["memory.backend"]);
|
|
231
|
+
return typeof backend === "string" ? backend : undefined;
|
|
232
|
+
} catch {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
225
237
|
/**
|
|
226
238
|
* Look up a mnemopi memory row by id across every live session's scoped banks.
|
|
227
239
|
* First hit wins; returns `null` when the id is not stored anywhere in scope.
|
|
@@ -278,6 +290,10 @@ export class MemoryProtocolHandler implements ProtocolHandler {
|
|
|
278
290
|
readonly immutable = true;
|
|
279
291
|
|
|
280
292
|
async resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
|
|
293
|
+
const backend = memoryBackendFromContext(context);
|
|
294
|
+
if (backend === "off") {
|
|
295
|
+
throw new Error("Unknown protocol: memory://");
|
|
296
|
+
}
|
|
281
297
|
const namespace = url.rawHost || url.hostname;
|
|
282
298
|
if (!namespace) {
|
|
283
299
|
throw new Error("memory:// URL requires a namespace: memory://root or memory://<memory-id>");
|
|
@@ -290,6 +306,23 @@ export class MemoryProtocolHandler implements ProtocolHandler {
|
|
|
290
306
|
// clipped recall preview before overwriting it (issue #4443).
|
|
291
307
|
if (namespace !== MEMORY_NAMESPACE) {
|
|
292
308
|
const mnemopiStates = mnemopiSessionStatesFromRegistry();
|
|
309
|
+
const hindsightActive =
|
|
310
|
+
backend === "hindsight" ||
|
|
311
|
+
(mnemopiStates.length === 0 &&
|
|
312
|
+
AgentRegistry.global()
|
|
313
|
+
.list()
|
|
314
|
+
.some(ref => ref.session?.getHindsightSessionState?.()));
|
|
315
|
+
if (hindsightActive) {
|
|
316
|
+
// Hindsight keeps memories server-side and exposes no
|
|
317
|
+
// `memory://<id>` addressing, yet the shared `recall` tool
|
|
318
|
+
// description still steers a follow-up `read memory://<id>`.
|
|
319
|
+
// Return a corrective pointer so that stray read self-corrects in
|
|
320
|
+
// one turn instead of derailing on the generic namespace error
|
|
321
|
+
// (issue #7587).
|
|
322
|
+
throw new Error(
|
|
323
|
+
"Hindsight memories are not addressable via memory://. Recall results are final — use `recall` to search or `reflect` to synthesize. `read memory://<id>` is only available with memory.backend=mnemopi.",
|
|
324
|
+
);
|
|
325
|
+
}
|
|
293
326
|
if (mnemopiStates.length === 0) {
|
|
294
327
|
throw new Error(
|
|
295
328
|
`Unknown memory namespace: ${namespace}. Supported: ${MEMORY_NAMESPACE} (file-backed memory summary), or a mnemopi memory id when memory.backend=mnemopi is active.`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
|
-
import xterm, { type Terminal as XtermTerminal } from "@
|
|
2
|
+
import xterm, { type Terminal as XtermTerminal } from "@oh-my-pi/pi-utils/vterm";
|
|
3
3
|
import { readTerminalRows } from "../tools/terminal-output";
|
|
4
4
|
import { DAEMON_PTY_COLUMNS, DAEMON_PTY_ROWS } from "./protocol";
|
|
5
5
|
|
package/src/lsp/client.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type {
|
|
|
17
17
|
ServerConfig,
|
|
18
18
|
WorkspaceEdit,
|
|
19
19
|
} from "./types";
|
|
20
|
-
import { detectLanguageId, fileToUri } from "./utils";
|
|
20
|
+
import { detectLanguageId, EquivalentUriMap, fileToUri } from "./utils";
|
|
21
21
|
|
|
22
22
|
// =============================================================================
|
|
23
23
|
// Client State
|
|
@@ -787,7 +787,7 @@ export async function getOrCreateClient(
|
|
|
787
787
|
proc,
|
|
788
788
|
config,
|
|
789
789
|
requestId: 0,
|
|
790
|
-
diagnostics: new
|
|
790
|
+
diagnostics: new EquivalentUriMap(),
|
|
791
791
|
diagnosticsVersion: 0,
|
|
792
792
|
dynamicCapabilityRegistrations: new Map(),
|
|
793
793
|
openFiles: new Map(),
|
package/src/lsp/utils.ts
CHANGED
|
@@ -78,6 +78,35 @@ function laxUriToFile(uri: string): string {
|
|
|
78
78
|
return filePath;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** Map that treats equivalent file URI spellings as the same key. */
|
|
82
|
+
export class EquivalentUriMap<Value> extends Map<string, Value> {
|
|
83
|
+
#key(uri: string): string {
|
|
84
|
+
if (!uri.startsWith("file://")) return uri;
|
|
85
|
+
const filePath = path.normalize(uriToFile(uri));
|
|
86
|
+
return process.platform === "win32" ? filePath.toLowerCase() : filePath;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
override delete(uri: string): boolean {
|
|
90
|
+
const key = this.#key(uri);
|
|
91
|
+
return super.delete(key);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
override get(uri: string): Value | undefined {
|
|
95
|
+
const key = this.#key(uri);
|
|
96
|
+
return super.get(key);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
override has(uri: string): boolean {
|
|
100
|
+
const key = this.#key(uri);
|
|
101
|
+
return super.has(key);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override set(uri: string, value: Value): this {
|
|
105
|
+
const key = this.#key(uri);
|
|
106
|
+
return super.set(key, value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
81
110
|
// =============================================================================
|
|
82
111
|
// Diagnostic Formatting
|
|
83
112
|
// =============================================================================
|
package/src/main.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
setProjectDir,
|
|
22
22
|
VERSION,
|
|
23
23
|
} from "@oh-my-pi/pi-utils";
|
|
24
|
-
import chalk from "chalk";
|
|
24
|
+
import chalk from "@oh-my-pi/pi-utils/chalk";
|
|
25
25
|
import { reset as resetCapabilities } from "./capability";
|
|
26
26
|
import { type Args, reportUnrecognizedFlags } from "./cli/args";
|
|
27
27
|
import { applyExtensionFlags, type ExtensionFlagSink } from "./cli/extension-flags";
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
} from "./discovery/helpers";
|
|
53
53
|
import { injectOmpExtensionCliRoots } from "./discovery/omp-extension-roots";
|
|
54
54
|
import { formatExtensionLoadNotifications } from "./extensibility/extensions/load-errors";
|
|
55
|
+
import { loadExtensions } from "./extensibility/extensions/loader";
|
|
55
56
|
import { ExtensionRunner } from "./extensibility/extensions/runner";
|
|
56
57
|
import type { ExtensionUIContext } from "./extensibility/extensions/types";
|
|
57
58
|
import { scheduleMarketplaceAutoUpdate } from "./extensibility/plugins/marketplace-auto-update";
|
|
@@ -358,11 +359,31 @@ export interface AcpSessionFactoryOptions {
|
|
|
358
359
|
sessionDir?: string;
|
|
359
360
|
authStorage: AuthStorage;
|
|
360
361
|
modelRegistry: ModelRegistry;
|
|
361
|
-
parsedArgs: Pick<Args, "apiKey">;
|
|
362
|
+
parsedArgs: Pick<Args, "apiKey" | "trustedExtensions">;
|
|
362
363
|
rawArgs: string[];
|
|
363
364
|
createSession: (options: CreateAgentSessionOptions) => Promise<CreateAgentSessionResult>;
|
|
364
365
|
}
|
|
365
366
|
|
|
367
|
+
async function loadTrustedSessionExtensions(
|
|
368
|
+
options: Pick<CreateAgentSessionOptions, "additionalExtensionPaths">,
|
|
369
|
+
cwd: string,
|
|
370
|
+
eventBus: EventBus,
|
|
371
|
+
) {
|
|
372
|
+
const paths = options.additionalExtensionPaths ?? [];
|
|
373
|
+
for (const trustedPath of paths) {
|
|
374
|
+
let stat: fsSync.Stats;
|
|
375
|
+
try {
|
|
376
|
+
stat = fsSync.statSync(trustedPath);
|
|
377
|
+
} catch {
|
|
378
|
+
throw new Error(`Trusted extension must be an existing module file: ${trustedPath}`);
|
|
379
|
+
}
|
|
380
|
+
if (!stat.isFile()) {
|
|
381
|
+
throw new Error(`Trusted extension must be a module file, not a directory: ${trustedPath}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return loadExtensions(paths, cwd, eventBus);
|
|
385
|
+
}
|
|
386
|
+
|
|
366
387
|
/**
|
|
367
388
|
* Build the per-`session/new` factory used by ACP mode.
|
|
368
389
|
*
|
|
@@ -385,6 +406,16 @@ export function createAcpSessionFactory(args: AcpSessionFactoryOptions): AcpSess
|
|
|
385
406
|
// policy (PR #3736 follow-up).
|
|
386
407
|
const titleSystemPromptSource = discoverTitleSystemPromptFile(cwd);
|
|
387
408
|
const titleSystemPrompt = await resolvePromptInput(titleSystemPromptSource, "title system prompt");
|
|
409
|
+
const eventBus = new EventBus();
|
|
410
|
+
const trustedExtensions =
|
|
411
|
+
args.parsedArgs.trustedExtensions && args.parsedArgs.trustedExtensions.length > 0
|
|
412
|
+
? await loadTrustedSessionExtensions(args.baseOptions, cwd, eventBus)
|
|
413
|
+
: undefined;
|
|
414
|
+
if (trustedExtensions && trustedExtensions.errors.length > 0) {
|
|
415
|
+
throw new Error(
|
|
416
|
+
`Trusted extension failed to load: ${trustedExtensions.errors.map(item => item.error).join("; ")}`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
388
419
|
const { session: nextSession } = await args.createSession({
|
|
389
420
|
...args.baseOptions,
|
|
390
421
|
cwd,
|
|
@@ -398,6 +429,8 @@ export function createAcpSessionFactory(args: AcpSessionFactoryOptions): AcpSess
|
|
|
398
429
|
deferUsageReserveConfirmation: true,
|
|
399
430
|
enableMCP: false,
|
|
400
431
|
titleSystemPrompt,
|
|
432
|
+
eventBus,
|
|
433
|
+
preloadedExtensions: trustedExtensions,
|
|
401
434
|
});
|
|
402
435
|
if (args.parsedArgs.apiKey && !args.baseOptions.model && nextSession.model) {
|
|
403
436
|
args.authStorage.setRuntimeApiKey(nextSession.model.provider, args.parsedArgs.apiKey);
|
|
@@ -1114,14 +1147,34 @@ export async function buildSessionOptions(
|
|
|
1114
1147
|
options.rules = [];
|
|
1115
1148
|
}
|
|
1116
1149
|
|
|
1117
|
-
//
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1150
|
+
// Trusted extension paths are an exact allowlist for extension modules.
|
|
1151
|
+
if (parsed.trustedExtensions && parsed.trustedExtensions.length > 0) {
|
|
1152
|
+
const trustedPaths = parsed.trustedExtensions.map(trustedPath => {
|
|
1153
|
+
let resolvedPath: string;
|
|
1154
|
+
let stat: fsSync.Stats;
|
|
1155
|
+
try {
|
|
1156
|
+
resolvedPath = fsSync.realpathSync.native(trustedPath);
|
|
1157
|
+
stat = fsSync.statSync(resolvedPath);
|
|
1158
|
+
} catch {
|
|
1159
|
+
throw new Error(`Trusted extension must be an existing module file: ${trustedPath}`);
|
|
1160
|
+
}
|
|
1161
|
+
if (!stat.isFile()) {
|
|
1162
|
+
throw new Error(`Trusted extension must be a module file, not a directory: ${trustedPath}`);
|
|
1163
|
+
}
|
|
1164
|
+
return resolvedPath;
|
|
1165
|
+
});
|
|
1124
1166
|
options.disableExtensionDiscovery = true;
|
|
1167
|
+
options.additionalExtensionPaths = trustedPaths;
|
|
1168
|
+
} else {
|
|
1169
|
+
// Additional extension paths from CLI
|
|
1170
|
+
const cliExtensionPaths = [...(parsed.extensions ?? []), ...(parsed.hooks ?? [])];
|
|
1171
|
+
if (cliExtensionPaths.length > 0) {
|
|
1172
|
+
options.additionalExtensionPaths = cliExtensionPaths;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
if (parsed.noExtensions) {
|
|
1176
|
+
options.disableExtensionDiscovery = true;
|
|
1177
|
+
}
|
|
1125
1178
|
}
|
|
1126
1179
|
|
|
1127
1180
|
return options;
|
|
@@ -1194,16 +1247,20 @@ export async function runRootCommand(
|
|
|
1194
1247
|
// warning before we reach the await site below.
|
|
1195
1248
|
pluginPreloadPromise.catch(() => {});
|
|
1196
1249
|
|
|
1197
|
-
//
|
|
1198
|
-
//
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1250
|
+
// Trusted files load as exact module paths, never as package roots whose
|
|
1251
|
+
// sibling hooks/tools/commands/MCP content could be discovered implicitly.
|
|
1252
|
+
if (!parsedArgs.trustedExtensions?.length) {
|
|
1253
|
+
// Register CLI-provided extension package paths (`--extension`, `--hook`) so
|
|
1254
|
+
// the `omp-plugins` discovery provider can surface their `skills/`, `hooks/`,
|
|
1255
|
+
// `tools/`, `commands/`, `rules/`, `prompts/`, and `.mcp.json` sub-trees.
|
|
1256
|
+
// Explicit roots remain authorized under `--no-extensions`; only ambient
|
|
1257
|
+
// extension discovery is disabled.
|
|
1258
|
+
const cliExtensions = [...(parsedArgs.extensions ?? []), ...(parsedArgs.hooks ?? [])];
|
|
1259
|
+
injectOmpExtensionCliRoots(cliExtensions, home, getProjectDir(), {
|
|
1260
|
+
mode: parsedArgs.noExtensions ? "explicit-only" : "merge",
|
|
1261
|
+
replace: true,
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1207
1264
|
|
|
1208
1265
|
let cwd = getProjectDir();
|
|
1209
1266
|
// Classify the host before opening auth or settings storage so every
|
|
@@ -1542,12 +1599,14 @@ export async function runRootCommand(
|
|
|
1542
1599
|
// string-flag value such as `--target @notes.md` is the flag's value, not a
|
|
1543
1600
|
// file — and the same result is handed to createAgentSession via
|
|
1544
1601
|
// `preloadedExtensions` so the discovery work is not repeated.
|
|
1545
|
-
if (isInteractive) {
|
|
1602
|
+
if (isInteractive && !parsedArgs.trustedExtensions?.length) {
|
|
1546
1603
|
sessionOptions.extensions = [...(sessionOptions.extensions ?? []), createWarpEventBridgeExtension()];
|
|
1547
1604
|
}
|
|
1548
1605
|
|
|
1549
1606
|
const eventBus = new EventBus();
|
|
1550
|
-
const extensionsResult =
|
|
1607
|
+
const extensionsResult = parsedArgs.trustedExtensions?.length
|
|
1608
|
+
? await loadTrustedSessionExtensions(sessionOptions, cwd, eventBus)
|
|
1609
|
+
: await loadSessionExtensions(sessionOptions, cwd, settingsInstance, eventBus);
|
|
1551
1610
|
const extensionFlagSink: ExtensionFlagSink = {
|
|
1552
1611
|
getFlags: () => ExtensionRunner.aggregateFlags(extensionsResult.extensions),
|
|
1553
1612
|
setFlagValue: (name, value) => {
|
|
@@ -1556,6 +1615,11 @@ export async function runRootCommand(
|
|
|
1556
1615
|
};
|
|
1557
1616
|
const initialArgs = applyExtensionFlags(extensionFlagSink, rawArgs) ?? parsedArgs;
|
|
1558
1617
|
normalizeContinueSessionArgs(initialArgs, rawArgs);
|
|
1618
|
+
if ((parsedArgs.trustedExtensions?.length ?? 0) > 0 && extensionsResult.errors.length > 0) {
|
|
1619
|
+
throw new Error(
|
|
1620
|
+
`Trusted extension failed to load: ${extensionsResult.errors.map(item => item.error).join("; ")}`,
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1559
1623
|
for (const message of formatExtensionLoadNotifications(extensionsResult.errors)) {
|
|
1560
1624
|
if (isInteractive) {
|
|
1561
1625
|
notifs.push({ kind: "warn", message });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Adapted from markit-ai (MIT). See ../NOTICE.
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import mammoth from "
|
|
3
|
+
import mammoth from "@oh-my-pi/pi-utils/docx";
|
|
4
4
|
import { createTurndown, normalizeTablesHtml } from "../../utils/turndown";
|
|
5
5
|
import type { ConversionResult, Converter, StreamInfo } from "../types";
|
|
6
6
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Adapted from markit-ai (MIT). See ../NOTICE.
|
|
2
|
-
import { XMLParser } from "
|
|
2
|
+
import { XMLParser } from "@oh-my-pi/pi-utils/xml";
|
|
3
3
|
import { createTurndown, normalizeTablesHtml } from "../../utils/turndown";
|
|
4
4
|
import { unzip, unzipText } from "../../utils/zip";
|
|
5
5
|
import type { ConversionResult, Converter, StreamInfo } from "../types";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Adapted from markit-ai (MIT). See ../NOTICE.
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { XMLParser } from "
|
|
3
|
+
import { XMLParser } from "@oh-my-pi/pi-utils/xml";
|
|
4
4
|
import { unzip, unzipText } from "../../utils/zip";
|
|
5
5
|
import type { ConversionResult, Converter, StreamInfo } from "../types";
|
|
6
6
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Adapted from markit-ai (MIT). See ../NOTICE.
|
|
2
|
-
import { XMLParser } from "
|
|
2
|
+
import { XMLParser } from "@oh-my-pi/pi-utils/xml";
|
|
3
3
|
import { unzip, unzipText } from "../../utils/zip";
|
|
4
4
|
import type { ConversionResult, Converter, StreamInfo } from "../types";
|
|
5
5
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
4
|
+
import type { AssistantMessage, Model } from "@oh-my-pi/pi-ai";
|
|
5
|
+
import { getBlobsDir, isEnoent, logger, type postmortem, VERSION } from "@oh-my-pi/pi-utils";
|
|
3
6
|
import {
|
|
4
7
|
type Agent,
|
|
5
8
|
type AgentSideConnection,
|
|
@@ -39,10 +42,7 @@ import {
|
|
|
39
42
|
type SetSessionModeRequest,
|
|
40
43
|
type SetSessionModeResponse,
|
|
41
44
|
type Usage,
|
|
42
|
-
} from "@
|
|
43
|
-
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
44
|
-
import type { AssistantMessage, Model } from "@oh-my-pi/pi-ai";
|
|
45
|
-
import { getBlobsDir, isEnoent, logger, type postmortem, VERSION } from "@oh-my-pi/pi-utils";
|
|
45
|
+
} from "@oh-my-pi/pi-utils/acp";
|
|
46
46
|
import { disableProvider, enableProvider, reset as resetCapabilities } from "../../capability";
|
|
47
47
|
import { Settings } from "../../config/settings";
|
|
48
48
|
import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
|
|
@@ -2312,7 +2312,7 @@ export class AcpAgent implements Agent {
|
|
|
2312
2312
|
this.#clientCapabilities,
|
|
2313
2313
|
);
|
|
2314
2314
|
if (this.#clientCapabilities?.elicitation?.form != null) {
|
|
2315
|
-
record.session.setUsageFallbackConfirmer(confirmation => {
|
|
2315
|
+
record.session.setUsageFallbackConfirmer((confirmation, signal) => {
|
|
2316
2316
|
const reserve =
|
|
2317
2317
|
confirmation.remainingPercent === undefined
|
|
2318
2318
|
? "inside the configured reserve margin"
|
|
@@ -2320,6 +2320,7 @@ export class AcpAgent implements Agent {
|
|
|
2320
2320
|
return uiContext.confirm(
|
|
2321
2321
|
"Coding-plan reserve reached",
|
|
2322
2322
|
`${confirmation.from} has ${reserve}. Switch to ${confirmation.to}? Choose No to keep using the current plan.`,
|
|
2323
|
+
{ signal },
|
|
2323
2324
|
);
|
|
2324
2325
|
});
|
|
2325
2326
|
}
|
|
@@ -2347,7 +2348,7 @@ export class AcpAgent implements Agent {
|
|
|
2347
2348
|
record.session.sessionManager.appendLabelChange(targetId, label);
|
|
2348
2349
|
},
|
|
2349
2350
|
getActiveTools: () => record.session.getEnabledToolNames(),
|
|
2350
|
-
getAllTools: () => record.session.
|
|
2351
|
+
getAllTools: () => record.session.getAllToolInfos(),
|
|
2351
2352
|
setActiveTools: toolNames => record.session.setActiveToolsByName(toolNames),
|
|
2352
2353
|
getCommands: () => getSessionSlashCommands(record.session),
|
|
2353
2354
|
setModel: async model => {
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
ToolCallContent,
|
|
6
6
|
ToolCallLocation,
|
|
7
7
|
ToolKind,
|
|
8
|
-
} from "@
|
|
8
|
+
} from "@oh-my-pi/pi-utils/acp";
|
|
9
9
|
import { parseXdUrl } from "../../internal-urls/xd-protocol";
|
|
10
10
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
11
11
|
import { resolveToCwd } from "../../tools/path-utils";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as stream from "node:stream";
|
|
2
|
-
import { AgentSideConnection, ndJsonStream, type Stream } from "@agentclientprotocol/sdk";
|
|
3
2
|
import { postmortem } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { AgentSideConnection, ndJsonStream, type Stream } from "@oh-my-pi/pi-utils/acp";
|
|
4
4
|
import type { AgentSession } from "../../session/agent-session";
|
|
5
5
|
import { AcpAgent } from "./acp-agent";
|
|
6
6
|
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import type { AgentMetricsSummary, AgentRef, AgentStatus } from "../../registry/agent-registry";
|
|
2
|
+
import { MAIN_AGENT_ID } from "../../registry/agent-registry";
|
|
3
|
+
import type { ObservableSession } from "../session-observer-registry";
|
|
4
|
+
|
|
5
|
+
export type AgentMetrics = AgentMetricsSummary;
|
|
6
|
+
|
|
7
|
+
export interface AggregateMetrics extends AgentMetrics {
|
|
8
|
+
reportedAgents: number;
|
|
9
|
+
/** Rows whose duration is an observer-measured active runtime. */
|
|
10
|
+
activeDurationAgents: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AgentTreeProjection {
|
|
14
|
+
rows: AgentRef[];
|
|
15
|
+
depthById: Map<string, number>;
|
|
16
|
+
parentById: Map<string, string>;
|
|
17
|
+
lastSiblingById: Map<string, boolean>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const STATUS_ORDER: Record<AgentStatus, number> = { running: 0, idle: 1, parked: 2, aborted: 3 };
|
|
21
|
+
|
|
22
|
+
function finiteMetric(value: number | undefined): number {
|
|
23
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Exact observer usage for one roster entry. */
|
|
27
|
+
export function progressMetrics(observed: ObservableSession | undefined): AgentMetrics | undefined {
|
|
28
|
+
const progress = observed?.progress;
|
|
29
|
+
if (!progress) return undefined;
|
|
30
|
+
const { tokens, requests, toolCount: tools, cost, durationMs } = progress;
|
|
31
|
+
if (
|
|
32
|
+
typeof tokens !== "number" ||
|
|
33
|
+
!Number.isFinite(tokens) ||
|
|
34
|
+
typeof requests !== "number" ||
|
|
35
|
+
!Number.isFinite(requests) ||
|
|
36
|
+
typeof tools !== "number" ||
|
|
37
|
+
!Number.isFinite(tools) ||
|
|
38
|
+
typeof cost !== "number" ||
|
|
39
|
+
!Number.isFinite(cost) ||
|
|
40
|
+
typeof durationMs !== "number" ||
|
|
41
|
+
!Number.isFinite(durationMs)
|
|
42
|
+
) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
tokens,
|
|
47
|
+
requests,
|
|
48
|
+
tools,
|
|
49
|
+
cost,
|
|
50
|
+
durationMs,
|
|
51
|
+
durationKind: "active",
|
|
52
|
+
contextTokens:
|
|
53
|
+
typeof progress.contextTokens === "number" && Number.isFinite(progress.contextTokens)
|
|
54
|
+
? progress.contextTokens
|
|
55
|
+
: undefined,
|
|
56
|
+
contextWindow:
|
|
57
|
+
typeof progress.contextWindow === "number" && Number.isFinite(progress.contextWindow)
|
|
58
|
+
? progress.contextWindow
|
|
59
|
+
: undefined,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Read direct assistant usage from a live session. SessionStats also includes
|
|
65
|
+
* usage embedded in completed `task` tool results, so using it for a parent
|
|
66
|
+
* row would double-count child rows in the aggregate.
|
|
67
|
+
*/
|
|
68
|
+
export function readSessionMetrics(session: NonNullable<AgentRef["session"]>): AgentMetrics | undefined {
|
|
69
|
+
try {
|
|
70
|
+
const stats = session.getSessionStats();
|
|
71
|
+
const messages = session.agent?.state?.messages;
|
|
72
|
+
if (!Array.isArray(messages)) {
|
|
73
|
+
return {
|
|
74
|
+
tokens: stats.tokens.input + stats.tokens.output + stats.tokens.cacheWrite,
|
|
75
|
+
requests: stats.assistantMessages,
|
|
76
|
+
tools: stats.toolCalls,
|
|
77
|
+
cost: stats.cost,
|
|
78
|
+
durationMs: 0,
|
|
79
|
+
durationKind: "unknown",
|
|
80
|
+
contextTokens: stats.contextUsage?.tokens,
|
|
81
|
+
contextWindow: stats.contextUsage?.contextWindow,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let tokens = 0;
|
|
86
|
+
let requests = 0;
|
|
87
|
+
let tools = 0;
|
|
88
|
+
let cost = 0;
|
|
89
|
+
for (const message of messages) {
|
|
90
|
+
if (message.role !== "assistant") continue;
|
|
91
|
+
requests++;
|
|
92
|
+
tokens += message.usage.input + message.usage.output + message.usage.cacheWrite;
|
|
93
|
+
tools += message.content.filter(content => content.type === "toolCall").length;
|
|
94
|
+
cost += message.usage.cost.total;
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
tokens,
|
|
98
|
+
requests,
|
|
99
|
+
tools,
|
|
100
|
+
cost,
|
|
101
|
+
durationMs: 0,
|
|
102
|
+
durationKind: "unknown",
|
|
103
|
+
contextTokens: stats.contextUsage?.tokens,
|
|
104
|
+
contextWindow: stats.contextUsage?.contextWindow,
|
|
105
|
+
};
|
|
106
|
+
} catch {
|
|
107
|
+
// Render-only doubles and sessions being torn down may not expose a
|
|
108
|
+
// complete statistics host. Missing metrics are preferable to a broken hub.
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function aggregateMetrics(args: {
|
|
114
|
+
rows: readonly AgentRef[];
|
|
115
|
+
observedById: ReadonlyMap<string, ObservableSession>;
|
|
116
|
+
metricsFor: (ref: AgentRef, observed: ObservableSession | undefined) => AgentMetrics | undefined;
|
|
117
|
+
fallbackStatsSession: (
|
|
118
|
+
ref: AgentRef,
|
|
119
|
+
observed: ObservableSession | undefined,
|
|
120
|
+
) => NonNullable<AgentRef["session"]> | undefined;
|
|
121
|
+
sessionMetrics: WeakMap<object, { metrics: AgentMetrics | undefined }>;
|
|
122
|
+
refreshFallback: boolean;
|
|
123
|
+
}): { metrics: AggregateMetrics; hasFallbackLiveSessions: boolean } {
|
|
124
|
+
const total: AggregateMetrics = {
|
|
125
|
+
tokens: 0,
|
|
126
|
+
requests: 0,
|
|
127
|
+
tools: 0,
|
|
128
|
+
cost: 0,
|
|
129
|
+
durationMs: 0,
|
|
130
|
+
durationKind: "active",
|
|
131
|
+
reportedAgents: 0,
|
|
132
|
+
activeDurationAgents: 0,
|
|
133
|
+
};
|
|
134
|
+
let hasFallbackLiveSessions = false;
|
|
135
|
+
const countedFallbackSessions = new Set<NonNullable<AgentRef["session"]>>();
|
|
136
|
+
for (const ref of args.rows) {
|
|
137
|
+
const observed = args.observedById.get(ref.id);
|
|
138
|
+
const fallbackSession = args.fallbackStatsSession(ref, observed);
|
|
139
|
+
if (fallbackSession) {
|
|
140
|
+
hasFallbackLiveSessions = true;
|
|
141
|
+
if (args.refreshFallback || !args.sessionMetrics.has(fallbackSession)) {
|
|
142
|
+
args.sessionMetrics.set(fallbackSession, { metrics: readSessionMetrics(fallbackSession) });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const metrics = args.metricsFor(ref, observed);
|
|
146
|
+
if (!metrics || (fallbackSession && countedFallbackSessions.has(fallbackSession))) continue;
|
|
147
|
+
if (fallbackSession) countedFallbackSessions.add(fallbackSession);
|
|
148
|
+
total.reportedAgents++;
|
|
149
|
+
total.tokens += finiteMetric(metrics.tokens);
|
|
150
|
+
total.requests += finiteMetric(metrics.requests);
|
|
151
|
+
total.tools += finiteMetric(metrics.tools);
|
|
152
|
+
total.cost += finiteMetric(metrics.cost);
|
|
153
|
+
if (metrics.durationKind === "active") {
|
|
154
|
+
total.durationMs += finiteMetric(metrics.durationMs);
|
|
155
|
+
total.activeDurationAgents++;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { metrics: total, hasFallbackLiveSessions };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Parent-before-child projection preserving the roster's stable sibling order. */
|
|
162
|
+
export function projectAgentTree(refs: readonly AgentRef[]): AgentTreeProjection {
|
|
163
|
+
const ids = new Set<string>();
|
|
164
|
+
const operationalIndex = new Map<string, number>();
|
|
165
|
+
for (let i = 0; i < refs.length; i++) {
|
|
166
|
+
ids.add(refs[i].id);
|
|
167
|
+
operationalIndex.set(refs[i].id, i);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const parentById = new Map<string, string>();
|
|
171
|
+
const children = new Map<string, AgentRef[]>();
|
|
172
|
+
for (const ref of refs) {
|
|
173
|
+
const parent =
|
|
174
|
+
ref.parentId && ref.parentId !== MAIN_AGENT_ID && ids.has(ref.parentId) ? ref.parentId : MAIN_AGENT_ID;
|
|
175
|
+
parentById.set(ref.id, parent);
|
|
176
|
+
const siblings = children.get(parent);
|
|
177
|
+
if (siblings) siblings.push(ref);
|
|
178
|
+
else children.set(parent, [ref]);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// A tree group occupies the position of its earliest operational row.
|
|
182
|
+
// Compute subtree minima iteratively so pathological lineage depth remains stack-safe.
|
|
183
|
+
const subtreeOrder = new Map<string, number>();
|
|
184
|
+
const visiting = new Set<string>();
|
|
185
|
+
const ranked = new Set<string>();
|
|
186
|
+
for (const start of refs) {
|
|
187
|
+
if (ranked.has(start.id)) continue;
|
|
188
|
+
const stack: Array<{ ref: AgentRef; expanded: boolean }> = [{ ref: start, expanded: false }];
|
|
189
|
+
while (stack.length > 0) {
|
|
190
|
+
const current = stack.pop();
|
|
191
|
+
if (!current) continue;
|
|
192
|
+
if (current.expanded) {
|
|
193
|
+
let order = operationalIndex.get(current.ref.id) ?? Number.MAX_SAFE_INTEGER;
|
|
194
|
+
for (const child of children.get(current.ref.id) ?? []) {
|
|
195
|
+
order = Math.min(order, subtreeOrder.get(child.id) ?? Number.MAX_SAFE_INTEGER);
|
|
196
|
+
}
|
|
197
|
+
subtreeOrder.set(current.ref.id, order);
|
|
198
|
+
visiting.delete(current.ref.id);
|
|
199
|
+
ranked.add(current.ref.id);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (ranked.has(current.ref.id) || visiting.has(current.ref.id)) continue;
|
|
203
|
+
visiting.add(current.ref.id);
|
|
204
|
+
stack.push({ ref: current.ref, expanded: true });
|
|
205
|
+
const descendants = children.get(current.ref.id);
|
|
206
|
+
if (!descendants) continue;
|
|
207
|
+
for (let i = descendants.length - 1; i >= 0; i--) {
|
|
208
|
+
const child = descendants[i];
|
|
209
|
+
if (!ranked.has(child.id) && !visiting.has(child.id)) stack.push({ ref: child, expanded: false });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const siblings of children.values()) {
|
|
214
|
+
siblings.sort(
|
|
215
|
+
(a, b) =>
|
|
216
|
+
(subtreeOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (subtreeOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER) ||
|
|
217
|
+
(operationalIndex.get(a.id) ?? Number.MAX_SAFE_INTEGER) -
|
|
218
|
+
(operationalIndex.get(b.id) ?? Number.MAX_SAFE_INTEGER),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const lastSiblingById = new Map<string, boolean>();
|
|
223
|
+
for (const siblings of children.values()) {
|
|
224
|
+
for (let i = 0; i < siblings.length; i++) lastSiblingById.set(siblings[i].id, i === siblings.length - 1);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const rows: AgentRef[] = [];
|
|
228
|
+
const visited = new Set<string>();
|
|
229
|
+
const depthById = new Map<string, number>();
|
|
230
|
+
const visit = (root: AgentRef, rootDepth: number): void => {
|
|
231
|
+
const stack: Array<{ ref: AgentRef; depth: number }> = [{ ref: root, depth: rootDepth }];
|
|
232
|
+
while (stack.length > 0) {
|
|
233
|
+
const current = stack.pop();
|
|
234
|
+
if (!current || visited.has(current.ref.id)) continue;
|
|
235
|
+
visited.add(current.ref.id);
|
|
236
|
+
depthById.set(current.ref.id, current.depth);
|
|
237
|
+
rows.push(current.ref);
|
|
238
|
+
const descendants = children.get(current.ref.id);
|
|
239
|
+
if (!descendants) continue;
|
|
240
|
+
for (let i = descendants.length - 1; i >= 0; i--)
|
|
241
|
+
stack.push({ ref: descendants[i], depth: current.depth + 1 });
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
for (const root of children.get(MAIN_AGENT_ID) ?? []) visit(root, 0);
|
|
245
|
+
// Corrupt persisted parent cycles remain visible as roots instead of disappearing.
|
|
246
|
+
for (const ref of refs) visit(ref, 0);
|
|
247
|
+
return { rows, depthById, parentById, lastSiblingById };
|
|
248
|
+
}
|