@oh-my-pi/pi-coding-agent 16.0.1 → 16.0.3
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 +70 -0
- package/README.md +0 -1
- package/dist/cli.js +316 -371
- package/dist/types/advisor/advise-tool.d.ts +30 -1
- package/dist/types/commands/install.d.ts +1 -1
- package/dist/types/config/model-resolver.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +0 -10
- package/dist/types/eval/js/shared/runtime.d.ts +1 -0
- package/dist/types/eval/js/worker-core.d.ts +1 -0
- package/dist/types/exec/non-interactive-env.d.ts +2 -0
- package/dist/types/extensibility/extensions/loader.d.ts +2 -2
- package/dist/types/goals/runtime.d.ts +0 -1
- package/dist/types/mcp/tool-bridge.d.ts +3 -0
- package/dist/types/modes/components/custom-editor.d.ts +14 -4
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +3 -2
- package/dist/types/modes/theme/mermaid-cache.d.ts +18 -1
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +16 -1
- package/dist/types/sdk.d.ts +8 -0
- package/dist/types/session/agent-session.d.ts +20 -8
- package/dist/types/session/messages.d.ts +3 -0
- package/dist/types/session/session-dump-format.d.ts +8 -2
- package/dist/types/session/session-entries.d.ts +4 -0
- package/dist/types/session/session-history-format.d.ts +2 -0
- package/dist/types/session/session-manager.d.ts +22 -0
- package/dist/types/stt/downloader.d.ts +5 -5
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/persisted-revive.d.ts +36 -0
- package/dist/types/tiny/models.d.ts +8 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/index.d.ts +0 -1
- package/dist/types/utils/markit.d.ts +8 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +156 -12
- package/src/advisor/advise-tool.ts +48 -6
- package/src/advisor/runtime.ts +10 -3
- package/src/auto-thinking/classifier.ts +12 -3
- package/src/cli/args.ts +1 -0
- package/src/cli.ts +2 -2
- package/src/commands/install.ts +3 -3
- package/src/config/model-resolver.ts +63 -12
- package/src/config/settings-schema.ts +0 -11
- package/src/discovery/github.ts +89 -1
- package/src/eval/agent-bridge.ts +2 -0
- package/src/eval/js/context-manager.ts +2 -1
- package/src/eval/js/shared/runtime.ts +189 -15
- package/src/eval/js/worker-core.ts +19 -0
- package/src/exec/bash-executor.ts +2 -2
- package/src/exec/non-interactive-env.ts +71 -0
- package/src/export/html/index.ts +1 -1
- package/src/export/html/tool-views.generated.js +34 -35
- package/src/extensibility/extensions/loader.ts +21 -9
- package/src/extensibility/extensions/runner.ts +17 -1
- package/src/extensibility/plugins/loader.ts +154 -21
- package/src/extensibility/plugins/manager.ts +40 -33
- package/src/goals/runtime.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +9 -11
- package/src/main.ts +20 -0
- package/src/mcp/render.ts +11 -1
- package/src/mcp/tool-bridge.ts +3 -0
- package/src/modes/components/custom-editor.test.ts +63 -18
- package/src/modes/components/custom-editor.ts +63 -15
- package/src/modes/controllers/command-controller.ts +2 -2
- package/src/modes/controllers/input-controller.ts +15 -9
- package/src/modes/controllers/selector-controller.ts +13 -8
- package/src/modes/controllers/tan-command-controller.ts +1 -0
- package/src/modes/interactive-mode.ts +4 -2
- package/src/modes/setup-wizard/wizard-overlay.ts +26 -4
- package/src/modes/theme/mermaid-cache.ts +74 -11
- package/src/modes/theme/theme.ts +14 -1
- package/src/modes/types.ts +1 -1
- package/src/prompts/system/system-prompt.md +2 -1
- package/src/registry/agent-lifecycle.ts +60 -8
- package/src/sdk.ts +20 -26
- package/src/session/agent-session.ts +381 -110
- package/src/session/artifacts.ts +19 -1
- package/src/session/messages.ts +1 -1
- package/src/session/session-dump-format.ts +167 -23
- package/src/session/session-entries.ts +4 -0
- package/src/session/session-history-format.ts +37 -3
- package/src/session/session-manager.ts +94 -4
- package/src/slash-commands/builtin-registry.ts +4 -7
- package/src/stt/asr-client.ts +6 -0
- package/src/stt/downloader.ts +13 -6
- package/src/stt/stt-controller.ts +52 -11
- package/src/system-prompt.ts +7 -1
- package/src/task/executor.ts +118 -6
- package/src/task/index.ts +2 -2
- package/src/task/persisted-revive.ts +128 -0
- package/src/tiny/models.ts +10 -0
- package/src/tiny/worker.ts +4 -3
- package/src/tools/builtin-names.ts +0 -1
- package/src/tools/index.ts +0 -4
- package/src/tools/output-meta.ts +17 -3
- package/src/utils/lang-from-path.ts +5 -0
- package/src/utils/markit.ts +24 -1
- package/src/utils/title-generator.ts +4 -4
- package/dist/types/tools/render-mermaid.d.ts +0 -38
- package/src/prompts/tools/render-mermaid.md +0 -9
- package/src/tools/render-mermaid.ts +0 -69
|
@@ -4,10 +4,11 @@ import * as path from "node:path";
|
|
|
4
4
|
import { logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { settings } from "../config/settings";
|
|
6
6
|
import { type SttStreamHandle, sttClient } from "./asr-client";
|
|
7
|
-
import {
|
|
7
|
+
import { downloadSttModel, isSttModelCached } from "./downloader";
|
|
8
8
|
import { resolveSttModelSpec } from "./models";
|
|
9
9
|
import {
|
|
10
10
|
detectRecorder,
|
|
11
|
+
ensureRecorder,
|
|
11
12
|
type RecordingHandle,
|
|
12
13
|
type StreamingRecordingHandle,
|
|
13
14
|
startRecording,
|
|
@@ -36,7 +37,7 @@ interface Editor {
|
|
|
36
37
|
|
|
37
38
|
export class STTController {
|
|
38
39
|
#state: SttState = "idle";
|
|
39
|
-
#
|
|
40
|
+
#resolvedModelKey: string | null = null;
|
|
40
41
|
#toggling = false;
|
|
41
42
|
#stopAfterStart = false;
|
|
42
43
|
#disposed = false;
|
|
@@ -92,15 +93,39 @@ export class STTController {
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
async #ensureDeps(options: ToggleOptions): Promise<boolean> {
|
|
95
|
-
|
|
96
|
+
const modelKey = resolveSttModelSpec(settings.get("stt.modelName") as string | undefined).key;
|
|
97
|
+
// Keyed on the model rather than a one-shot flag: switching stt.modelName
|
|
98
|
+
// mid-session must re-run preflight so an uncached new tier downloads here
|
|
99
|
+
// (with progress) instead of blocking silently at stop.
|
|
100
|
+
if (this.#resolvedModelKey === modelKey) return true;
|
|
96
101
|
try {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
// Only clear the status line if we actually wrote to it: the cached
|
|
103
|
+
// fast path (recorder on PATH, model present) emits nothing, so an
|
|
104
|
+
// unconditional clear would be a stray write.
|
|
105
|
+
let wroteStatus = false;
|
|
106
|
+
const status = (msg: string): void => {
|
|
107
|
+
wroteStatus = true;
|
|
108
|
+
options.showStatus(msg);
|
|
109
|
+
};
|
|
110
|
+
// A recorder is required to capture audio; startRecording /
|
|
111
|
+
// startStreamingRecording only *detect* a recorder and throw when none
|
|
112
|
+
// exists, so provision one here. Instant when sox/ffmpeg/arecord is on
|
|
113
|
+
// PATH — only a first-run static-ffmpeg download actually blocks.
|
|
114
|
+
await ensureRecorder(p => status(p.stage + (p.percent != null ? ` (${p.percent}%)` : "")));
|
|
115
|
+
// Loading the multi-hundred-MB speech model into the worker is what made
|
|
116
|
+
// the old "Checking STT dependencies…" step slow. Don't pay it before
|
|
117
|
+
// recording: when the weights are already cached, start now and warm the
|
|
118
|
+
// model in the background — the stream/transcribe paths load it on demand
|
|
119
|
+
// (memoized in the worker) and it is hot by the time recording stops.
|
|
120
|
+
// Only a genuine first-use download blocks, with explicit progress, so we
|
|
121
|
+
// never record silently against missing weights.
|
|
122
|
+
if (await isSttModelCached(modelKey)) {
|
|
123
|
+
this.#warmModel(modelKey);
|
|
124
|
+
} else {
|
|
125
|
+
await downloadSttModel(modelKey, p => status(`Downloading speech model ${p.label} (${p.percent}%)`));
|
|
126
|
+
}
|
|
127
|
+
if (wroteStatus) options.showStatus("");
|
|
128
|
+
this.#resolvedModelKey = modelKey;
|
|
104
129
|
return true;
|
|
105
130
|
} catch (err) {
|
|
106
131
|
const msg = err instanceof Error ? err.message : "Failed to setup STT dependencies";
|
|
@@ -110,6 +135,22 @@ export class STTController {
|
|
|
110
135
|
}
|
|
111
136
|
}
|
|
112
137
|
|
|
138
|
+
/** Warm the speech model in the worker without blocking recording. The worker
|
|
139
|
+
* memoizes the load, so the stream/transcribe path reuses it and the model is
|
|
140
|
+
* hot by the time recording stops. Only called when the weights are already
|
|
141
|
+
* cached, so no network fetch happens. On load failure (corrupt cache, OOM,
|
|
142
|
+
* runtime install) invalidate the resolved key so the next toggle re-runs
|
|
143
|
+
* preflight and retries instead of skipping it forever. */
|
|
144
|
+
#warmModel(modelKey: string): void {
|
|
145
|
+
void downloadSttModel(modelKey).catch(err => {
|
|
146
|
+
// Guard against a concurrent model switch clobbering a newer resolution.
|
|
147
|
+
if (!this.#disposed && this.#resolvedModelKey === modelKey) this.#resolvedModelKey = null;
|
|
148
|
+
logger.debug("stt: background model warmup failed", {
|
|
149
|
+
error: err instanceof Error ? err.message : String(err),
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
113
154
|
async #start(editor: Editor, options: ToggleOptions): Promise<void> {
|
|
114
155
|
if (!(await this.#ensureDeps(options))) return;
|
|
115
156
|
// Live transcription needs a recorder that can pipe PCM; the Windows
|
|
@@ -334,6 +375,6 @@ export class STTController {
|
|
|
334
375
|
this.#tempFile = null;
|
|
335
376
|
}
|
|
336
377
|
this.#state = "idle";
|
|
337
|
-
this.#
|
|
378
|
+
this.#resolvedModelKey = null;
|
|
338
379
|
}
|
|
339
380
|
}
|
package/src/system-prompt.ts
CHANGED
|
@@ -615,7 +615,13 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
615
615
|
resolvedCustomPrompt,
|
|
616
616
|
resolvedAppendPrompt,
|
|
617
617
|
]);
|
|
618
|
-
const
|
|
618
|
+
const contextPromptSources = contextFiles.map(file => file.content);
|
|
619
|
+
const promptSources = [
|
|
620
|
+
effectiveSystemPromptCustomization,
|
|
621
|
+
resolvedCustomPrompt,
|
|
622
|
+
resolvedAppendPrompt,
|
|
623
|
+
...contextPromptSources,
|
|
624
|
+
];
|
|
619
625
|
const injectedAlwaysApplyRules = dedupeAlwaysApplyRules(alwaysApplyRules, promptSources);
|
|
620
626
|
|
|
621
627
|
const environment = await logger.time("getEnvironmentInfo", getEnvironmentInfo);
|
package/src/task/executor.ts
CHANGED
|
@@ -7,11 +7,16 @@
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import type { AgentEvent, AgentIdentity, AgentTelemetryConfig, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
9
9
|
import { recordHandoff, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
|
|
10
|
-
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
10
|
+
import type { Api, Model, Usage } from "@oh-my-pi/pi-ai";
|
|
11
11
|
import { logger, popLoopPhase, prompt, pushLoopPhase, untilAborted } from "@oh-my-pi/pi-utils";
|
|
12
12
|
import type { Rule } from "../capability/rule";
|
|
13
13
|
import { ModelRegistry } from "../config/model-registry";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
formatModelSelectorValue,
|
|
16
|
+
formatModelStringWithRouting,
|
|
17
|
+
resolveModelOverride,
|
|
18
|
+
resolveModelOverrideWithAuthFallback,
|
|
19
|
+
} from "../config/model-resolver";
|
|
15
20
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
16
21
|
import { Settings } from "../config/settings";
|
|
17
22
|
import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
|
|
@@ -120,6 +125,74 @@ function normalizeModelPatterns(value: string | string[] | undefined): string[]
|
|
|
120
125
|
.filter(Boolean);
|
|
121
126
|
}
|
|
122
127
|
|
|
128
|
+
const SUBAGENT_RETRY_FALLBACK_ROLE_PREFIX = "subagent:";
|
|
129
|
+
|
|
130
|
+
interface SubagentRetryFallbackCandidate {
|
|
131
|
+
model: Model<Api>;
|
|
132
|
+
selector: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function resolveSubagentRetryFallbackCandidates(
|
|
136
|
+
modelPatterns: string[],
|
|
137
|
+
modelRegistry: ModelRegistry,
|
|
138
|
+
settings: Settings,
|
|
139
|
+
): SubagentRetryFallbackCandidate[] {
|
|
140
|
+
const candidates: SubagentRetryFallbackCandidate[] = [];
|
|
141
|
+
const seen = new Set<string>();
|
|
142
|
+
for (const pattern of modelPatterns) {
|
|
143
|
+
const resolved = resolveModelOverride([pattern], modelRegistry, settings);
|
|
144
|
+
if (!resolved.model) continue;
|
|
145
|
+
const selector = resolved.explicitThinkingLevel
|
|
146
|
+
? formatModelSelectorValue(formatModelStringWithRouting(resolved.model), resolved.thinkingLevel)
|
|
147
|
+
: formatModelStringWithRouting(resolved.model);
|
|
148
|
+
if (seen.has(selector)) continue;
|
|
149
|
+
seen.add(selector);
|
|
150
|
+
candidates.push({ model: resolved.model, selector });
|
|
151
|
+
}
|
|
152
|
+
return candidates;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function installSubagentRetryFallbackChain(args: {
|
|
156
|
+
settings: Settings;
|
|
157
|
+
id: string;
|
|
158
|
+
candidates: SubagentRetryFallbackCandidate[];
|
|
159
|
+
model: Model<Api> | undefined;
|
|
160
|
+
authFallbackUsed: boolean;
|
|
161
|
+
}): string | undefined {
|
|
162
|
+
const { settings, id, candidates, model, authFallbackUsed } = args;
|
|
163
|
+
if (!model || authFallbackUsed || candidates.length <= 1) return undefined;
|
|
164
|
+
|
|
165
|
+
const selectedIndex = candidates.findIndex(
|
|
166
|
+
candidate => candidate.model.provider === model.provider && candidate.model.id === model.id,
|
|
167
|
+
);
|
|
168
|
+
if (selectedIndex < 0) return undefined;
|
|
169
|
+
const fallbackSelectors = candidates.slice(selectedIndex + 1).map(candidate => candidate.selector);
|
|
170
|
+
if (fallbackSelectors.length === 0) return undefined;
|
|
171
|
+
|
|
172
|
+
const role = `${SUBAGENT_RETRY_FALLBACK_ROLE_PREFIX}${id}`;
|
|
173
|
+
const modelRoles: Record<string, string> = {};
|
|
174
|
+
const existingRoles = settings.getModelRoles();
|
|
175
|
+
for (const existingRole in existingRoles) {
|
|
176
|
+
const selector = existingRoles[existingRole];
|
|
177
|
+
if (selector) {
|
|
178
|
+
modelRoles[existingRole] = selector;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
modelRoles[role] = candidates[selectedIndex].selector;
|
|
182
|
+
settings.override("modelRoles", modelRoles);
|
|
183
|
+
const fallbackChains: Record<string, string[]> = {
|
|
184
|
+
[role]: fallbackSelectors,
|
|
185
|
+
};
|
|
186
|
+
const existingFallbackChains = settings.get("retry.fallbackChains");
|
|
187
|
+
for (const existingRole in existingFallbackChains) {
|
|
188
|
+
if (existingRole !== role) {
|
|
189
|
+
fallbackChains[existingRole] = existingFallbackChains[existingRole];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
settings.override("retry.fallbackChains", fallbackChains);
|
|
193
|
+
return role;
|
|
194
|
+
}
|
|
195
|
+
|
|
123
196
|
function renderIrcPeerRoster(selfId: string): string {
|
|
124
197
|
const peers = AgentRegistry.global()
|
|
125
198
|
.list()
|
|
@@ -278,6 +351,12 @@ export interface ExecutorOptions {
|
|
|
278
351
|
parentTelemetry?: AgentTelemetryConfig;
|
|
279
352
|
/** Skills to autoload via sendCustomMessage before the first prompt */
|
|
280
353
|
autoloadSkills?: Skill[];
|
|
354
|
+
/**
|
|
355
|
+
* Registry id of the spawning agent, recorded as this subagent's parent.
|
|
356
|
+
* Forwarded verbatim to the SDK; the executor never derives it (the spawner
|
|
357
|
+
* passes its own `getAgentId()`).
|
|
358
|
+
*/
|
|
359
|
+
parentAgentId?: string;
|
|
281
360
|
}
|
|
282
361
|
|
|
283
362
|
function parseStringifiedJson(value: unknown): unknown {
|
|
@@ -1222,6 +1301,16 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
1222
1301
|
popLoopPhase();
|
|
1223
1302
|
}
|
|
1224
1303
|
}
|
|
1304
|
+
if (event.type === "retry_fallback_applied") {
|
|
1305
|
+
progress.resolvedModel = event.to;
|
|
1306
|
+
scheduleProgress(true);
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
if (event.type === "retry_fallback_succeeded") {
|
|
1310
|
+
progress.resolvedModel = event.model;
|
|
1311
|
+
scheduleProgress(true);
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1225
1314
|
});
|
|
1226
1315
|
|
|
1227
1316
|
const captureSalvage = (session: AgentSession): void => {
|
|
@@ -1817,13 +1906,26 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1817
1906
|
resolvedModel: model.id,
|
|
1818
1907
|
});
|
|
1819
1908
|
}
|
|
1909
|
+
const retryFallbackRole = installSubagentRetryFallbackChain({
|
|
1910
|
+
settings: subagentSettings,
|
|
1911
|
+
id,
|
|
1912
|
+
candidates: resolveSubagentRetryFallbackCandidates(modelPatterns, modelRegistry, settings),
|
|
1913
|
+
model,
|
|
1914
|
+
authFallbackUsed,
|
|
1915
|
+
});
|
|
1916
|
+
if (retryFallbackRole) {
|
|
1917
|
+
logger.debug("Configured subagent runtime model fallback chain", {
|
|
1918
|
+
role: retryFallbackRole,
|
|
1919
|
+
requested: modelPatterns,
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1820
1922
|
if (model?.contextWindow && model.contextWindow > 0) {
|
|
1821
1923
|
progress.contextWindow = model.contextWindow;
|
|
1822
1924
|
}
|
|
1823
1925
|
if (model) {
|
|
1824
1926
|
progress.resolvedModel = explicitThinkingLevel
|
|
1825
|
-
?
|
|
1826
|
-
:
|
|
1927
|
+
? formatModelSelectorValue(formatModelStringWithRouting(model), resolvedThinkingLevel)
|
|
1928
|
+
: formatModelStringWithRouting(model);
|
|
1827
1929
|
}
|
|
1828
1930
|
const effectiveThinkingLevel = explicitThinkingLevel
|
|
1829
1931
|
? resolvedThinkingLevel
|
|
@@ -1831,7 +1933,12 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1831
1933
|
|
|
1832
1934
|
const effectiveCwd = worktree ?? cwd;
|
|
1833
1935
|
const sessionManager = sessionFile
|
|
1834
|
-
? await awaitAbortable(
|
|
1936
|
+
? await awaitAbortable(
|
|
1937
|
+
SessionManager.open(sessionFile, undefined, undefined, {
|
|
1938
|
+
initialCwd: effectiveCwd,
|
|
1939
|
+
suppressBreadcrumb: true,
|
|
1940
|
+
}),
|
|
1941
|
+
)
|
|
1835
1942
|
: SessionManager.inMemory(effectiveCwd);
|
|
1836
1943
|
if (options.parentArtifactManager) {
|
|
1837
1944
|
sessionManager.adoptArtifactManager(options.parentArtifactManager);
|
|
@@ -1920,6 +2027,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1920
2027
|
parentHindsightSessionState: options.parentHindsightSessionState,
|
|
1921
2028
|
parentMnemopiSessionState: options.parentMnemopiSessionState,
|
|
1922
2029
|
parentTaskPrefix: id,
|
|
2030
|
+
parentAgentId: options.parentAgentId,
|
|
1923
2031
|
agentId: id,
|
|
1924
2032
|
agentDisplayName: subagentDisplayName,
|
|
1925
2033
|
enableLsp: lspEnabled,
|
|
@@ -1952,7 +2060,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1952
2060
|
// (createAgentSession → agent.replaceMessages). Isolated runs are not
|
|
1953
2061
|
// resumable (worktree is merged + cleaned) and never get a reviver.
|
|
1954
2062
|
reviveSession = async () => {
|
|
1955
|
-
const reopened = await SessionManager.open(sessionFile
|
|
2063
|
+
const reopened = await SessionManager.open(sessionFile, undefined, undefined, {
|
|
2064
|
+
suppressBreadcrumb: true,
|
|
2065
|
+
});
|
|
1956
2066
|
if (options.parentArtifactManager) {
|
|
1957
2067
|
reopened.adoptArtifactManager(options.parentArtifactManager);
|
|
1958
2068
|
}
|
|
@@ -1988,6 +2098,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1988
2098
|
systemPrompt: session.agent.state.systemPrompt.join("\n\n"),
|
|
1989
2099
|
task,
|
|
1990
2100
|
tools: session.getActiveToolNames(),
|
|
2101
|
+
spawns: spawnsEnv,
|
|
2102
|
+
readSummarize: agent.readSummarize,
|
|
1991
2103
|
outputSchema,
|
|
1992
2104
|
});
|
|
1993
2105
|
|
package/src/task/index.ts
CHANGED
|
@@ -46,7 +46,7 @@ import "../tools/review";
|
|
|
46
46
|
import type { AsyncJobManager } from "../async";
|
|
47
47
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
48
48
|
import { loadOverallPlanReference } from "../plan-mode/plan-handoff";
|
|
49
|
-
import { AgentRegistry } from "../registry/agent-registry";
|
|
49
|
+
import { AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
50
50
|
import { generateCommitMessage } from "../utils/commit-message-generator";
|
|
51
51
|
import * as git from "../utils/git";
|
|
52
52
|
import { type DiscoveryResult, discoverAgents, getAgent } from "./discovery";
|
|
@@ -156,7 +156,6 @@ export const READ_ONLY_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
|
156
156
|
"reflect",
|
|
157
157
|
"retain",
|
|
158
158
|
"memory_edit",
|
|
159
|
-
"render_mermaid",
|
|
160
159
|
"inspect_image",
|
|
161
160
|
"checkpoint",
|
|
162
161
|
"rewind",
|
|
@@ -1301,6 +1300,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1301
1300
|
parentMnemopiSessionState: this.session.getMnemopiSessionState?.(),
|
|
1302
1301
|
parentTelemetry: this.session.getTelemetry?.(),
|
|
1303
1302
|
parentEvalSessionId,
|
|
1303
|
+
parentAgentId: this.session.getAgentId?.() ?? MAIN_AGENT_ID,
|
|
1304
1304
|
};
|
|
1305
1305
|
|
|
1306
1306
|
const runTask = async (): Promise<SingleResult> => {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import type { ModelRegistry } from "../config/model-registry";
|
|
4
|
+
import type { Settings } from "../config/settings";
|
|
5
|
+
import { MCPManager } from "../mcp/manager";
|
|
6
|
+
import type { PersistedSubagentReviverFactory } from "../registry/agent-lifecycle";
|
|
7
|
+
import { AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
8
|
+
import { createAgentSession } from "../sdk";
|
|
9
|
+
import type { AgentSession } from "../session/agent-session";
|
|
10
|
+
import type { AuthStorage } from "../session/auth-storage";
|
|
11
|
+
import { SessionManager } from "../session/session-manager";
|
|
12
|
+
import { createMCPProxyTools, createSubagentSettings } from "./executor";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Ambient context the reviver needs at revive time. The top-level session is
|
|
16
|
+
* kept LIVE (cwd / artifact manager read on demand) so a later `/new` or cwd
|
|
17
|
+
* move is followed rather than snapshotted; auth/models/settings are
|
|
18
|
+
* process-stable and captured by reference.
|
|
19
|
+
*/
|
|
20
|
+
export interface PersistedSubagentReviveContext {
|
|
21
|
+
session: AgentSession;
|
|
22
|
+
authStorage: AuthStorage;
|
|
23
|
+
modelRegistry: ModelRegistry;
|
|
24
|
+
settings: Settings;
|
|
25
|
+
/** LSP policy of the top-level session; revived subagents inherit it rather than defaulting on. */
|
|
26
|
+
enableLsp: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build the factory the {@link AgentLifecycleManager} uses to cold-revive a
|
|
31
|
+
* `parked` subagent ref restored from disk (Agent Hub scan, collab mirror, or a
|
|
32
|
+
* resumed process). Such a ref carries a sessionFile but no in-memory adoption —
|
|
33
|
+
* the executor's live reviver closure died with the process/turn that spawned
|
|
34
|
+
* it — so `ensureLive` (IRC sends, hub focus) would otherwise refuse it.
|
|
35
|
+
*
|
|
36
|
+
* This rebuilds the subagent the same way `--resume` rebuilds a session: reopen
|
|
37
|
+
* the JSONL and replay it through {@link createAgentSession}. The catch is that
|
|
38
|
+
* resume restores only conversation/model from the file — the runtime contract
|
|
39
|
+
* (tools / system prompt / output schema / kind) is built from options, so a
|
|
40
|
+
* bare reopen would resurrect a wrong (top-level) session. We source that
|
|
41
|
+
* contract from the persisted `session_init` entry instead, and mirror the
|
|
42
|
+
* executor's subagent wiring (MCP proxy tools, depth-derived gating,
|
|
43
|
+
* yield-required, active-tool clamp, registry status sync).
|
|
44
|
+
*/
|
|
45
|
+
export function createPersistedSubagentReviverFactory(
|
|
46
|
+
ctx: PersistedSubagentReviveContext,
|
|
47
|
+
): PersistedSubagentReviverFactory {
|
|
48
|
+
const registry = AgentRegistry.global();
|
|
49
|
+
return async ref => {
|
|
50
|
+
const sessionFile = ref.sessionFile;
|
|
51
|
+
if (!sessionFile) return undefined;
|
|
52
|
+
const peek = await SessionManager.peekSessionInit(sessionFile);
|
|
53
|
+
// No persisted contract (pre-session_init file) or the recorded workspace
|
|
54
|
+
// is gone (isolated/merged worktree, moved dir): leave it transcript-only
|
|
55
|
+
// (history://) rather than resurrect a wrong or broken session.
|
|
56
|
+
if (!peek?.init) return undefined;
|
|
57
|
+
try {
|
|
58
|
+
await fs.stat(peek.cwd);
|
|
59
|
+
} catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const init = peek.init;
|
|
63
|
+
// taskDepth drives real capability gating (task-spawn allowance, memory
|
|
64
|
+
// startup, …); derive it from the persisted parent chain rather than
|
|
65
|
+
// assuming a fixed level.
|
|
66
|
+
let taskDepth = 1;
|
|
67
|
+
let parentId = ref.parentId;
|
|
68
|
+
const seen = new Set<string>();
|
|
69
|
+
while (parentId && parentId !== MAIN_AGENT_ID && !seen.has(parentId)) {
|
|
70
|
+
seen.add(parentId);
|
|
71
|
+
taskDepth++;
|
|
72
|
+
parentId = registry.get(parentId)?.parentId;
|
|
73
|
+
}
|
|
74
|
+
return async () => {
|
|
75
|
+
// Re-open fresh on every revive: park closes the writer, so this takes
|
|
76
|
+
// the single-writer lock cleanly and restores the full message history.
|
|
77
|
+
const reopened = await SessionManager.open(sessionFile, undefined, undefined, {
|
|
78
|
+
suppressBreadcrumb: true,
|
|
79
|
+
});
|
|
80
|
+
const artifactManager = ctx.session.sessionManager.getArtifactManager();
|
|
81
|
+
if (artifactManager) reopened.adoptArtifactManager(artifactManager);
|
|
82
|
+
// Reuse the parent's live MCP connections via proxy tools (no
|
|
83
|
+
// re-discovery), exactly as the executor does for live subagents.
|
|
84
|
+
const mcpManager = MCPManager.instance();
|
|
85
|
+
const mcpProxyTools = mcpManager ? createMCPProxyTools(mcpManager) : [];
|
|
86
|
+
const { session } = await createAgentSession({
|
|
87
|
+
cwd: ctx.session.sessionManager.getCwd(),
|
|
88
|
+
authStorage: ctx.authStorage,
|
|
89
|
+
modelRegistry: ctx.modelRegistry,
|
|
90
|
+
settings: createSubagentSettings(
|
|
91
|
+
ctx.settings,
|
|
92
|
+
init.readSummarize === false ? { "read.summarize.enabled": false } : undefined,
|
|
93
|
+
),
|
|
94
|
+
sessionManager: reopened,
|
|
95
|
+
agentId: ref.id,
|
|
96
|
+
agentDisplayName: ref.displayName,
|
|
97
|
+
parentTaskPrefix: ref.id,
|
|
98
|
+
parentAgentId: ref.parentId,
|
|
99
|
+
taskDepth,
|
|
100
|
+
toolNames: init.tools,
|
|
101
|
+
outputSchema: init.outputSchema,
|
|
102
|
+
requireYieldTool: true,
|
|
103
|
+
systemPrompt: () => [init.systemPrompt],
|
|
104
|
+
// Old files predate persisted spawns: deny re-spawning rather than let
|
|
105
|
+
// createAgentSession default to wildcard ("*").
|
|
106
|
+
spawns: init.spawns ?? "",
|
|
107
|
+
hasUI: false,
|
|
108
|
+
enableLsp: ctx.enableLsp,
|
|
109
|
+
enableMCP: !mcpManager,
|
|
110
|
+
mcpManager,
|
|
111
|
+
customTools: mcpProxyTools.length > 0 ? mcpProxyTools : undefined,
|
|
112
|
+
});
|
|
113
|
+
// Clamp the active set to the persisted list: createAgentSession's
|
|
114
|
+
// `alwaysInclude` can re-add non-defaultInactive extension/custom tools
|
|
115
|
+
// the original run didn't carry. Unknown/missing names are ignored.
|
|
116
|
+
await session.setActiveToolsByName(init.tools);
|
|
117
|
+
// Cold revives must drive registry status themselves — createAgentSession
|
|
118
|
+
// doesn't wire this generically (the live path does it in the executor).
|
|
119
|
+
// Without it the idle-TTL timer never clears on a turn and the lifecycle
|
|
120
|
+
// could park the agent mid-run.
|
|
121
|
+
session.subscribe(event => {
|
|
122
|
+
if (event.type === "agent_start") registry.setStatus(ref.id, "running");
|
|
123
|
+
else if (event.type === "agent_end") registry.setStatus(ref.id, "idle");
|
|
124
|
+
});
|
|
125
|
+
return session;
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
}
|
package/src/tiny/models.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface TinyTitleLocalModelSpec {
|
|
|
10
10
|
label: string;
|
|
11
11
|
description: string;
|
|
12
12
|
contextNote: string;
|
|
13
|
+
/** Model family emits hidden reasoning unless the chat template disables it. */
|
|
14
|
+
reasoning?: boolean;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export const TINY_TITLE_LOCAL_MODELS = [
|
|
@@ -28,6 +30,7 @@ export const TINY_TITLE_LOCAL_MODELS = [
|
|
|
28
30
|
label: "Qwen3 0.6B",
|
|
29
31
|
description: "Most robust local option; slower first load, about 500 MB cached.",
|
|
30
32
|
contextNote: "Use when title quality matters more than local startup cost.",
|
|
33
|
+
reasoning: true,
|
|
31
34
|
},
|
|
32
35
|
{
|
|
33
36
|
key: "gemma-270m",
|
|
@@ -122,6 +125,7 @@ export const TINY_MEMORY_LOCAL_MODELS = [
|
|
|
122
125
|
description:
|
|
123
126
|
"Recommended; most disciplined extraction (ignores chit-chat), good consolidation, about 1.1 GB cached.",
|
|
124
127
|
contextNote: "Best single-model pick for memory from the local experiment.",
|
|
128
|
+
reasoning: true,
|
|
125
129
|
},
|
|
126
130
|
{
|
|
127
131
|
key: "gemma-3-1b",
|
|
@@ -196,6 +200,12 @@ export function getTinyMemoryModelSpec(key: TinyMemoryLocalModelKey): (typeof TI
|
|
|
196
200
|
return spec;
|
|
197
201
|
}
|
|
198
202
|
|
|
203
|
+
/** Return whether a memory local model may emit reasoning tokens before answers. */
|
|
204
|
+
export function isTinyMemoryReasoningModelKey(key: TinyMemoryLocalModelKey): boolean {
|
|
205
|
+
const spec = getTinyMemoryModelSpec(key);
|
|
206
|
+
return "reasoning" in spec && spec.reasoning === true;
|
|
207
|
+
}
|
|
208
|
+
|
|
199
209
|
/** Any local model key (title or memory), used by the shared inference worker. */
|
|
200
210
|
export type TinyLocalModelKey = TinyTitleLocalModelKey | TinyMemoryLocalModelKey;
|
|
201
211
|
|
package/src/tiny/worker.ts
CHANGED
|
@@ -31,7 +31,8 @@ const TITLE_PREFILL = "<title>";
|
|
|
31
31
|
const TITLE_CLOSE = "</title>";
|
|
32
32
|
const TITLE_MAX_NEW_TOKENS = 20;
|
|
33
33
|
const STOP_DECODE_WINDOW_TOKENS = 32;
|
|
34
|
-
const
|
|
34
|
+
const MEMORY_COMPLETION_DEFAULT_MAX_NEW_TOKENS = 256;
|
|
35
|
+
const COMPLETION_MAX_NEW_TOKENS = 1024;
|
|
35
36
|
const TINY_TITLE_SYSTEM_PROMPT = prompt.render(tinyTitleSystemPrompt);
|
|
36
37
|
const TRANSFORMERS_PACKAGE = "@huggingface/transformers";
|
|
37
38
|
const COMPILED_TRANSFORMERS_VERSION = process.env.PI_TINY_TRANSFORMERS_VERSION;
|
|
@@ -426,8 +427,8 @@ async function generateCompletion(
|
|
|
426
427
|
): Promise<string | null> {
|
|
427
428
|
const generator = await loadPipeline(modelKey, transport, requestId);
|
|
428
429
|
const text = buildCompletionPrompt(generator, promptText);
|
|
429
|
-
const requested = maxTokens ??
|
|
430
|
-
const maxNewTokens = Math.min(Math.max(1, requested),
|
|
430
|
+
const requested = maxTokens ?? MEMORY_COMPLETION_DEFAULT_MAX_NEW_TOKENS;
|
|
431
|
+
const maxNewTokens = Math.min(Math.max(1, requested), COMPLETION_MAX_NEW_TOKENS);
|
|
431
432
|
const output = (await generator(text, {
|
|
432
433
|
max_new_tokens: maxNewTokens,
|
|
433
434
|
do_sample: false,
|
package/src/tools/index.ts
CHANGED
|
@@ -55,7 +55,6 @@ import { MemoryReflectTool } from "./memory-reflect";
|
|
|
55
55
|
import { MemoryRetainTool } from "./memory-retain";
|
|
56
56
|
import { wrapToolWithMetaNotice } from "./output-meta";
|
|
57
57
|
import { ReadTool } from "./read";
|
|
58
|
-
import { RenderMermaidTool } from "./render-mermaid";
|
|
59
58
|
import { createReportToolIssueTool, isAutoQaEnabled } from "./report-tool-issue";
|
|
60
59
|
import { ResolveTool } from "./resolve";
|
|
61
60
|
import { reportFindingTool } from "./review";
|
|
@@ -94,7 +93,6 @@ export * from "./memory-recall";
|
|
|
94
93
|
export * from "./memory-reflect";
|
|
95
94
|
export * from "./memory-retain";
|
|
96
95
|
export * from "./read";
|
|
97
|
-
export * from "./render-mermaid";
|
|
98
96
|
export * from "./report-tool-issue";
|
|
99
97
|
export * from "./resolve";
|
|
100
98
|
export * from "./review";
|
|
@@ -420,7 +418,6 @@ export const BUILTIN_TOOLS: Record<BuiltinToolName, ToolFactory> = {
|
|
|
420
418
|
edit: s => new EditTool(s),
|
|
421
419
|
ast_grep: s => new AstGrepTool(s),
|
|
422
420
|
ast_edit: s => new AstEditTool(s),
|
|
423
|
-
render_mermaid: s => new RenderMermaidTool(s),
|
|
424
421
|
ask: AskTool.createIf,
|
|
425
422
|
debug: DebugTool.createIf,
|
|
426
423
|
eval: s => new EvalTool(s),
|
|
@@ -563,7 +560,6 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
563
560
|
if (name === "github") return session.settings.get("github.enabled");
|
|
564
561
|
if (name === "ast_grep") return session.settings.get("astGrep.enabled");
|
|
565
562
|
if (name === "ast_edit") return session.settings.get("astEdit.enabled");
|
|
566
|
-
if (name === "render_mermaid") return session.settings.get("renderMermaid.enabled");
|
|
567
563
|
if (name === "inspect_image") return session.settings.get("inspect_image.enabled");
|
|
568
564
|
if (name === "web_search") return session.settings.get("web_search.enabled");
|
|
569
565
|
// search_tool_bm25 is allowed when either legacy mcp.discoveryMode or new tools.discoveryMode is active.
|
package/src/tools/output-meta.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
AgentToolUpdateCallback,
|
|
13
13
|
} from "@oh-my-pi/pi-agent-core";
|
|
14
14
|
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
15
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
15
16
|
import { getDefault, type Settings } from "../config/settings";
|
|
16
17
|
import { formatGroupedDiagnosticMessages } from "../lsp/utils";
|
|
17
18
|
import type { Theme } from "../modes/theme/theme";
|
|
@@ -616,9 +617,22 @@ async function spillLargeResultToArtifact(
|
|
|
616
617
|
const totalBytes = Buffer.byteLength(fullText, "utf-8");
|
|
617
618
|
if (totalBytes <= threshold) return result;
|
|
618
619
|
|
|
619
|
-
// Save full output as artifact
|
|
620
|
-
|
|
621
|
-
|
|
620
|
+
// Save the full output as an artifact so the elided bytes stay recoverable.
|
|
621
|
+
// In a persistent session this hits `Bun.write`, which can throw (disk full,
|
|
622
|
+
// permissions). The spill wraps arbitrary tools (built-in, MCP, extension,
|
|
623
|
+
// RPC-host); a save failure must never convert a successful call into an
|
|
624
|
+
// error, nor re-expose the full (possibly context-blowing) output. Mirror
|
|
625
|
+
// `enforceInlineByteCap`: always truncate past the threshold, and only
|
|
626
|
+
// attach the `artifact://` recovery link when the save actually succeeded.
|
|
627
|
+
let artifactId: string | undefined;
|
|
628
|
+
try {
|
|
629
|
+
artifactId = await sessionManager.saveArtifact(fullText, toolName);
|
|
630
|
+
} catch (error) {
|
|
631
|
+
logger.warn("Failed to spill large tool result to artifact", {
|
|
632
|
+
tool: toolName,
|
|
633
|
+
error: error instanceof Error ? error.message : String(error),
|
|
634
|
+
});
|
|
635
|
+
}
|
|
622
636
|
|
|
623
637
|
// Truncate: middle elision when a head budget is configured, otherwise tail-only.
|
|
624
638
|
const useMiddle = headBytes > 0;
|
|
@@ -66,6 +66,7 @@ const EXTENSION_LANG: Record<string, readonly [string, string]> = {
|
|
|
66
66
|
cljc: ["clojure", "clojure"],
|
|
67
67
|
cljs: ["clojure", "clojure"],
|
|
68
68
|
edn: ["clojure", "clojure"],
|
|
69
|
+
el: ["emacs-lisp", "emacs-lisp"],
|
|
69
70
|
|
|
70
71
|
// .NET
|
|
71
72
|
cs: ["csharp", "csharp"],
|
|
@@ -209,6 +210,7 @@ export function getLanguageFromPath(filePath: string): string | undefined {
|
|
|
209
210
|
if (baseName === "dockerfile" || baseName.startsWith("dockerfile.") || baseName === "containerfile") {
|
|
210
211
|
return "dockerfile";
|
|
211
212
|
}
|
|
213
|
+
if (baseName === ".emacs") return "emacs-lisp";
|
|
212
214
|
if (baseName === "justfile") return "just";
|
|
213
215
|
if (baseName === "cmakelists.txt") return "cmake";
|
|
214
216
|
|
|
@@ -223,6 +225,9 @@ export function detectLanguageId(filePath: string): string {
|
|
|
223
225
|
if (baseName === "dockerfile" || baseName.startsWith("dockerfile.") || baseName === "containerfile") {
|
|
224
226
|
return "dockerfile";
|
|
225
227
|
}
|
|
228
|
+
if (baseName === ".emacs") {
|
|
229
|
+
return "emacs-lisp";
|
|
230
|
+
}
|
|
226
231
|
if (baseName === "makefile" || baseName === "gnumakefile") {
|
|
227
232
|
return "makefile";
|
|
228
233
|
}
|
package/src/utils/markit.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { untilAborted } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { logger, untilAborted } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { Markit, StreamInfo } from "markit-ai";
|
|
3
3
|
import { ToolAbortError } from "../tools/tool-errors";
|
|
4
4
|
|
|
@@ -8,6 +8,29 @@ export interface MarkitConversionResult {
|
|
|
8
8
|
error?: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
interface MuPdfWasmModuleConfig {
|
|
12
|
+
print?: (...values: unknown[]) => void;
|
|
13
|
+
printErr?: (...values: unknown[]) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare global {
|
|
17
|
+
var $libmupdf_wasm_Module: MuPdfWasmModuleConfig | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function logMuPdfWasmOutput(stream: "stdout" | "stderr", values: unknown[]): void {
|
|
21
|
+
const message = values.length === 1 && typeof values[0] === "string" ? values[0] : values.map(String).join(" ");
|
|
22
|
+
logger.debug("mupdf wasm output", { stream, message });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function installMuPdfWasmLogger(): void {
|
|
26
|
+
const moduleConfig = globalThis.$libmupdf_wasm_Module ?? {};
|
|
27
|
+
moduleConfig.print = (...values) => logMuPdfWasmOutput("stdout", values);
|
|
28
|
+
moduleConfig.printErr = (...values) => logMuPdfWasmOutput("stderr", values);
|
|
29
|
+
globalThis.$libmupdf_wasm_Module = moduleConfig;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
installMuPdfWasmLogger();
|
|
33
|
+
|
|
11
34
|
let markit: () => Markit | Promise<Markit> = async () => {
|
|
12
35
|
const promise = import("markit-ai").then(({ Markit }) => {
|
|
13
36
|
const instance = new Markit();
|