@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
|
@@ -1,26 +1,281 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { ADVISOR_TRANSCRIPT_FILENAME, isAdvisorTranscriptName } from "../advisor/transcript-recorder";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveExplicitModelRole } from "../config/model-resolver";
|
|
5
|
+
import { EPHEMERAL_MODEL_CHANGE_ROLE } from "../session/session-entries";
|
|
6
|
+
import { visitEntriesFromFileStream } from "../session/session-loader";
|
|
7
|
+
import { loadBundledAgents } from "../task/agents";
|
|
8
|
+
import { isReadOnlyAgent } from "../task/read-only-policy";
|
|
5
9
|
import { persistedVibeChildIds } from "../vibe/runtime";
|
|
6
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type AgentHistorySummary,
|
|
12
|
+
type AgentMetricsSummary,
|
|
13
|
+
type AgentRegistry,
|
|
14
|
+
getAgentTombstonePath,
|
|
15
|
+
MAIN_AGENT_ID,
|
|
16
|
+
} from "./agent-registry";
|
|
17
|
+
|
|
18
|
+
/** Maximum prefix entries inspected for task metadata. */
|
|
19
|
+
const MAX_METADATA_LINES = 64;
|
|
20
|
+
|
|
21
|
+
interface PersistedAgentMetadata {
|
|
22
|
+
activity?: string;
|
|
23
|
+
createdAt?: number;
|
|
24
|
+
lastActivity?: number;
|
|
25
|
+
history?: AgentHistorySummary;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface PersistedTranscript {
|
|
29
|
+
id: string;
|
|
30
|
+
sessionFile: string;
|
|
31
|
+
createdAt?: number;
|
|
32
|
+
lastActivity?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function recordOf(value: unknown): Record<string, unknown> | undefined {
|
|
36
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
37
|
+
? (value as Record<string, unknown>)
|
|
38
|
+
: undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function timestampOf(value: unknown): number | undefined {
|
|
42
|
+
if (typeof value !== "string") return undefined;
|
|
43
|
+
const timestamp = Date.parse(value);
|
|
44
|
+
return Number.isFinite(timestamp) ? timestamp : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function summarizePersistedTask(task: string): string | undefined {
|
|
48
|
+
const withoutPreamble = task.replace(/^Complete the assignment below,\s*thoroughly:\s*/i, "");
|
|
49
|
+
const lines = withoutPreamble.split(/\r?\n/);
|
|
50
|
+
const targetIndex = lines.findIndex(line => line.trim().toLowerCase() === "# target");
|
|
51
|
+
const targetLines: string[] = [];
|
|
52
|
+
if (targetIndex >= 0) {
|
|
53
|
+
for (const line of lines.slice(targetIndex + 1)) {
|
|
54
|
+
if (line.trimStart().startsWith("# ")) break;
|
|
55
|
+
targetLines.push(line);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const summary = (targetLines.length > 0 ? targetLines : lines).join(" ").replace(/\s+/g, " ").trim();
|
|
59
|
+
return summary ? summary.slice(0, 1_000) : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function finiteNumber(value: unknown): number {
|
|
63
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function inferBundledAgent(systemPrompt: string): { agent?: string; modelRole?: string; readOnly?: boolean } {
|
|
67
|
+
const matches = loadBundledAgents().filter(agent => {
|
|
68
|
+
const rolePrompt = agent.systemPrompt.trim();
|
|
69
|
+
return rolePrompt.length > 0 && systemPrompt.includes(rolePrompt);
|
|
70
|
+
});
|
|
71
|
+
// `task` and `sonic` intentionally share a prompt body. Ambiguous historical
|
|
72
|
+
// prompts stay unlabelled rather than inventing provenance.
|
|
73
|
+
if (matches.length !== 1) return {};
|
|
74
|
+
const [agent] = matches;
|
|
75
|
+
return {
|
|
76
|
+
agent: agent.name,
|
|
77
|
+
modelRole: resolveExplicitModelRole(agent.model),
|
|
78
|
+
readOnly: isReadOnlyAgent(agent),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function usageTokens(usage: Record<string, unknown>): number {
|
|
83
|
+
const computed = finiteNumber(usage.input) + finiteNumber(usage.output) + finiteNumber(usage.cacheWrite);
|
|
84
|
+
return computed > 0 ? computed : finiteNumber(usage.totalTokens);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface AssistantMetrics {
|
|
88
|
+
tokens: number;
|
|
89
|
+
tools: number;
|
|
90
|
+
cost: number;
|
|
91
|
+
contextTokens?: number;
|
|
92
|
+
resolvedModel?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function assistantMetrics(message: Record<string, unknown>): AssistantMetrics {
|
|
96
|
+
const usage = recordOf(message.usage) ?? {};
|
|
97
|
+
const cost = recordOf(usage.cost);
|
|
98
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
99
|
+
const provider = typeof message.provider === "string" ? message.provider : undefined;
|
|
100
|
+
const model = typeof message.model === "string" ? message.model : undefined;
|
|
101
|
+
return {
|
|
102
|
+
tokens: usageTokens(usage),
|
|
103
|
+
tools: content.filter(part => recordOf(part)?.type === "toolCall").length,
|
|
104
|
+
cost: finiteNumber(cost?.total),
|
|
105
|
+
contextTokens: finiteNumber(usage.totalTokens) || undefined,
|
|
106
|
+
resolvedModel: provider && model ? `${provider}/${model}` : undefined,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function readPersistedAgentHistory(
|
|
111
|
+
transcript: PersistedTranscript,
|
|
112
|
+
shouldContinue: () => boolean,
|
|
113
|
+
): Promise<AgentHistorySummary> {
|
|
114
|
+
const parents = new Map<string, string | undefined>();
|
|
115
|
+
const assistantById = new Map<string, AssistantMetrics>();
|
|
116
|
+
const modelChangeById = new Map<string, { model: string; role?: string; resolvedModelIsFallback: boolean }>();
|
|
117
|
+
let leafId: string | undefined;
|
|
118
|
+
let leafTimestamp: number | undefined;
|
|
119
|
+
try {
|
|
120
|
+
await visitEntriesFromFileStream(
|
|
121
|
+
transcript.sessionFile,
|
|
122
|
+
entry => {
|
|
123
|
+
const record = recordOf(entry);
|
|
124
|
+
if (!record) return;
|
|
125
|
+
const id = typeof record.id === "string" ? record.id : undefined;
|
|
126
|
+
if (!id) return;
|
|
127
|
+
const parentId = typeof record.parentId === "string" ? record.parentId : undefined;
|
|
128
|
+
parents.set(id, parentId);
|
|
129
|
+
leafId = id;
|
|
130
|
+
const parsedTimestamp = timestampOf(record.timestamp);
|
|
131
|
+
if (parsedTimestamp !== undefined) leafTimestamp = parsedTimestamp;
|
|
132
|
+
if (record.type === "model_change" && typeof record.model === "string") {
|
|
133
|
+
modelChangeById.set(id, {
|
|
134
|
+
model: record.model,
|
|
135
|
+
role: typeof record.role === "string" ? record.role : undefined,
|
|
136
|
+
resolvedModelIsFallback: record.resolvedModelIsFallback === true,
|
|
137
|
+
});
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (record.type !== "message") return;
|
|
141
|
+
const message = recordOf(record.message);
|
|
142
|
+
if (message?.role === "assistant") assistantById.set(id, assistantMetrics(message));
|
|
143
|
+
},
|
|
144
|
+
{ shouldContinue },
|
|
145
|
+
);
|
|
146
|
+
} catch {
|
|
147
|
+
return {};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const metrics: AgentMetricsSummary = {
|
|
151
|
+
tokens: 0,
|
|
152
|
+
requests: 0,
|
|
153
|
+
tools: 0,
|
|
154
|
+
cost: 0,
|
|
155
|
+
durationMs: Math.max(
|
|
156
|
+
0,
|
|
157
|
+
(leafTimestamp ?? transcript.lastActivity ?? transcript.createdAt ?? 0) -
|
|
158
|
+
(transcript.createdAt ?? leafTimestamp ?? 0),
|
|
159
|
+
),
|
|
160
|
+
durationKind: "span",
|
|
161
|
+
};
|
|
162
|
+
let resolvedModel: string | undefined;
|
|
163
|
+
let resolvedModelIsFallback: boolean | undefined;
|
|
164
|
+
let modelRole: string | undefined;
|
|
165
|
+
let contextTokens: number | undefined;
|
|
166
|
+
let modelChangeFound = false;
|
|
167
|
+
const visited = new Set<string>();
|
|
168
|
+
for (let id = leafId; id && !visited.has(id); id = parents.get(id)) {
|
|
169
|
+
visited.add(id);
|
|
170
|
+
const modelChange = modelChangeById.get(id);
|
|
171
|
+
if (modelChange && !modelChangeFound) {
|
|
172
|
+
modelChangeFound = true;
|
|
173
|
+
resolvedModel = modelChange.model;
|
|
174
|
+
resolvedModelIsFallback = modelChange.resolvedModelIsFallback;
|
|
175
|
+
if (modelChange.role && modelChange.role !== EPHEMERAL_MODEL_CHANGE_ROLE) {
|
|
176
|
+
modelRole = modelChange.role;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const assistant = assistantById.get(id);
|
|
180
|
+
if (!assistant) continue;
|
|
181
|
+
if (!modelChangeFound && resolvedModel === undefined && assistant.resolvedModel) {
|
|
182
|
+
resolvedModel = assistant.resolvedModel;
|
|
183
|
+
}
|
|
184
|
+
metrics.requests++;
|
|
185
|
+
metrics.tokens += assistant.tokens;
|
|
186
|
+
metrics.tools += assistant.tools;
|
|
187
|
+
metrics.cost += assistant.cost;
|
|
188
|
+
contextTokens ??= assistant.contextTokens;
|
|
189
|
+
}
|
|
190
|
+
if (contextTokens !== undefined) metrics.contextTokens = contextTokens;
|
|
191
|
+
return {
|
|
192
|
+
...(metrics.requests > 0 ? { metrics } : {}),
|
|
193
|
+
...(resolvedModel ? { resolvedModel, resolvedModelIsFallback } : {}),
|
|
194
|
+
...(modelRole ? { modelRole } : {}),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
7
197
|
|
|
8
198
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
199
|
+
* Read only the small session prefix needed by the Hub. A subagent's first
|
|
200
|
+
* `session_init` is written before its conversation, so this never walks a
|
|
201
|
+
* multi-megabyte historical transcript just to populate one roster row.
|
|
12
202
|
*/
|
|
13
|
-
async function
|
|
14
|
-
|
|
203
|
+
async function readPersistedAgentMetadata(sessionFile: string): Promise<PersistedAgentMetadata> {
|
|
204
|
+
const stat = fs.promises.stat(sessionFile).catch(() => undefined);
|
|
205
|
+
const artifactBase = sessionFile.slice(0, -".jsonl".length);
|
|
206
|
+
const outputPath = `${artifactBase}.md`;
|
|
207
|
+
const patchPath = `${artifactBase}.patch`;
|
|
208
|
+
const artifactFiles = Promise.all([Bun.file(outputPath).exists(), Bun.file(patchPath).exists()]);
|
|
209
|
+
let createdAt: number | undefined;
|
|
210
|
+
let activity: string | undefined;
|
|
211
|
+
let history: AgentHistorySummary = {};
|
|
15
212
|
try {
|
|
16
|
-
|
|
213
|
+
await visitEntriesFromFileStream(
|
|
214
|
+
sessionFile,
|
|
215
|
+
entry => {
|
|
216
|
+
const record = recordOf(entry);
|
|
217
|
+
if (!record) return;
|
|
218
|
+
if (record.type === "session") {
|
|
219
|
+
createdAt ??= timestampOf(record.timestamp);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (record.type === "model_change") {
|
|
223
|
+
if (typeof record.model === "string") history.resolvedModel = record.model;
|
|
224
|
+
if (typeof record.role === "string" && record.role !== EPHEMERAL_MODEL_CHANGE_ROLE) {
|
|
225
|
+
history.modelRole = record.role;
|
|
226
|
+
}
|
|
227
|
+
if (typeof record.resolvedModelIsFallback === "boolean") {
|
|
228
|
+
history.resolvedModelIsFallback = record.resolvedModelIsFallback;
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (record.type !== "session_init") return;
|
|
233
|
+
createdAt ??= timestampOf(record.timestamp);
|
|
234
|
+
if (typeof record.task === "string") activity = summarizePersistedTask(record.task);
|
|
235
|
+
const inferred = typeof record.systemPrompt === "string" ? inferBundledAgent(record.systemPrompt) : {};
|
|
236
|
+
history = {
|
|
237
|
+
...history,
|
|
238
|
+
...inferred,
|
|
239
|
+
agent: typeof record.agent === "string" ? record.agent : inferred.agent,
|
|
240
|
+
modelRole:
|
|
241
|
+
typeof record.modelRole === "string" ? record.modelRole : (history.modelRole ?? inferred.modelRole),
|
|
242
|
+
resolvedModel: typeof record.resolvedModel === "string" ? record.resolvedModel : history.resolvedModel,
|
|
243
|
+
readOnly: typeof record.readOnly === "boolean" ? record.readOnly : inferred.readOnly,
|
|
244
|
+
};
|
|
245
|
+
return false;
|
|
246
|
+
},
|
|
247
|
+
{ maxRecords: MAX_METADATA_LINES },
|
|
248
|
+
);
|
|
17
249
|
} catch {
|
|
18
|
-
|
|
250
|
+
// A readable transcript is still useful even when its optional metadata
|
|
251
|
+
// prefix is malformed.
|
|
19
252
|
}
|
|
253
|
+
const [file, [hasOutput, hasPatch]] = await Promise.all([stat, artifactFiles]);
|
|
254
|
+
return {
|
|
255
|
+
activity,
|
|
256
|
+
createdAt: createdAt ?? file?.birthtimeMs,
|
|
257
|
+
lastActivity: file?.mtimeMs,
|
|
258
|
+
history: {
|
|
259
|
+
...history,
|
|
260
|
+
...(hasOutput ? { outputPath } : {}),
|
|
261
|
+
...(hasPatch ? { patchPath } : {}),
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function readPersistedVibeChildIds(sessionFile: string, shouldContinue: () => boolean): Promise<Set<string>> {
|
|
267
|
+
const ids = new Set<string>();
|
|
20
268
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
269
|
+
await visitEntriesFromFileStream(
|
|
270
|
+
sessionFile,
|
|
271
|
+
entry => {
|
|
272
|
+
for (const id of persistedVibeChildIds([entry])) ids.add(id);
|
|
273
|
+
},
|
|
274
|
+
{ shouldContinue },
|
|
275
|
+
);
|
|
276
|
+
return ids;
|
|
277
|
+
} catch {
|
|
278
|
+
return new Set();
|
|
24
279
|
}
|
|
25
280
|
}
|
|
26
281
|
|
|
@@ -28,11 +283,30 @@ async function readPersistedVibeChildIds(sessionFile: string): Promise<Set<strin
|
|
|
28
283
|
export async function registerPersistedSubagents(
|
|
29
284
|
registry: AgentRegistry,
|
|
30
285
|
sessionFile: string | null | undefined,
|
|
286
|
+
options: { shouldContinue?: () => boolean } = {},
|
|
31
287
|
): Promise<void> {
|
|
32
288
|
if (!sessionFile?.endsWith(".jsonl")) return;
|
|
33
|
-
const
|
|
289
|
+
const shouldContinue = options.shouldContinue ?? (() => true);
|
|
290
|
+
if (!shouldContinue()) return;
|
|
291
|
+
const vibeOwnedIds = await readPersistedVibeChildIds(sessionFile, shouldContinue);
|
|
292
|
+
if (!shouldContinue()) return;
|
|
34
293
|
const root = sessionFile.slice(0, -6);
|
|
35
|
-
|
|
294
|
+
const transcripts: PersistedTranscript[] = [];
|
|
295
|
+
await registerPersistedSubagentsFromDir(registry, root, undefined, vibeOwnedIds, transcripts, shouldContinue);
|
|
296
|
+
if (!shouldContinue()) return;
|
|
297
|
+
let nextTranscript = 0;
|
|
298
|
+
const workers = Array.from({ length: Math.min(4, transcripts.length) }, async () => {
|
|
299
|
+
for (;;) {
|
|
300
|
+
if (!shouldContinue()) return;
|
|
301
|
+
const index = nextTranscript++;
|
|
302
|
+
const transcript = transcripts[index];
|
|
303
|
+
if (!transcript) return;
|
|
304
|
+
const history = await readPersistedAgentHistory(transcript, shouldContinue);
|
|
305
|
+
if (!shouldContinue()) return;
|
|
306
|
+
registry.setHistory(transcript.id, history, transcript.sessionFile);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
await Promise.all(workers);
|
|
36
310
|
}
|
|
37
311
|
|
|
38
312
|
async function registerPersistedSubagentsFromDir(
|
|
@@ -40,14 +314,25 @@ async function registerPersistedSubagentsFromDir(
|
|
|
40
314
|
dir: string,
|
|
41
315
|
parentId: string | undefined,
|
|
42
316
|
vibeOwnedIds: ReadonlySet<string>,
|
|
317
|
+
transcripts: PersistedTranscript[],
|
|
318
|
+
shouldContinue: () => boolean,
|
|
43
319
|
): Promise<void> {
|
|
320
|
+
if (!shouldContinue()) return;
|
|
44
321
|
let entries: fs.Dirent[];
|
|
45
322
|
try {
|
|
46
323
|
entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
47
324
|
} catch {
|
|
48
325
|
return;
|
|
49
326
|
}
|
|
327
|
+
if (!shouldContinue()) return;
|
|
328
|
+
let entriesSinceYield = 0;
|
|
50
329
|
for (const entry of entries) {
|
|
330
|
+
if (!shouldContinue()) return;
|
|
331
|
+
if (++entriesSinceYield >= 16) {
|
|
332
|
+
entriesSinceYield = 0;
|
|
333
|
+
await Bun.sleep(0);
|
|
334
|
+
}
|
|
335
|
+
if (!shouldContinue()) return;
|
|
51
336
|
if (!entry.isFile() || !entry.name.endsWith(".jsonl") || entry.name.includes(".bak")) continue;
|
|
52
337
|
const sessionFile = path.join(dir, entry.name);
|
|
53
338
|
// The advisor transcript is observability-only: register it as a non-peer
|
|
@@ -66,6 +351,8 @@ async function registerPersistedSubagentsFromDir(
|
|
|
66
351
|
// user task literally named `<owner>/advisor`): leave it, skip the advisor.
|
|
67
352
|
if (existing && existing.kind !== "advisor") continue;
|
|
68
353
|
if (existing?.sessionFile !== sessionFile) {
|
|
354
|
+
const metadata = await readPersistedAgentMetadata(sessionFile);
|
|
355
|
+
if (!shouldContinue()) return;
|
|
69
356
|
// The id is reused across `/new`; refresh it to the current session's file.
|
|
70
357
|
if (existing) registry.unregister(advisorId);
|
|
71
358
|
registry.register({
|
|
@@ -75,14 +362,34 @@ async function registerPersistedSubagentsFromDir(
|
|
|
75
362
|
parentId: owner,
|
|
76
363
|
session: null,
|
|
77
364
|
sessionFile,
|
|
365
|
+
activity: metadata.activity,
|
|
366
|
+
createdAt: metadata.createdAt,
|
|
367
|
+
lastActivity: metadata.lastActivity,
|
|
368
|
+
history: { ...metadata.history, readOnly: true },
|
|
78
369
|
status: "parked",
|
|
79
370
|
});
|
|
371
|
+
transcripts.push({
|
|
372
|
+
id: advisorId,
|
|
373
|
+
sessionFile,
|
|
374
|
+
createdAt: metadata.createdAt,
|
|
375
|
+
lastActivity: metadata.lastActivity,
|
|
376
|
+
});
|
|
80
377
|
}
|
|
81
378
|
continue;
|
|
82
379
|
}
|
|
83
380
|
const id = entry.name.slice(0, -6);
|
|
84
381
|
if (vibeOwnedIds.has(id) && registry.get(id)?.sessionFile !== sessionFile) continue;
|
|
382
|
+
let tombstoned = false;
|
|
383
|
+
try {
|
|
384
|
+
await fs.promises.access(getAgentTombstonePath(sessionFile));
|
|
385
|
+
tombstoned = true;
|
|
386
|
+
} catch (error) {
|
|
387
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") continue;
|
|
388
|
+
}
|
|
389
|
+
if (!shouldContinue()) return;
|
|
85
390
|
if (!registry.get(id)) {
|
|
391
|
+
const metadata = await readPersistedAgentMetadata(sessionFile);
|
|
392
|
+
if (!shouldContinue()) return;
|
|
86
393
|
registry.register({
|
|
87
394
|
id,
|
|
88
395
|
displayName: id,
|
|
@@ -90,9 +397,27 @@ async function registerPersistedSubagentsFromDir(
|
|
|
90
397
|
parentId: parentId ?? MAIN_AGENT_ID,
|
|
91
398
|
session: null,
|
|
92
399
|
sessionFile,
|
|
93
|
-
|
|
400
|
+
activity: metadata.activity,
|
|
401
|
+
createdAt: metadata.createdAt,
|
|
402
|
+
lastActivity: metadata.lastActivity,
|
|
403
|
+
history: metadata.history,
|
|
404
|
+
status: tombstoned ? "aborted" : "parked",
|
|
405
|
+
});
|
|
406
|
+
const ref = registry.get(id);
|
|
407
|
+
transcripts.push({
|
|
408
|
+
id,
|
|
409
|
+
sessionFile,
|
|
410
|
+
createdAt: ref?.createdAt,
|
|
411
|
+
lastActivity: ref?.lastActivity,
|
|
94
412
|
});
|
|
95
413
|
}
|
|
96
|
-
await registerPersistedSubagentsFromDir(
|
|
414
|
+
await registerPersistedSubagentsFromDir(
|
|
415
|
+
registry,
|
|
416
|
+
path.join(dir, id),
|
|
417
|
+
id,
|
|
418
|
+
vibeOwnedIds,
|
|
419
|
+
transcripts,
|
|
420
|
+
shouldContinue,
|
|
421
|
+
);
|
|
97
422
|
}
|
|
98
423
|
}
|
package/src/sdk.ts
CHANGED
|
@@ -2283,9 +2283,10 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
|
|
2283
2283
|
}
|
|
2284
2284
|
}
|
|
2285
2285
|
const usageReservePolicy = settings.get("retry.usageReservePolicy");
|
|
2286
|
+
const modelFallbackEnabled = settings.get("retry.modelFallback");
|
|
2286
2287
|
if (
|
|
2287
|
-
(hasUsageFallbackCandidate ||
|
|
2288
|
-
|
|
2288
|
+
((modelFallbackEnabled && (hasUsageFallbackCandidate || usageFallbackTriggered)) ||
|
|
2289
|
+
usageReservePolicy === "fail-closed") &&
|
|
2289
2290
|
settings.get("retry.usageAwareFallback")
|
|
2290
2291
|
) {
|
|
2291
2292
|
let usageHealth: ModelUsageHealth | undefined;
|
|
@@ -2308,8 +2309,10 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
|
|
2308
2309
|
`Usage depleted for ${primary.model.provider}/${primary.model.id}; reserve policy is fail-closed.`,
|
|
2309
2310
|
);
|
|
2310
2311
|
}
|
|
2311
|
-
|
|
2312
|
-
|
|
2312
|
+
if (modelFallbackEnabled) {
|
|
2313
|
+
usageFallbackTriggered = true;
|
|
2314
|
+
continue;
|
|
2315
|
+
}
|
|
2313
2316
|
}
|
|
2314
2317
|
if (usageHealth?.state === "reserve") {
|
|
2315
2318
|
if (usageReservePolicy === "fail-closed") {
|
|
@@ -2317,7 +2320,10 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
|
|
2317
2320
|
`Usage reserve reached for ${primary.model.provider}/${primary.model.id}; reserve policy is fail-closed.`,
|
|
2318
2321
|
);
|
|
2319
2322
|
}
|
|
2320
|
-
if (
|
|
2323
|
+
if (
|
|
2324
|
+
modelFallbackEnabled &&
|
|
2325
|
+
(usageReservePolicy === "auto" || (!options.hasUI && !options.deferUsageReserveConfirmation))
|
|
2326
|
+
) {
|
|
2321
2327
|
usageFallbackTriggered = true;
|
|
2322
2328
|
continue;
|
|
2323
2329
|
}
|
|
@@ -92,6 +92,14 @@ export interface UsageFallbackConfirmation {
|
|
|
92
92
|
remainingPercent: number | undefined;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Confirms whether a reserve-triggered model fallback may proceed.
|
|
97
|
+
*
|
|
98
|
+
* Interactive callers use the confirmation details to present the pending
|
|
99
|
+
* route change; aborting `signal` cancels that pending confirmation.
|
|
100
|
+
*/
|
|
101
|
+
export type UsageFallbackConfirmer = (confirmation: UsageFallbackConfirmation, signal: AbortSignal) => Promise<boolean>;
|
|
102
|
+
|
|
95
103
|
/** Identifies a retry fallback chain already entered during startup model resolution. */
|
|
96
104
|
export interface InitialRetryFallbackState {
|
|
97
105
|
/** Role whose configured primary was unavailable. */
|