@oh-my-pi/pi-coding-agent 17.1.3 → 17.1.4
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 +54 -0
- package/dist/cli.js +3759 -3751
- package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +1 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +8 -0
- package/dist/types/cli/update-cli.d.ts +41 -0
- package/dist/types/cli/usage-cli.d.ts +4 -2
- package/dist/types/commands/update.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +19 -0
- package/dist/types/config/settings-schema.d.ts +30 -7
- package/dist/types/cursor.d.ts +47 -1
- package/dist/types/eval/js/process-entry.d.ts +2 -1
- package/dist/types/eval/js/worker-core.d.ts +4 -1
- package/dist/types/extensibility/extensions/runner.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +15 -7
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +9 -0
- package/dist/types/modes/components/custom-editor.d.ts +12 -8
- package/dist/types/plan-mode/approved-plan.d.ts +3 -2
- package/dist/types/secrets/obfuscator.d.ts +7 -31
- package/dist/types/session/agent-session.d.ts +19 -4
- package/dist/types/session/prewalk.d.ts +2 -1
- package/dist/types/session/session-advisors.d.ts +8 -0
- package/dist/types/session/session-metadata.d.ts +29 -0
- package/dist/types/session/turn-recovery.d.ts +5 -5
- package/dist/types/stt/sherpa-runtime.d.ts +38 -0
- package/dist/types/thinking.d.ts +24 -0
- package/dist/types/tools/bash.d.ts +5 -4
- package/dist/types/tools/computer/exposure.d.ts +8 -0
- package/dist/types/tools/computer/supervisor.d.ts +1 -1
- package/dist/types/tools/computer/worker-entry.d.ts +1 -1
- package/dist/types/tools/computer/worker.d.ts +1 -1
- package/dist/types/tools/computer.d.ts +6 -0
- package/dist/types/tools/shell-tokenize.d.ts +14 -0
- package/dist/types/tools/todo.d.ts +7 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +80 -10
- package/src/advisor/runtime.ts +27 -1
- package/src/cli/__tests__/auth-gateway-catalog.test.ts +111 -0
- package/src/cli/auth-gateway-cli.ts +63 -16
- package/src/cli/config-cli.ts +25 -7
- package/src/cli/update-cli.ts +229 -29
- package/src/cli/usage-cli.ts +144 -15
- package/src/cli.ts +21 -15
- package/src/commands/update.ts +6 -0
- package/src/config/__tests__/model-registry.test.ts +42 -7
- package/src/config/model-registry.ts +67 -4
- package/src/config/settings-schema.ts +39 -8
- package/src/config/settings.ts +24 -2
- package/src/cursor.ts +153 -0
- package/src/dap/session.ts +70 -11
- package/src/eval/__tests__/js-context-manager.test.ts +9 -1
- package/src/eval/__tests__/process-entry-import.test.ts +111 -1
- package/src/eval/js/process-entry.ts +9 -5
- package/src/eval/js/worker-core.ts +6 -2
- package/src/exec/bash-executor.ts +4 -2
- package/src/extensibility/extensions/runner.ts +23 -8
- package/src/extensibility/legacy-pi-ai-shim.ts +9 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +14 -2
- package/src/extensibility/legacy-pi-tui-shim.ts +33 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/main.ts +22 -1
- package/src/modes/acp/acp-agent.ts +6 -0
- package/src/modes/components/assistant-message.ts +88 -11
- package/src/modes/components/chat-transcript-builder.ts +2 -0
- package/src/modes/components/custom-editor.test.ts +170 -0
- package/src/modes/components/custom-editor.ts +79 -29
- package/src/modes/components/settings-defs.ts +5 -1
- package/src/modes/controllers/event-controller.ts +96 -4
- package/src/modes/controllers/selector-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +34 -8
- package/src/modes/utils/context-usage.ts +19 -2
- package/src/modes/utils/interactive-context-helpers.ts +1 -0
- package/src/plan-mode/approved-plan.ts +18 -10
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +10 -1
- package/src/sdk.ts +4 -0
- package/src/secrets/obfuscator.ts +36 -126
- package/src/session/agent-session.ts +69 -60
- package/src/session/prewalk.ts +8 -3
- package/src/session/session-advisors.ts +67 -3
- package/src/session/session-metadata.ts +53 -0
- package/src/session/session-provider-boundary.ts +0 -1
- package/src/session/session-tools.ts +35 -1
- package/src/session/turn-recovery.ts +36 -19
- package/src/slash-commands/builtin-registry.ts +49 -7
- package/src/stt/asr-worker.ts +2 -37
- package/src/stt/sherpa-runtime.ts +71 -0
- package/src/task/executor.ts +6 -5
- package/src/thinking.ts +39 -0
- package/src/tools/bash.ts +43 -15
- package/src/tools/computer/exposure.ts +38 -0
- package/src/tools/computer/supervisor.ts +61 -13
- package/src/tools/computer/worker-entry.ts +28 -19
- package/src/tools/computer/worker.ts +3 -7
- package/src/tools/computer.ts +65 -10
- package/src/tools/gh-cache-invalidation.ts +2 -82
- package/src/tools/shell-tokenize.ts +83 -0
- package/src/tools/todo.ts +44 -17
|
@@ -30,7 +30,7 @@ import type {
|
|
|
30
30
|
ServiceTier,
|
|
31
31
|
SimpleStreamOptions,
|
|
32
32
|
} from "@oh-my-pi/pi-ai";
|
|
33
|
-
import { isUsageLimitOutcome, resolveModelServiceTier } from "@oh-my-pi/pi-ai";
|
|
33
|
+
import { isUsageLimitOutcome, resolveModelServiceTier, streamSimple } from "@oh-my-pi/pi-ai";
|
|
34
34
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
35
35
|
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
36
36
|
import { extractHttpStatusFromError, extractRetryHint, logger } from "@oh-my-pi/pi-utils";
|
|
@@ -93,8 +93,10 @@ import { formatSessionDumpText } from "./session-dump-format";
|
|
|
93
93
|
import type { CompactionEntry, SessionEntry } from "./session-entries";
|
|
94
94
|
import { formatSessionHistoryMarkdown } from "./session-history-format";
|
|
95
95
|
import type { SessionManager } from "./session-manager";
|
|
96
|
+
import { buildSessionMetadata } from "./session-metadata";
|
|
96
97
|
import type { YieldQueue } from "./yield-queue";
|
|
97
98
|
|
|
99
|
+
const ADVISOR_CODEX_SSE_MAX_ATTEMPTS = 1;
|
|
98
100
|
/** Advisor statistics for the advisor status command. */
|
|
99
101
|
export interface AdvisorStats {
|
|
100
102
|
configured: boolean;
|
|
@@ -328,6 +330,17 @@ export class SessionAdvisors {
|
|
|
328
330
|
this.#resetAdvisorSessionState();
|
|
329
331
|
}
|
|
330
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Rebind every live advisor to the active primary conversation's provider
|
|
335
|
+
* identity (session id, prompt-cache key, credential + metadata resolvers,
|
|
336
|
+
* telemetry). Invoked on every provider-session change — including branch
|
|
337
|
+
* paths that skip conversation restore — so advisors never keep emitting the
|
|
338
|
+
* previous conversation's session id/metadata (issue #6625).
|
|
339
|
+
*/
|
|
340
|
+
refreshProviderIdentity(): void {
|
|
341
|
+
for (const advisor of this.#advisors) this.#refreshAdvisorProviderIdentity(advisor);
|
|
342
|
+
}
|
|
343
|
+
|
|
331
344
|
/** Re-primes advisor transcript views after an in-conversation history rewrite. */
|
|
332
345
|
resetAllRuntimes(): void {
|
|
333
346
|
this.#resetAllAdvisorRuntimes();
|
|
@@ -391,6 +404,38 @@ export class SessionAdvisors {
|
|
|
391
404
|
this.#advisorInterruptImmuneTurnStart = this.#advisorPrimaryTurnsCompleted + 1;
|
|
392
405
|
}
|
|
393
406
|
|
|
407
|
+
/** Rebind one advisor to the active primary conversation's provider identity. */
|
|
408
|
+
#refreshAdvisorProviderIdentity(advisor: ActiveAdvisor): void {
|
|
409
|
+
const primaryProviderSessionId = this.#host.sessionId();
|
|
410
|
+
const providerSessionId = getOrCreateAdvisorProviderSessionId(
|
|
411
|
+
this.#advisorProviderSessionIds,
|
|
412
|
+
primaryProviderSessionId,
|
|
413
|
+
advisor.slug,
|
|
414
|
+
);
|
|
415
|
+
advisor.providerSessionId = providerSessionId;
|
|
416
|
+
advisor.agent.sessionId = providerSessionId;
|
|
417
|
+
advisor.agent.promptCacheKey = this.#host.agent.promptCacheKey ?? providerSessionId;
|
|
418
|
+
advisor.agent.getApiKey = requestModel => this.#host.modelRegistry.resolver(requestModel, providerSessionId);
|
|
419
|
+
advisor.agent.setMetadataResolver(
|
|
420
|
+
providerSessionId
|
|
421
|
+
? provider => buildSessionMetadata(providerSessionId, provider, this.#host.modelRegistry.authStorage)
|
|
422
|
+
: undefined,
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
const telemetry = advisor.agent.telemetry;
|
|
426
|
+
if (telemetry?.agent) {
|
|
427
|
+
advisor.agent.setTelemetry({
|
|
428
|
+
...telemetry,
|
|
429
|
+
agent: {
|
|
430
|
+
...telemetry.agent,
|
|
431
|
+
id: advisor.slug
|
|
432
|
+
? `${primaryProviderSessionId}-advisor-${advisor.slug}`
|
|
433
|
+
: `${primaryProviderSessionId}-advisor`,
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
394
439
|
/**
|
|
395
440
|
* Re-prime the advisor across a conversation boundary: `/new`, `/branch`,
|
|
396
441
|
* `/btw`, `/tree`, and session switch/resume. Beyond {@link AdvisorRuntime.reset}
|
|
@@ -644,6 +689,15 @@ export class SessionAdvisors {
|
|
|
644
689
|
tools: advisorToolMap,
|
|
645
690
|
allowNativeDelete: advisorCanMutateFiles,
|
|
646
691
|
});
|
|
692
|
+
const baseAdvisorStreamFn = this.#advisorStreamFn ?? streamSimple;
|
|
693
|
+
const advisorStreamFn: StreamFn = (requestModel, context, options) =>
|
|
694
|
+
baseAdvisorStreamFn(
|
|
695
|
+
requestModel,
|
|
696
|
+
context,
|
|
697
|
+
requestModel.api === "openai-codex-responses"
|
|
698
|
+
? { ...options, codexSseMaxAttempts: ADVISOR_CODEX_SSE_MAX_ATTEMPTS }
|
|
699
|
+
: options,
|
|
700
|
+
);
|
|
647
701
|
const advisorAgent = new Agent({
|
|
648
702
|
initialState: {
|
|
649
703
|
systemPrompt,
|
|
@@ -659,7 +713,7 @@ export class SessionAdvisors {
|
|
|
659
713
|
cwdResolver: () => this.#host.sessionManager.getCwd(),
|
|
660
714
|
preferWebsockets: this.#host.preferWebsockets,
|
|
661
715
|
getApiKey: requestModel => this.#host.modelRegistry.resolver(requestModel, advisorProviderSessionId),
|
|
662
|
-
streamFn:
|
|
716
|
+
streamFn: advisorStreamFn,
|
|
663
717
|
onPayload: this.#host.onPayload,
|
|
664
718
|
onResponse: this.#host.onResponse,
|
|
665
719
|
onSseEvent: this.#host.onSseEvent,
|
|
@@ -772,6 +826,7 @@ export class SessionAdvisors {
|
|
|
772
826
|
retryFallbackPendingSuccess: false,
|
|
773
827
|
signature,
|
|
774
828
|
};
|
|
829
|
+
this.#refreshAdvisorProviderIdentity(advisorRef);
|
|
775
830
|
this.#attachAdvisorRecorderFeed(advisorRef);
|
|
776
831
|
if (seedToCurrent) runtime.seedTo(this.#host.agent.state.messages.length);
|
|
777
832
|
this.#advisorStatuses.set(slug, { name: advisorName, status: "running" });
|
|
@@ -1247,7 +1302,15 @@ export class SessionAdvisors {
|
|
|
1247
1302
|
for (const candidate of candidates) {
|
|
1248
1303
|
const apiKey = await this.#host.modelRegistry.getApiKey(candidate, advisorProviderSessionId);
|
|
1249
1304
|
if (!apiKey) continue;
|
|
1250
|
-
|
|
1305
|
+
// The advisor overflow-compaction one-shot bypasses the advisor `Agent`,
|
|
1306
|
+
// so its installed metadata resolver never runs. Emit the same
|
|
1307
|
+
// `metadata.user_id` identity here (resolved per candidate provider,
|
|
1308
|
+
// after the session-sticky credential is selected) so summarization
|
|
1309
|
+
// requests carry the advisor session id like every other advisor call
|
|
1310
|
+
// (issue #6625).
|
|
1311
|
+
const advisorMetadata = advisorProviderSessionId
|
|
1312
|
+
? buildSessionMetadata(advisorProviderSessionId, candidate.provider, this.#host.modelRegistry.authStorage)
|
|
1313
|
+
: undefined;
|
|
1251
1314
|
try {
|
|
1252
1315
|
compactResult = await compact(
|
|
1253
1316
|
preparation,
|
|
@@ -1262,6 +1325,7 @@ export class SessionAdvisors {
|
|
|
1262
1325
|
tools: agent.state.tools,
|
|
1263
1326
|
sessionId: advisorProviderSessionId,
|
|
1264
1327
|
promptCacheKey: advisorProviderSessionId,
|
|
1328
|
+
metadata: advisorMetadata,
|
|
1265
1329
|
providerSessionState: this.#host.providerSessionState,
|
|
1266
1330
|
codexCompaction,
|
|
1267
1331
|
},
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { deriveClaudeDeviceId } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { getInstallId } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import type { AuthStorage } from "./auth-storage";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build the per-request `metadata` payload for the Anthropic provider, shaped
|
|
7
|
+
* like real Claude Code's `getAPIMetadata` output (`{ session_id, account_uuid,
|
|
8
|
+
* device_id }`) so the backend buckets requests under one session and attributes
|
|
9
|
+
* them to the authenticated OAuth account when available. Resolved at request
|
|
10
|
+
* time so token refreshes and login/logout transitions don't strand a stale
|
|
11
|
+
* account UUID in memory. `account_uuid` and `device_id` are omitted for
|
|
12
|
+
* non-Anthropic providers to avoid leaking the user's Claude identity to
|
|
13
|
+
* third-party APIs (including Anthropic-format-compatible proxies such as
|
|
14
|
+
* cloudflare-ai-gateway or gitlab-duo).
|
|
15
|
+
*
|
|
16
|
+
* Installed via `Agent#setMetadataResolver` on the main `AgentSession`, each
|
|
17
|
+
* subagent session, and the separately constructed advisor `Agent` — each with
|
|
18
|
+
* its own provider session id — so Main, subagent, and Advisor requests each
|
|
19
|
+
* expose a distinct, stable provider-facing session identity.
|
|
20
|
+
*
|
|
21
|
+
* `provider` is the target provider string (e.g. `"anthropic"`) and gates the
|
|
22
|
+
* `account_uuid` and `device_id` lookups — only `"anthropic"` requests carry them.
|
|
23
|
+
*
|
|
24
|
+
* `sessionId` is forwarded to the auth-storage session-sticky lookup so that
|
|
25
|
+
* multi-credential setups attribute to the same OAuth account used for the
|
|
26
|
+
* actual API request rather than always picking the first credential.
|
|
27
|
+
*
|
|
28
|
+
* `authStorage` is treated as optional so test fixtures that stub `modelRegistry`
|
|
29
|
+
* without a real storage layer still work; the resolver simply skips the lookup
|
|
30
|
+
* and emits `{ session_id }` alone, matching the no-OAuth-credential path.
|
|
31
|
+
*/
|
|
32
|
+
export function buildSessionMetadata(
|
|
33
|
+
sessionId: string,
|
|
34
|
+
provider: string,
|
|
35
|
+
authStorage: AuthStorage | undefined,
|
|
36
|
+
): Record<string, unknown> {
|
|
37
|
+
const userId: Record<string, string> = { session_id: sessionId };
|
|
38
|
+
// Only look up account_uuid when the request is going to Anthropic. Injecting
|
|
39
|
+
// a Claude OAuth account_uuid into requests bound for other providers (including
|
|
40
|
+
// Anthropic-format-compatible proxies like cloudflare-ai-gateway or gitlab-duo)
|
|
41
|
+
// would leak the user's Anthropic identity to unrelated third-party APIs.
|
|
42
|
+
if (provider === "anthropic") {
|
|
43
|
+
const accountUuid = authStorage?.getOAuthAccountId("anthropic", sessionId);
|
|
44
|
+
if (typeof accountUuid === "string" && accountUuid.length > 0) {
|
|
45
|
+
userId.account_uuid = accountUuid;
|
|
46
|
+
// Claude Code's `device_id` is a stable 64-hex account-scoped install
|
|
47
|
+
// identifier. Include both omp's persistent install id and the Claude
|
|
48
|
+
// account UUID so two accounts on the same install do not share a device.
|
|
49
|
+
userId.device_id = deriveClaudeDeviceId(getInstallId(), accountUuid);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { user_id: JSON.stringify(userId) };
|
|
53
|
+
}
|
|
@@ -17,6 +17,7 @@ import type { MemoryBackendStartOptions } from "../memory-backend/types";
|
|
|
17
17
|
import xdevMountNoticePrompt from "../prompts/system/xdev-mount-notice.md" with { type: "text" };
|
|
18
18
|
import { usesCodexTaskPrompt } from "../task/prompt-policy";
|
|
19
19
|
import { isMCPToolName, normalizeToolNames } from "../tools/builtin-names";
|
|
20
|
+
import { computerExposureMode } from "../tools/computer/exposure";
|
|
20
21
|
import { wrapToolWithMetaNotice } from "../tools/output-meta";
|
|
21
22
|
import { ToolAbortError, ToolError } from "../tools/tool-errors";
|
|
22
23
|
import { isMountableUnderXdev, type XdevRegistry } from "../tools/xdev";
|
|
@@ -294,6 +295,16 @@ export class SessionTools {
|
|
|
294
295
|
return usesCodexTaskPrompt(model) ? "task-policy:gpt-5.6" : "task-policy:default";
|
|
295
296
|
}
|
|
296
297
|
|
|
298
|
+
#logComputerState(message: string, enabled: boolean): void {
|
|
299
|
+
const model = this.#host.model();
|
|
300
|
+
logger.debug(message, {
|
|
301
|
+
enabled,
|
|
302
|
+
active: this.getEnabledToolNames().includes("computer"),
|
|
303
|
+
model: model ? formatModelString(model) : undefined,
|
|
304
|
+
exposure: computerExposureMode(model),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
297
308
|
/** Rebuilds model-dependent tool prompts after a model change. */
|
|
298
309
|
async syncAfterModelChange(previousEditMode: EditMode): Promise<void> {
|
|
299
310
|
const currentEditMode = this.resolveActiveEditMode();
|
|
@@ -303,6 +314,20 @@ export class SessionTools {
|
|
|
303
314
|
if (editModeChanged || modelChanged) {
|
|
304
315
|
await this.refreshBaseSystemPrompt();
|
|
305
316
|
}
|
|
317
|
+
const computerExpected = this.#host.settings.get("computer.enabled");
|
|
318
|
+
const computerActive = this.getEnabledToolNames().includes("computer");
|
|
319
|
+
if (computerExpected && !computerActive) {
|
|
320
|
+
const model = this.#host.model();
|
|
321
|
+
const modelName = model ? formatModelString(model) : "the current model";
|
|
322
|
+
logger.warn("Enabled computer tool missing after model change", { model: modelName });
|
|
323
|
+
this.#host.emitNotice(
|
|
324
|
+
"warning",
|
|
325
|
+
`Computer use remains enabled, but the computer tool is unavailable to ${modelName}.`,
|
|
326
|
+
"computer",
|
|
327
|
+
);
|
|
328
|
+
} else if (computerExpected) {
|
|
329
|
+
this.#logComputerState("Computer tool retained after model change", true);
|
|
330
|
+
}
|
|
306
331
|
}
|
|
307
332
|
|
|
308
333
|
/** Enabled MCP tools in their current presentation partition. */
|
|
@@ -713,16 +738,24 @@ export class SessionTools {
|
|
|
713
738
|
* tool (e.g. restricted child sessions have no factory).
|
|
714
739
|
*/
|
|
715
740
|
async setComputerToolEnabled(enabled: boolean): Promise<boolean> {
|
|
741
|
+
const logState = (): void => this.#logComputerState("Computer tool state changed", enabled);
|
|
716
742
|
const active = this.getEnabledToolNames();
|
|
717
743
|
if (!enabled) {
|
|
718
744
|
if (active.includes("computer")) {
|
|
719
745
|
await this.applyActiveToolsByName(active.filter(name => name !== "computer"));
|
|
720
746
|
}
|
|
747
|
+
logState();
|
|
721
748
|
return true;
|
|
722
749
|
}
|
|
723
750
|
if (!this.#toolRegistry.has("computer")) {
|
|
724
751
|
const tool = await this.#createComputerTool?.();
|
|
725
|
-
if (tool?.name !== "computer")
|
|
752
|
+
if (tool?.name !== "computer") {
|
|
753
|
+
const model = this.#host.model();
|
|
754
|
+
logger.warn("Computer tool could not be created", {
|
|
755
|
+
model: model ? formatModelString(model) : undefined,
|
|
756
|
+
});
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
726
759
|
const wrapped = this.#wrapRuntimeTool(tool);
|
|
727
760
|
this.#toolRegistry.set(wrapped.name, wrapped);
|
|
728
761
|
this.#builtInToolNames.add(wrapped.name);
|
|
@@ -730,6 +763,7 @@ export class SessionTools {
|
|
|
730
763
|
if (!active.includes("computer")) {
|
|
731
764
|
await this.applyActiveToolsByName([...active, "computer"]);
|
|
732
765
|
}
|
|
766
|
+
logState();
|
|
733
767
|
return true;
|
|
734
768
|
}
|
|
735
769
|
|
|
@@ -774,10 +774,6 @@ export class TurnRecovery {
|
|
|
774
774
|
return id;
|
|
775
775
|
}
|
|
776
776
|
|
|
777
|
-
#isGenericAbortSentinel(message: AssistantMessage): boolean {
|
|
778
|
-
return message.errorMessage === "Request was aborted" || message.errorMessage === "Request was aborted.";
|
|
779
|
-
}
|
|
780
|
-
|
|
781
777
|
/**
|
|
782
778
|
* Retry an empty, reason-less provider abort: a turn with no content that
|
|
783
779
|
* carries the generic sentinel (bare `abort()`), whether the provider
|
|
@@ -806,7 +802,9 @@ export class TurnRecovery {
|
|
|
806
802
|
|
|
807
803
|
const id = this.#classifyRetryMessage(message);
|
|
808
804
|
if (message.stopReason === "aborted" && AIError.is(id, AIError.Flag.Abort)) return true;
|
|
809
|
-
if (
|
|
805
|
+
if (message.errorMessage !== "Request was aborted" && message.errorMessage !== "Request was aborted.") {
|
|
806
|
+
return false;
|
|
807
|
+
}
|
|
810
808
|
|
|
811
809
|
message.errorId = AIError.create(AIError.Flag.Abort);
|
|
812
810
|
return true;
|
|
@@ -830,30 +828,48 @@ export class TurnRecovery {
|
|
|
830
828
|
}
|
|
831
829
|
|
|
832
830
|
/**
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
831
|
+
* Classify a reasonless abort or stream stall whose emitted tool calls all
|
|
832
|
+
* have results. The failed assistant/tool-result pair stays in context so
|
|
833
|
+
* continuation cannot replay completed side effects; synthetic results tell
|
|
834
|
+
* the next turn that an unexecuted call must be reissued.
|
|
837
835
|
*/
|
|
838
|
-
|
|
839
|
-
if (message.stopReason !== "error" || !message.errorMessage?.toLowerCase().includes("stream stall")) {
|
|
840
|
-
return false;
|
|
841
|
-
}
|
|
836
|
+
classifyResolvedInterruptedToolTurn(message: AssistantMessage): "reasonless-abort" | "stream-stall" | undefined {
|
|
842
837
|
const id = this.#classifyRetryMessage(message);
|
|
843
|
-
|
|
844
|
-
|
|
838
|
+
const genericAbort =
|
|
839
|
+
message.errorMessage === "Request was aborted" || message.errorMessage === "Request was aborted.";
|
|
840
|
+
const reasonlessAbort =
|
|
841
|
+
(message.stopReason === "aborted" || message.stopReason === "error") &&
|
|
842
|
+
!this.#host.abortInProgress() &&
|
|
843
|
+
!this.#host.isDisposed() &&
|
|
844
|
+
!this.#host.streamingEditAbortTriggered() &&
|
|
845
|
+
((message.stopReason === "aborted" && AIError.is(id, AIError.Flag.Abort)) || genericAbort);
|
|
846
|
+
const streamStall =
|
|
847
|
+
message.stopReason === "error" &&
|
|
848
|
+
message.errorMessage?.toLowerCase().includes("stream stall") === true &&
|
|
849
|
+
AIError.retriable(id);
|
|
850
|
+
if (!reasonlessAbort && !streamStall) return undefined;
|
|
851
|
+
if (reasonlessAbort && genericAbort) message.errorId = AIError.create(AIError.Flag.Abort);
|
|
852
|
+
|
|
853
|
+
// The Cursor server-execution marker gate applies only to the stream-stall
|
|
854
|
+
// path: an unmarked/unresolved Cursor block there means the server has not
|
|
855
|
+
// finished executing, so resuming would race it. A reasonless abort instead
|
|
856
|
+
// ends the turn and the agent loop pairs every un-run call (Cursor's unmarked
|
|
857
|
+
// `todo`/MCP blocks included) with a synthetic `executed: false` result, so
|
|
858
|
+
// the tool-result reconciliation below is the safety gate and the marker is
|
|
859
|
+
// irrelevant.
|
|
845
860
|
const resolvedToolCallIds: string[] = [];
|
|
846
861
|
for (const block of message.content) {
|
|
847
862
|
if (block.type !== "toolCall") continue;
|
|
848
863
|
if (
|
|
864
|
+
streamStall &&
|
|
849
865
|
message.provider === "cursor" &&
|
|
850
866
|
(!(kCursorExecResolved in block) || block[kCursorExecResolved] !== true)
|
|
851
867
|
) {
|
|
852
|
-
return
|
|
868
|
+
return undefined;
|
|
853
869
|
}
|
|
854
870
|
resolvedToolCallIds.push(block.id);
|
|
855
871
|
}
|
|
856
|
-
if (resolvedToolCallIds.length === 0) return
|
|
872
|
+
if (resolvedToolCallIds.length === 0) return undefined;
|
|
857
873
|
|
|
858
874
|
const messages = this.#host.agent.state.messages;
|
|
859
875
|
let assistantIndex = -1;
|
|
@@ -864,14 +880,15 @@ export class TurnRecovery {
|
|
|
864
880
|
break;
|
|
865
881
|
}
|
|
866
882
|
}
|
|
867
|
-
if (assistantIndex < 0) return
|
|
883
|
+
if (assistantIndex < 0) return undefined;
|
|
868
884
|
|
|
869
885
|
const unresolvedToolCallIds = new Set(resolvedToolCallIds);
|
|
870
886
|
for (let i = assistantIndex + 1; i < messages.length; i++) {
|
|
871
887
|
const candidate = messages[i];
|
|
872
888
|
if (candidate.role === "toolResult") unresolvedToolCallIds.delete(candidate.toolCallId);
|
|
873
889
|
}
|
|
874
|
-
|
|
890
|
+
if (unresolvedToolCallIds.size > 0) return undefined;
|
|
891
|
+
return reasonlessAbort ? "reasonless-abort" : "stream-stall";
|
|
875
892
|
}
|
|
876
893
|
/**
|
|
877
894
|
* Retried turns remove the failed assistant message from active context.
|
|
@@ -7,7 +7,12 @@ import { APP_NAME, getProjectDir, setProjectDir } from "@oh-my-pi/pi-utils";
|
|
|
7
7
|
import { reset as resetCapabilities } from "../capability";
|
|
8
8
|
import { COLLAB_GUEST_ALLOWED_COMMANDS, CollabGuestLink } from "../collab/guest";
|
|
9
9
|
import { CollabHost } from "../collab/host";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
expandRoleAlias,
|
|
12
|
+
formatModelString,
|
|
13
|
+
getModelMatchPreferences,
|
|
14
|
+
resolveCliModel,
|
|
15
|
+
} from "../config/model-resolver";
|
|
11
16
|
import { applyProviderGlobalsFromSettings } from "../config/provider-globals";
|
|
12
17
|
import type { SettingPath, SettingValue } from "../config/settings";
|
|
13
18
|
import { settings } from "../config/settings";
|
|
@@ -37,6 +42,8 @@ import type { SessionOAuthAccountList } from "../session/agent-session-types";
|
|
|
37
42
|
import { COMPACT_MODES, parseCompactArgs } from "../session/compact-modes";
|
|
38
43
|
import { resolveResumableSession } from "../session/session-listing";
|
|
39
44
|
import { formatShakeSummary, type ShakeMode } from "../session/shake-types";
|
|
45
|
+
import type { ComputerTool } from "../tools/computer";
|
|
46
|
+
import { computerExposureMode } from "../tools/computer/exposure";
|
|
40
47
|
import { expandTilde, resolveToCwd } from "../tools/path-utils";
|
|
41
48
|
import { urlHyperlinkAlways } from "../tui";
|
|
42
49
|
import {
|
|
@@ -90,9 +97,41 @@ function formatFastModeStatus(session: AgentSession): string {
|
|
|
90
97
|
return session.isFastModeEnabled() ? "on" : "off";
|
|
91
98
|
}
|
|
92
99
|
|
|
93
|
-
/**
|
|
100
|
+
/** Detailed, session-effective `/computer status` diagnostics. */
|
|
94
101
|
function formatComputerUseStatus(session: AgentSession): string {
|
|
95
|
-
|
|
102
|
+
const enabled = session.settings.get("computer.enabled");
|
|
103
|
+
const active = session.getEnabledToolNames().includes("computer");
|
|
104
|
+
const model = session.model;
|
|
105
|
+
const modelName = model ? formatModelString(model) : "none";
|
|
106
|
+
const exposure = !enabled || !active ? "not exposed" : computerExposureMode(model);
|
|
107
|
+
const toolState = active ? "active" : enabled ? "unavailable" : "inactive";
|
|
108
|
+
const configured = {
|
|
109
|
+
backend: session.settings.get("computer.backend"),
|
|
110
|
+
display: session.settings.get("computer.display"),
|
|
111
|
+
maxWidth: session.settings.get("computer.maxWidth"),
|
|
112
|
+
maxHeight: session.settings.get("computer.maxHeight"),
|
|
113
|
+
};
|
|
114
|
+
const computerTool = session.getToolByName("computer") as Pick<ComputerTool, "effectiveConfiguration"> | undefined;
|
|
115
|
+
const effective = computerTool?.effectiveConfiguration ?? configured;
|
|
116
|
+
const configurationChanged =
|
|
117
|
+
effective.backend !== configured.backend ||
|
|
118
|
+
effective.display !== configured.display ||
|
|
119
|
+
effective.maxWidth !== configured.maxWidth ||
|
|
120
|
+
effective.maxHeight !== configured.maxHeight;
|
|
121
|
+
return [
|
|
122
|
+
`Computer use: ${enabled ? "enabled" : "disabled"}`,
|
|
123
|
+
`tool: ${toolState}`,
|
|
124
|
+
`backend: ${effective.backend}`,
|
|
125
|
+
`display: ${effective.display}`,
|
|
126
|
+
`capture: ${effective.maxWidth}×${effective.maxHeight}`,
|
|
127
|
+
...(configurationChanged
|
|
128
|
+
? [
|
|
129
|
+
`next-session settings: backend=${configured.backend}, display=${configured.display}, capture=${configured.maxWidth}×${configured.maxHeight}`,
|
|
130
|
+
]
|
|
131
|
+
: []),
|
|
132
|
+
`model: ${modelName}`,
|
|
133
|
+
`exposure: ${exposure}`,
|
|
134
|
+
].join(" · ");
|
|
96
135
|
}
|
|
97
136
|
|
|
98
137
|
/**
|
|
@@ -107,7 +146,9 @@ async function applyComputerUseToggle(session: AgentSession, enable: boolean): P
|
|
|
107
146
|
return "Computer use is unavailable in this session.";
|
|
108
147
|
}
|
|
109
148
|
session.settings.override("computer.enabled", enable);
|
|
110
|
-
return
|
|
149
|
+
return enable
|
|
150
|
+
? `Computer use enabled for this session. ${formatComputerUseStatus(session)}`
|
|
151
|
+
: "Computer use disabled for this session.";
|
|
111
152
|
}
|
|
112
153
|
|
|
113
154
|
const AUTOCOMPLETE_DETAIL_LIMIT = 48;
|
|
@@ -573,11 +614,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
573
614
|
{ name: "status", description: "Show computer use status" },
|
|
574
615
|
],
|
|
575
616
|
allowArgs: true,
|
|
576
|
-
getTuiAutocompleteDescription: runtime =>
|
|
617
|
+
getTuiAutocompleteDescription: runtime =>
|
|
618
|
+
`Computer: ${runtime.ctx.session.settings.get("computer.enabled") ? "on" : "off"}`,
|
|
577
619
|
handle: async (command, runtime) => {
|
|
578
620
|
const arg = command.args.trim().toLowerCase();
|
|
579
621
|
if (arg === "status") {
|
|
580
|
-
await runtime.output(
|
|
622
|
+
await runtime.output(formatComputerUseStatus(runtime.session));
|
|
581
623
|
return commandConsumed();
|
|
582
624
|
}
|
|
583
625
|
if (!arg || arg === "toggle" || arg === "on" || arg === "off") {
|
|
@@ -590,7 +632,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
590
632
|
handleTui: async (command, runtime) => {
|
|
591
633
|
const arg = command.args.trim().toLowerCase();
|
|
592
634
|
if (arg === "status") {
|
|
593
|
-
runtime.ctx.showStatus(
|
|
635
|
+
runtime.ctx.showStatus(formatComputerUseStatus(runtime.ctx.session));
|
|
594
636
|
runtime.ctx.editor.setText("");
|
|
595
637
|
return;
|
|
596
638
|
}
|
package/src/stt/asr-worker.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
type SttModelKey,
|
|
36
36
|
type TransformersSttModelSpec,
|
|
37
37
|
} from "./models";
|
|
38
|
+
import { loadSourceSherpaRuntime, type SherpaOfflineRecognizer, type SherpaRuntime } from "./sherpa-runtime";
|
|
38
39
|
|
|
39
40
|
const ASR_TASK = "automatic-speech-recognition";
|
|
40
41
|
const SHERPA_PACKAGE = "sherpa-onnx-node";
|
|
@@ -50,7 +51,6 @@ const HF_RESOLVE_BASE = "https://huggingface.co";
|
|
|
50
51
|
// Coalesce download progress so streaming a multi-hundred-MB model file doesn't
|
|
51
52
|
// flood the IPC channel with one event per chunk.
|
|
52
53
|
const PROGRESS_EMIT_BYTES = 4_000_000;
|
|
53
|
-
const sourceRequire = createRequire(import.meta.url);
|
|
54
54
|
|
|
55
55
|
const sttModelDevicePreference = resolveTinyModelDevicePreference();
|
|
56
56
|
const sttModelDtypeOverride = resolveTinyModelDtypeOverride();
|
|
@@ -90,41 +90,6 @@ interface TransformersRuntime {
|
|
|
90
90
|
) => Promise<AutomaticSpeechRecognitionPipeline>;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
/** Recognition result returned by `sherpa-onnx-node`'s offline recognizer. */
|
|
94
|
-
interface SherpaOfflineResult {
|
|
95
|
-
text?: string;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** A sherpa-onnx offline stream that accepts a single waveform before decoding. */
|
|
99
|
-
interface SherpaOfflineStream {
|
|
100
|
-
acceptWaveform(audio: { samples: Float32Array; sampleRate: number }): void;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
interface SherpaOfflineRecognizer {
|
|
104
|
-
createStream(): SherpaOfflineStream;
|
|
105
|
-
decodeAsync(stream: SherpaOfflineStream): Promise<SherpaOfflineResult>;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Offline recognizer config passed to `sherpa-onnx-node` (transducer family). */
|
|
109
|
-
interface SherpaOfflineConfig {
|
|
110
|
-
modelConfig: {
|
|
111
|
-
transducer: { encoder: string; decoder: string; joiner: string };
|
|
112
|
-
tokens: string;
|
|
113
|
-
modelType: string;
|
|
114
|
-
numThreads: number;
|
|
115
|
-
provider: string;
|
|
116
|
-
debug: number;
|
|
117
|
-
};
|
|
118
|
-
decodingMethod: string;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Subset of the native `sherpa-onnx-node` module surface we use. */
|
|
122
|
-
interface SherpaRuntime {
|
|
123
|
-
OfflineRecognizer: {
|
|
124
|
-
createAsync(config: SherpaOfflineConfig): Promise<SherpaOfflineRecognizer>;
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
93
|
/** A warm model plus the engine that loaded it; cached per tier key. */
|
|
129
94
|
type LoadedModel =
|
|
130
95
|
| { engine: "transformers"; pipeline: AutomaticSpeechRecognitionPipeline }
|
|
@@ -182,7 +147,7 @@ function getSherpaRuntimeDir(): string {
|
|
|
182
147
|
*/
|
|
183
148
|
function loadSherpaRuntime(transport: SttTransport, requestId: string, modelKey: SttModelKey): Promise<SherpaRuntime> {
|
|
184
149
|
return sherpaRuntime.load(async () => {
|
|
185
|
-
if (!isCompiledBinary()) return
|
|
150
|
+
if (!isCompiledBinary()) return loadSourceSherpaRuntime(import.meta.url);
|
|
186
151
|
const runtimeDir = await ensureRuntimeInstalled({
|
|
187
152
|
runtimeDir: getSherpaRuntimeDir(),
|
|
188
153
|
install: { dependencies: { [SHERPA_PACKAGE]: getSherpaVersionSpec() } },
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import { resolveRuntimeModule } from "@oh-my-pi/pi-utils";
|
|
4
|
+
|
|
5
|
+
const SHERPA_PACKAGE = "sherpa-onnx-node";
|
|
6
|
+
|
|
7
|
+
interface SherpaOfflineResult {
|
|
8
|
+
text?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SherpaOfflineStream {
|
|
12
|
+
acceptWaveform(audio: { samples: Float32Array; sampleRate: number }): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface SherpaOfflineConfig {
|
|
16
|
+
modelConfig: {
|
|
17
|
+
transducer: { encoder: string; decoder: string; joiner: string };
|
|
18
|
+
tokens: string;
|
|
19
|
+
modelType: string;
|
|
20
|
+
numThreads: number;
|
|
21
|
+
provider: string;
|
|
22
|
+
debug: number;
|
|
23
|
+
};
|
|
24
|
+
decodingMethod: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A sherpa-onnx recognizer instance used by the STT worker. */
|
|
28
|
+
export interface SherpaOfflineRecognizer {
|
|
29
|
+
createStream(): SherpaOfflineStream;
|
|
30
|
+
decodeAsync(stream: SherpaOfflineStream): Promise<SherpaOfflineResult>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The native sherpa-onnx module surface used by the STT worker. */
|
|
34
|
+
export interface SherpaRuntime {
|
|
35
|
+
OfflineRecognizer: {
|
|
36
|
+
createAsync(config: SherpaOfflineConfig): Promise<SherpaOfflineRecognizer>;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Loads the nearest working source-workspace sherpa wrapper, including hoisted fallbacks. */
|
|
41
|
+
export function loadSourceSherpaRuntime(sourceUrl: string): SherpaRuntime {
|
|
42
|
+
const sourceRequire = createRequire(sourceUrl);
|
|
43
|
+
const nearestEntry = sourceRequire.resolve(SHERPA_PACKAGE);
|
|
44
|
+
try {
|
|
45
|
+
return createRequire(nearestEntry)(nearestEntry);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (!(error instanceof Error && error.message.startsWith("Could not find sherpa-onnx-node. Tried"))) {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
const platform = os.platform();
|
|
51
|
+
const platformPackage = `sherpa-onnx-${platform === "win32" ? "win" : platform}-${os.arch()}`;
|
|
52
|
+
for (const nodeModules of sourceRequire.resolve.paths(SHERPA_PACKAGE) ?? []) {
|
|
53
|
+
if (!resolveRuntimeModule(nodeModules, platformPackage)) continue;
|
|
54
|
+
const entry = resolveRuntimeModule(nodeModules, SHERPA_PACKAGE);
|
|
55
|
+
if (!entry || entry === nearestEntry) continue;
|
|
56
|
+
try {
|
|
57
|
+
return createRequire(entry)(entry);
|
|
58
|
+
} catch (candidateError) {
|
|
59
|
+
if (
|
|
60
|
+
!(
|
|
61
|
+
candidateError instanceof Error &&
|
|
62
|
+
candidateError.message.startsWith("Could not find sherpa-onnx-node. Tried")
|
|
63
|
+
)
|
|
64
|
+
) {
|
|
65
|
+
throw candidateError;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/task/executor.ts
CHANGED
|
@@ -46,7 +46,7 @@ import type { AuthStorage } from "../session/auth-storage";
|
|
|
46
46
|
import { SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../session/messages";
|
|
47
47
|
import { SessionManager } from "../session/session-manager";
|
|
48
48
|
import { truncateTail } from "../session/streaming-output";
|
|
49
|
-
import { type ConfiguredThinkingLevel, resolveTaskEffortLevel, type TaskEffort } from "../thinking";
|
|
49
|
+
import { type ConfiguredThinkingLevel, prewalkWouldBeNoop, resolveTaskEffortLevel, type TaskEffort } from "../thinking";
|
|
50
50
|
import type { ContextFileEntry, ToolSession } from "../tools";
|
|
51
51
|
import { resolveEvalBackends } from "../tools/eval-backends";
|
|
52
52
|
import { isIrcEnabled } from "../tools/hub";
|
|
@@ -2683,10 +2683,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2683
2683
|
pattern: prewalkPattern,
|
|
2684
2684
|
warning: resolvedPrewalk.warning,
|
|
2685
2685
|
});
|
|
2686
|
-
} else if (model
|
|
2687
|
-
//
|
|
2688
|
-
// the plan/checklist nudges — skip.
|
|
2689
|
-
|
|
2686
|
+
} else if (prewalkWouldBeNoop(model, effectiveThinkingLevel, target, resolvedPrewalk.thinkingLevel)) {
|
|
2687
|
+
// Same model AND same effective thinking level: switching would only
|
|
2688
|
+
// inject the plan/checklist nudges for no gain — skip. An effort-only
|
|
2689
|
+
// delta on the same model still arms (it is a real cheapening hand-off).
|
|
2690
|
+
logger.debug("Subagent prewalk target matches starting model and thinking level; skipping prewalk", {
|
|
2690
2691
|
agent: agent.name,
|
|
2691
2692
|
pattern: prewalkPattern,
|
|
2692
2693
|
});
|