@oh-my-pi/pi-coding-agent 17.0.4 → 17.0.5
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 +86 -43
- package/dist/cli.js +3540 -3521
- package/dist/types/advisor/advise-tool.d.ts +3 -0
- package/dist/types/advisor/runtime.d.ts +7 -1
- package/dist/types/async/job-manager.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +7 -0
- package/dist/types/config/model-resolver.d.ts +8 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/dap/client.d.ts +10 -0
- package/dist/types/dap/types.d.ts +6 -5
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +16 -0
- package/dist/types/launch/spawn-options.d.ts +10 -0
- package/dist/types/launch/spawn-options.test.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +2 -3
- package/dist/types/modes/components/status-line/types.d.ts +3 -1
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/components/transcript-container.d.ts +4 -3
- package/dist/types/modes/components/tree-selector.d.ts +6 -2
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/print-mode.d.ts +4 -0
- package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +19 -1
- package/dist/types/session/messages.d.ts +6 -0
- package/dist/types/tools/browser/aria/aria-snapshot.d.ts +17 -7
- package/dist/types/tools/browser/cmux/socket-client.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -0
- package/dist/types/tools/gh.d.ts +5 -3
- package/dist/types/tools/hub/index.d.ts +1 -1
- package/dist/types/tools/hub/jobs.d.ts +1 -0
- package/dist/types/tools/hub/types.d.ts +2 -0
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/dist/types/utils/git.d.ts +33 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +150 -0
- package/src/advisor/advise-tool.ts +4 -0
- package/src/advisor/runtime.ts +38 -14
- package/src/async/job-manager.ts +3 -0
- package/src/cli/bench-cli.ts +8 -2
- package/src/cli/dry-balance-cli.ts +1 -0
- package/src/config/model-registry.ts +89 -8
- package/src/config/model-resolver.ts +78 -14
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings.ts +3 -1
- package/src/dap/client.ts +168 -1
- package/src/dap/config.ts +51 -1
- package/src/dap/session.ts +575 -234
- package/src/dap/types.ts +6 -5
- package/src/discovery/agents.ts +2 -2
- package/src/extensibility/extensions/runner.ts +6 -4
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +26 -0
- package/src/launch/broker.ts +34 -31
- package/src/launch/client.ts +7 -1
- package/src/launch/spawn-options.test.ts +31 -0
- package/src/launch/spawn-options.ts +17 -0
- package/src/lsp/types.ts +5 -1
- package/src/main.ts +17 -4
- package/src/mcp/transports/stdio.test.ts +9 -1
- package/src/modes/components/ask-dialog.ts +137 -73
- package/src/modes/components/status-line/component.ts +2 -2
- package/src/modes/components/status-line/segments.ts +20 -3
- package/src/modes/components/status-line/types.ts +3 -1
- package/src/modes/components/tool-execution.ts +5 -0
- package/src/modes/components/transcript-container.ts +23 -122
- package/src/modes/components/tree-selector.ts +10 -4
- package/src/modes/controllers/event-controller.ts +1 -2
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +29 -8
- package/src/modes/interactive-mode.ts +40 -8
- package/src/modes/noninteractive-dispose.test.ts +12 -1
- package/src/modes/print-mode.ts +21 -9
- package/src/modes/rpc/rpc-input.ts +38 -0
- package/src/modes/rpc/rpc-mode.ts +7 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +3 -1
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +3 -2
- package/src/prompts/tools/browser.md +3 -2
- package/src/prompts/tools/debug.md +2 -7
- package/src/prompts/tools/github.md +6 -1
- package/src/prompts/tools/hub.md +4 -2
- package/src/prompts/tools/task-async-contract.md +1 -0
- package/src/prompts/tools/task.md +9 -2
- package/src/sdk.ts +38 -6
- package/src/session/agent-session.ts +395 -162
- package/src/session/messages.test.ts +91 -0
- package/src/session/messages.ts +248 -110
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/task/executor.ts +59 -33
- package/src/task/index.ts +46 -9
- package/src/task/worktree.ts +10 -0
- package/src/tools/browser/aria/aria-snapshot.ts +36 -8
- package/src/tools/browser/cmux/cmux-tab.ts +2 -1
- package/src/tools/browser/cmux/socket-client.ts +139 -3
- package/src/tools/browser/run-cancellation.ts +4 -0
- package/src/tools/browser/tab-protocol.ts +2 -0
- package/src/tools/browser/tab-supervisor.ts +21 -11
- package/src/tools/browser/tab-worker.ts +199 -33
- package/src/tools/debug.ts +3 -0
- package/src/tools/gh.ts +40 -2
- package/src/tools/hub/index.ts +4 -1
- package/src/tools/hub/jobs.ts +42 -1
- package/src/tools/hub/types.ts +2 -0
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/utils/git.ts +237 -0
- package/src/web/search/index.ts +9 -5
- package/src/web/search/providers/codex.ts +195 -99
package/dist/types/tools/gh.d.ts
CHANGED
|
@@ -7,9 +7,10 @@ import type { OutputMeta } from "./output-meta.js";
|
|
|
7
7
|
/** Runs `gh --json` for issue data, retrying without optional stateReason on older gh releases. */
|
|
8
8
|
export declare function githubIssueJsonWithStateReasonFallback<T>(cwd: string, args: readonly string[], signal: AbortSignal | undefined, options?: git.GhCommandOptions): Promise<T>;
|
|
9
9
|
declare const githubSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
10
|
-
op: "pr_checkout" | "pr_create" | "pr_push" | "repo_view" | "run_watch" | "search_code" | "search_commits" | "search_issues" | "search_prs" | "search_repos";
|
|
10
|
+
op: "file_read" | "pr_checkout" | "pr_create" | "pr_push" | "repo_view" | "run_watch" | "search_code" | "search_commits" | "search_issues" | "search_prs" | "search_repos";
|
|
11
11
|
repo?: string | undefined;
|
|
12
12
|
branch?: string | undefined;
|
|
13
|
+
path?: string | undefined;
|
|
13
14
|
pr?: string | string[] | undefined;
|
|
14
15
|
force?: boolean | undefined;
|
|
15
16
|
forceWithLease?: boolean | undefined;
|
|
@@ -219,14 +220,15 @@ export declare class GithubTool implements AgentTool<typeof githubSchema, GhTool
|
|
|
219
220
|
private readonly session;
|
|
220
221
|
readonly name = "github";
|
|
221
222
|
readonly approval: (args: unknown) => ToolApprovalDecision;
|
|
222
|
-
readonly summary = "Interact with GitHub
|
|
223
|
+
readonly summary = "Interact with GitHub repositories, files, pull requests, and Actions";
|
|
223
224
|
readonly loadMode = "discoverable";
|
|
224
225
|
readonly label = "GitHub";
|
|
225
226
|
readonly description: string;
|
|
226
227
|
readonly parameters: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
227
|
-
op: "pr_checkout" | "pr_create" | "pr_push" | "repo_view" | "run_watch" | "search_code" | "search_commits" | "search_issues" | "search_prs" | "search_repos";
|
|
228
|
+
op: "file_read" | "pr_checkout" | "pr_create" | "pr_push" | "repo_view" | "run_watch" | "search_code" | "search_commits" | "search_issues" | "search_prs" | "search_repos";
|
|
228
229
|
repo?: string | undefined;
|
|
229
230
|
branch?: string | undefined;
|
|
231
|
+
path?: string | undefined;
|
|
230
232
|
pr?: string | string[] | undefined;
|
|
231
233
|
force?: boolean | undefined;
|
|
232
234
|
forceWithLease?: boolean | undefined;
|
|
@@ -117,7 +117,7 @@ export declare class HubTool implements AgentTool<typeof hubSchema, HubDetails>
|
|
|
117
117
|
timeout?: number | undefined;
|
|
118
118
|
}, {}>;
|
|
119
119
|
readonly strict = true;
|
|
120
|
-
readonly interruptible
|
|
120
|
+
readonly interruptible: (params: Partial<HubParams>) => boolean;
|
|
121
121
|
readonly loadMode = "essential";
|
|
122
122
|
readonly examples: readonly ToolExample<typeof hubSchema.infer>[];
|
|
123
123
|
constructor(session: ToolSession);
|
|
@@ -30,6 +30,8 @@ export interface JobSnapshot {
|
|
|
30
30
|
status: "running" | "completed" | "failed" | "cancelled";
|
|
31
31
|
label: string;
|
|
32
32
|
durationMs: number;
|
|
33
|
+
/** Effective task model selector, including an explicit reasoning suffix when configured. */
|
|
34
|
+
resolvedModel?: string;
|
|
33
35
|
resultText?: string;
|
|
34
36
|
errorText?: string;
|
|
35
37
|
}
|
|
@@ -219,6 +219,39 @@ export declare function fetch(cwd: string, remote: string, source: string, targe
|
|
|
219
219
|
export declare function readTree(cwd: string, treeish: string, options?: Pick<CommandOptions, "env" | "signal">): Promise<void>;
|
|
220
220
|
/** Write the current index as a tree and return its object id. */
|
|
221
221
|
export declare function writeTree(cwd: string, options?: Pick<CommandOptions, "env" | "signal">): Promise<string>;
|
|
222
|
+
/** Outcome of {@link detachGitDir}. */
|
|
223
|
+
export type DetachGitDirResult =
|
|
224
|
+
/** `worktreeRoot` had no `.git`; nothing to detach. */
|
|
225
|
+
"no-git"
|
|
226
|
+
/** `.git` already resolves to an independent object DB — left untouched. */
|
|
227
|
+
| "independent"
|
|
228
|
+
/** Detached into a standalone repo borrowing `sourceCommonDir`'s objects. */
|
|
229
|
+
| "detached";
|
|
230
|
+
/**
|
|
231
|
+
* Sever a copied/mounted working tree from the git metadata it shares with a
|
|
232
|
+
* source checkout, turning it into a standalone repository that borrows the
|
|
233
|
+
* source object database through `objects/info/alternates`.
|
|
234
|
+
*
|
|
235
|
+
* Isolation backends (reflink/apfs/btrfs/rcopy…) materialise `merged` by
|
|
236
|
+
* copying `worktreeRoot` byte-for-byte. When `worktreeRoot` is a **linked git
|
|
237
|
+
* worktree** its `.git` is a pointer file (`gitdir: …/worktrees/<name>`), so
|
|
238
|
+
* the copy still resolves HEAD/index/refs through the source repo — a task's
|
|
239
|
+
* `git checkout`/`commit` inside the isolation then mutates the *parent*
|
|
240
|
+
* checkout. The rcopy `git worktree add` path leaks the other way: task
|
|
241
|
+
* branches land in the shared ref namespace and stack on each other.
|
|
242
|
+
*
|
|
243
|
+
* After detaching, the working tree keeps its files verbatim while:
|
|
244
|
+
* - HEAD, refs, and the index are frozen to the snapshot at call time;
|
|
245
|
+
* - all commits/branches the task creates stay private to the isolation;
|
|
246
|
+
* - objects resolve against `sourceCommonDir` via alternates, so history reads
|
|
247
|
+
* and later `git fetch <merged>` object transfer keep working;
|
|
248
|
+
* - the source checkout's HEAD, branch, index, and working tree are untouched.
|
|
249
|
+
*
|
|
250
|
+
* A full-copy `.git` (non-worktree source) already owns its object DB and is
|
|
251
|
+
* returned as `"independent"` without modification. `worktreeRoot` without a
|
|
252
|
+
* `.git` yields `"no-git"`.
|
|
253
|
+
*/
|
|
254
|
+
export declare function detachGitDir(worktreeRoot: string, sourceCommonDir: string): Promise<DetachGitDirResult>;
|
|
222
255
|
/** Run `git show` on a revision. */
|
|
223
256
|
export declare const show: ((cwd: string, revision: string, options?: {
|
|
224
257
|
format?: string;
|
|
@@ -25,7 +25,7 @@ export interface CodexSearchParams {
|
|
|
25
25
|
*/
|
|
26
26
|
export declare function searchCodex(params: SearchParams): Promise<SearchResponse>;
|
|
27
27
|
/**
|
|
28
|
-
* Checks
|
|
28
|
+
* Checks whether Codex web search has an API key or OAuth credential.
|
|
29
29
|
*/
|
|
30
30
|
export declare function hasCodexSearch(authStorage: AuthStorage): Promise<boolean>;
|
|
31
31
|
/** Search provider for OpenAI Codex web search. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "17.0.
|
|
4
|
+
"version": "17.0.5",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -52,17 +52,17 @@
|
|
|
52
52
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
53
53
|
"@babel/parser": "^7.29.7",
|
|
54
54
|
"@mozilla/readability": "^0.6.0",
|
|
55
|
-
"@oh-my-pi/hashline": "17.0.
|
|
56
|
-
"@oh-my-pi/omp-stats": "17.0.
|
|
57
|
-
"@oh-my-pi/pi-agent-core": "17.0.
|
|
58
|
-
"@oh-my-pi/pi-ai": "17.0.
|
|
59
|
-
"@oh-my-pi/pi-catalog": "17.0.
|
|
60
|
-
"@oh-my-pi/pi-mnemopi": "17.0.
|
|
61
|
-
"@oh-my-pi/pi-natives": "17.0.
|
|
62
|
-
"@oh-my-pi/pi-tui": "17.0.
|
|
63
|
-
"@oh-my-pi/pi-utils": "17.0.
|
|
64
|
-
"@oh-my-pi/pi-wire": "17.0.
|
|
65
|
-
"@oh-my-pi/snapcompact": "17.0.
|
|
55
|
+
"@oh-my-pi/hashline": "17.0.5",
|
|
56
|
+
"@oh-my-pi/omp-stats": "17.0.5",
|
|
57
|
+
"@oh-my-pi/pi-agent-core": "17.0.5",
|
|
58
|
+
"@oh-my-pi/pi-ai": "17.0.5",
|
|
59
|
+
"@oh-my-pi/pi-catalog": "17.0.5",
|
|
60
|
+
"@oh-my-pi/pi-mnemopi": "17.0.5",
|
|
61
|
+
"@oh-my-pi/pi-natives": "17.0.5",
|
|
62
|
+
"@oh-my-pi/pi-tui": "17.0.5",
|
|
63
|
+
"@oh-my-pi/pi-utils": "17.0.5",
|
|
64
|
+
"@oh-my-pi/pi-wire": "17.0.5",
|
|
65
|
+
"@oh-my-pi/snapcompact": "17.0.5",
|
|
66
66
|
"@opentelemetry/api": "^1.9.1",
|
|
67
67
|
"@opentelemetry/api-logs": "^0.220.0",
|
|
68
68
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "bun:test";
|
|
|
2
2
|
import type { AgentMessage, AgentTelemetryConfig } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
5
|
+
import { kCursorExecResolved } from "@oh-my-pi/pi-ai/utils/block-symbols";
|
|
5
6
|
import type { TUI } from "@oh-my-pi/pi-tui";
|
|
6
7
|
import { type } from "arktype";
|
|
7
8
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
@@ -519,6 +520,58 @@ describe("advisor", () => {
|
|
|
519
520
|
expect(message.content).toBe(originalContent);
|
|
520
521
|
});
|
|
521
522
|
|
|
523
|
+
it("keeps advise when Cursor emits exec-resolved native tools outside the grant (issue #5900)", () => {
|
|
524
|
+
const message = {
|
|
525
|
+
role: "assistant",
|
|
526
|
+
content: [
|
|
527
|
+
{ type: "text", text: "Investigating the networking design." },
|
|
528
|
+
{
|
|
529
|
+
type: "toolCall",
|
|
530
|
+
id: "tc-grep",
|
|
531
|
+
name: "grep",
|
|
532
|
+
arguments: { pattern: "backoff" },
|
|
533
|
+
[kCursorExecResolved]: true,
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
type: "toolCall",
|
|
537
|
+
id: "tc-bash",
|
|
538
|
+
name: "bash",
|
|
539
|
+
arguments: { command: "ls" },
|
|
540
|
+
[kCursorExecResolved]: true,
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
type: "toolCall",
|
|
544
|
+
id: "tc-advise",
|
|
545
|
+
name: "advise",
|
|
546
|
+
arguments: { note: "The retry backoff looks unbounded." },
|
|
547
|
+
},
|
|
548
|
+
],
|
|
549
|
+
stopReason: "toolUse",
|
|
550
|
+
} as unknown as AssistantMessage;
|
|
551
|
+
const originalContent = message.content;
|
|
552
|
+
|
|
553
|
+
// Grant is `advise` only (WATCHDOG.yml `tools: []`). The native grep/bash
|
|
554
|
+
// frames already ran server-side through the advisor-scoped bridge, which
|
|
555
|
+
// rejected them in-band; they must not discard the legitimate advise.
|
|
556
|
+
expect(quarantineAdvisorUnsafeOutput(message, new Set(["advise"]))).toBeUndefined();
|
|
557
|
+
expect(message.stopReason).toBe("toolUse");
|
|
558
|
+
expect(message.content).toBe(originalContent);
|
|
559
|
+
expect(JSON.stringify(message)).toContain("unbounded");
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it("still quarantines an ungranted native tool that was not exec-resolved", () => {
|
|
563
|
+
const message = {
|
|
564
|
+
role: "assistant",
|
|
565
|
+
content: [{ type: "toolCall", id: "tc-bash", name: "bash", arguments: { command: "ls" } }],
|
|
566
|
+
stopReason: "toolUse",
|
|
567
|
+
} as unknown as AssistantMessage;
|
|
568
|
+
|
|
569
|
+
expect(quarantineAdvisorUnsafeOutput(message, new Set(["advise"]))).toBe(
|
|
570
|
+
"Advisor response quarantined: requested unavailable tool bash",
|
|
571
|
+
);
|
|
572
|
+
expect(message.stopReason).toBe("error");
|
|
573
|
+
});
|
|
574
|
+
|
|
522
575
|
it("sanitizes destructive advise notes even when advise is an allowed tool", () => {
|
|
523
576
|
const message = {
|
|
524
577
|
role: "assistant",
|
|
@@ -865,6 +918,65 @@ describe("advisor", () => {
|
|
|
865
918
|
expect(promptInputs[1]).toContain("second");
|
|
866
919
|
});
|
|
867
920
|
|
|
921
|
+
it("waits for an in-flight review within the catch-up deadline", async () => {
|
|
922
|
+
const promptStarted = Promise.withResolvers<void>();
|
|
923
|
+
const releasePrompt = Promise.withResolvers<void>();
|
|
924
|
+
const messages: AgentMessage[] = [{ role: "user", content: "first", timestamp: 1 } as AgentMessage];
|
|
925
|
+
const agent: AdvisorAgent = {
|
|
926
|
+
prompt: async () => {
|
|
927
|
+
promptStarted.resolve();
|
|
928
|
+
await releasePrompt.promise;
|
|
929
|
+
},
|
|
930
|
+
abort: () => {},
|
|
931
|
+
reset: () => {},
|
|
932
|
+
state: { messages: [] },
|
|
933
|
+
};
|
|
934
|
+
const runtime = new AdvisorRuntime(agent, {
|
|
935
|
+
snapshotMessages: () => messages,
|
|
936
|
+
enqueueAdvice: () => {},
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
runtime.onTurnEnd();
|
|
940
|
+
await promptStarted.promise;
|
|
941
|
+
let settled = false;
|
|
942
|
+
const catchup = runtime.waitForCatchup(1000, 1).then(caughtUp => {
|
|
943
|
+
settled = true;
|
|
944
|
+
return caughtUp;
|
|
945
|
+
});
|
|
946
|
+
await Promise.resolve();
|
|
947
|
+
expect(settled).toBe(false);
|
|
948
|
+
|
|
949
|
+
releasePrompt.resolve();
|
|
950
|
+
expect(await catchup).toBe(true);
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
it("reports an in-flight review that exceeds the catch-up deadline", async () => {
|
|
954
|
+
const promptStarted = Promise.withResolvers<void>();
|
|
955
|
+
const releasePrompt = Promise.withResolvers<void>();
|
|
956
|
+
const messages: AgentMessage[] = [{ role: "user", content: "first", timestamp: 1 } as AgentMessage];
|
|
957
|
+
const agent: AdvisorAgent = {
|
|
958
|
+
prompt: async () => {
|
|
959
|
+
promptStarted.resolve();
|
|
960
|
+
await releasePrompt.promise;
|
|
961
|
+
},
|
|
962
|
+
abort: () => {},
|
|
963
|
+
reset: () => {},
|
|
964
|
+
state: { messages: [] },
|
|
965
|
+
};
|
|
966
|
+
const runtime = new AdvisorRuntime(agent, {
|
|
967
|
+
snapshotMessages: () => messages,
|
|
968
|
+
enqueueAdvice: () => {},
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
runtime.onTurnEnd();
|
|
972
|
+
await promptStarted.promise;
|
|
973
|
+
expect(await runtime.waitForCatchup(20, 1)).toBe(false);
|
|
974
|
+
expect(runtime.backlog).toBe(1);
|
|
975
|
+
|
|
976
|
+
releasePrompt.resolve();
|
|
977
|
+
await settleUntil(() => runtime.backlog === 0);
|
|
978
|
+
});
|
|
979
|
+
|
|
868
980
|
it("preserves the next user turn when an accepted empty stop is pruned", async () => {
|
|
869
981
|
const promptInputs: string[] = [];
|
|
870
982
|
const agent = makeAgent(promptInputs);
|
|
@@ -3779,6 +3891,44 @@ describe("advisor", () => {
|
|
|
3779
3891
|
// or it strands and #drainStrandedQueuedMessages auto-resumes it. Do not swap
|
|
3780
3892
|
// the call site back to session `isStreaming`.
|
|
3781
3893
|
describe("resolveAdvisorDeliveryChannel", () => {
|
|
3894
|
+
it("preserves every severity when a headless drain forbids primary turns", () => {
|
|
3895
|
+
for (const severity of [undefined, "nit", "concern", "blocker"] as const) {
|
|
3896
|
+
expect(
|
|
3897
|
+
resolveAdvisorDeliveryChannel({
|
|
3898
|
+
severity,
|
|
3899
|
+
autoResumeSuppressed: false,
|
|
3900
|
+
streaming: false,
|
|
3901
|
+
aborting: false,
|
|
3902
|
+
terminalAnswerNoQueuedWork: true,
|
|
3903
|
+
preserveOnly: true,
|
|
3904
|
+
}),
|
|
3905
|
+
).toBe("preserve");
|
|
3906
|
+
}
|
|
3907
|
+
});
|
|
3908
|
+
|
|
3909
|
+
it("keeps live headless advice on normal delivery channels until the primary finishes", () => {
|
|
3910
|
+
expect(
|
|
3911
|
+
resolveAdvisorDeliveryChannel({
|
|
3912
|
+
severity: "nit",
|
|
3913
|
+
autoResumeSuppressed: false,
|
|
3914
|
+
streaming: true,
|
|
3915
|
+
aborting: false,
|
|
3916
|
+
preserveOnly: true,
|
|
3917
|
+
}),
|
|
3918
|
+
).toBe("aside");
|
|
3919
|
+
for (const severity of ["concern", "blocker"] as const) {
|
|
3920
|
+
expect(
|
|
3921
|
+
resolveAdvisorDeliveryChannel({
|
|
3922
|
+
severity,
|
|
3923
|
+
autoResumeSuppressed: false,
|
|
3924
|
+
streaming: true,
|
|
3925
|
+
aborting: false,
|
|
3926
|
+
preserveOnly: true,
|
|
3927
|
+
}),
|
|
3928
|
+
).toBe("steer");
|
|
3929
|
+
}
|
|
3930
|
+
});
|
|
3931
|
+
|
|
3782
3932
|
it("routes a non-interrupting nit to the aside queue regardless of state", () => {
|
|
3783
3933
|
expect(
|
|
3784
3934
|
resolveAdvisorDeliveryChannel({
|
|
@@ -101,6 +101,8 @@ export function isAdvisorInterruptImmuneTurnActive(opts: {
|
|
|
101
101
|
/**
|
|
102
102
|
* Decide how one advisor note reaches the primary agent.
|
|
103
103
|
*
|
|
104
|
+
* - A `preserveOnly` caller records every note that arrives while the primary
|
|
105
|
+
* is idle as a visible card and never starts a new primary turn.
|
|
104
106
|
* - A non-interrupting `nit` always rides the non-interrupting aside queue.
|
|
105
107
|
* - An interrupting `concern`/`blocker` is normally steered into the agent: into
|
|
106
108
|
* the live turn while one is streaming, or (when idle) a triggered turn so the
|
|
@@ -129,7 +131,9 @@ export function resolveAdvisorDeliveryChannel(opts: {
|
|
|
129
131
|
aborting: boolean;
|
|
130
132
|
terminalAnswerNoQueuedWork?: boolean;
|
|
131
133
|
interruptImmuneTurnActive?: boolean;
|
|
134
|
+
preserveOnly?: boolean;
|
|
132
135
|
}): AdvisorDeliveryChannel {
|
|
136
|
+
if (opts.preserveOnly && !opts.streaming) return "preserve";
|
|
133
137
|
if (!isInterruptingSeverity(opts.severity)) return "aside";
|
|
134
138
|
if (opts.autoResumeSuppressed && (opts.aborting || !opts.streaming)) return "preserve";
|
|
135
139
|
if (opts.terminalAnswerNoQueuedWork && opts.severity !== "blocker" && !opts.streaming && !opts.aborting)
|
package/src/advisor/runtime.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
|
2
2
|
import { estimateTokens } from "@oh-my-pi/pi-agent-core/compaction";
|
|
3
3
|
import type { AssistantMessage, ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
5
|
+
import { type CursorExecResolvedCarrier, kCursorExecResolved } from "@oh-my-pi/pi-ai/utils/block-symbols";
|
|
5
6
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
6
7
|
import { obfuscateToolArguments, type SecretObfuscator } from "../secrets/obfuscator";
|
|
7
8
|
import { formatSessionHistoryMarkdown, PRIMARY_CONTEXT_CUSTOM_TYPES } from "../session/session-history-format";
|
|
@@ -129,7 +130,20 @@ export function quarantineAdvisorUnsafeOutput(
|
|
|
129
130
|
const unavailableToolNames = new Set<string>();
|
|
130
131
|
const generatedParts: string[] = [];
|
|
131
132
|
for (const block of message.content) {
|
|
132
|
-
|
|
133
|
+
// Cursor exec-channel native blocks (bash/read/grep/...) are stamped
|
|
134
|
+
// kCursorExecResolved: they already ran server-side through the
|
|
135
|
+
// advisor-scoped CursorExecHandlers bridge, which rejects ungranted
|
|
136
|
+
// tools in-band ("Tool not available") and lets the model self-correct.
|
|
137
|
+
// Quarantining them would discard the legitimate advise emitted in the
|
|
138
|
+
// same turn (issue #5900). The scoped bridge is the grant gate here, not
|
|
139
|
+
// this pre-dispatch check.
|
|
140
|
+
if (
|
|
141
|
+
block.type === "toolCall" &&
|
|
142
|
+
!availableToolNames.has(block.name) &&
|
|
143
|
+
(block as CursorExecResolvedCarrier)[kCursorExecResolved] !== true
|
|
144
|
+
) {
|
|
145
|
+
unavailableToolNames.add(block.name);
|
|
146
|
+
}
|
|
133
147
|
if (block.type === "toolCall" && block.name === "advise" && typeof block.arguments.note === "string") {
|
|
134
148
|
generatedParts.push(block.arguments.note);
|
|
135
149
|
}
|
|
@@ -219,8 +233,7 @@ interface PendingDelta {
|
|
|
219
233
|
|
|
220
234
|
interface CatchupWaiter {
|
|
221
235
|
threshold: number;
|
|
222
|
-
|
|
223
|
-
finish: () => void;
|
|
236
|
+
finish: (caughtUp: boolean) => void;
|
|
224
237
|
timer?: NodeJS.Timeout;
|
|
225
238
|
}
|
|
226
239
|
|
|
@@ -366,7 +379,13 @@ export class AdvisorRuntime {
|
|
|
366
379
|
}
|
|
367
380
|
}
|
|
368
381
|
|
|
369
|
-
|
|
382
|
+
/**
|
|
383
|
+
* Wait until the advisor backlog falls below `threshold`.
|
|
384
|
+
*
|
|
385
|
+
* Returns `false` when the deadline, abort signal, or a runtime failure releases
|
|
386
|
+
* the waiter before the requested backlog was drained.
|
|
387
|
+
*/
|
|
388
|
+
waitForCatchup(maxMs: number, threshold: number, signal?: AbortSignal): Promise<boolean> {
|
|
370
389
|
if (
|
|
371
390
|
this.disposed ||
|
|
372
391
|
signal?.aborted ||
|
|
@@ -378,21 +397,26 @@ export class AdvisorRuntime {
|
|
|
378
397
|
// primary would otherwise park for the full catch-up budget.
|
|
379
398
|
this.#failing
|
|
380
399
|
)
|
|
381
|
-
return Promise.resolve();
|
|
382
|
-
const { promise, resolve } = Promise.withResolvers<
|
|
400
|
+
return Promise.resolve(this.#backlog < threshold);
|
|
401
|
+
const { promise, resolve } = Promise.withResolvers<boolean>();
|
|
383
402
|
let waiter!: CatchupWaiter;
|
|
384
|
-
const finish = (): void => {
|
|
403
|
+
const finish = (caughtUp: boolean): void => {
|
|
385
404
|
const idx = this.#waiters.indexOf(waiter);
|
|
386
405
|
if (idx >= 0) this.#waiters.splice(idx, 1);
|
|
387
406
|
clearTimeout(waiter.timer);
|
|
388
|
-
signal?.removeEventListener("abort",
|
|
389
|
-
resolve();
|
|
407
|
+
signal?.removeEventListener("abort", abort);
|
|
408
|
+
resolve(caughtUp);
|
|
409
|
+
};
|
|
410
|
+
const abort = (): void => finish(false);
|
|
411
|
+
waiter = {
|
|
412
|
+
threshold,
|
|
413
|
+
finish,
|
|
414
|
+
timer: setTimeout(abort, maxMs),
|
|
390
415
|
};
|
|
391
|
-
waiter = { threshold, resolve, finish, timer: setTimeout(finish, maxMs) };
|
|
392
416
|
this.#waiters.push(waiter);
|
|
393
|
-
signal?.addEventListener("abort",
|
|
417
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
394
418
|
if (signal?.aborted) {
|
|
395
|
-
|
|
419
|
+
abort();
|
|
396
420
|
}
|
|
397
421
|
return promise;
|
|
398
422
|
}
|
|
@@ -571,14 +595,14 @@ export class AdvisorRuntime {
|
|
|
571
595
|
for (let i = this.#waiters.length - 1; i >= 0; i--) {
|
|
572
596
|
const w = this.#waiters[i];
|
|
573
597
|
if (this.#backlog < w.threshold) {
|
|
574
|
-
w.finish();
|
|
598
|
+
w.finish(true);
|
|
575
599
|
}
|
|
576
600
|
}
|
|
577
601
|
}
|
|
578
602
|
|
|
579
603
|
#wakeAllWaiters(): void {
|
|
580
604
|
for (const w of [...this.#waiters]) {
|
|
581
|
-
w.finish();
|
|
605
|
+
w.finish(false);
|
|
582
606
|
}
|
|
583
607
|
}
|
|
584
608
|
|
package/src/async/job-manager.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface AsyncJob {
|
|
|
37
37
|
promise: Promise<void>;
|
|
38
38
|
resultText?: string;
|
|
39
39
|
errorText?: string;
|
|
40
|
+
/** Latest tool-render details reported by the running job. */
|
|
41
|
+
latestDetails?: Record<string, unknown>;
|
|
40
42
|
/**
|
|
41
43
|
* Registry id of the agent that registered the job (e.g. "Main",
|
|
42
44
|
* "AuthLoader"). Used by scoped cancel/list APIs so a subagent's teardown
|
|
@@ -205,6 +207,7 @@ export class AsyncJobManager {
|
|
|
205
207
|
};
|
|
206
208
|
|
|
207
209
|
const reportProgress = async (text: string, details?: Record<string, unknown>): Promise<void> => {
|
|
210
|
+
if (details) job.latestDetails = details;
|
|
208
211
|
if (!options?.onProgress) return;
|
|
209
212
|
try {
|
|
210
213
|
await options.onProgress(text, details);
|
package/src/cli/bench-cli.ts
CHANGED
|
@@ -443,7 +443,7 @@ function resolveBenchModels(
|
|
|
443
443
|
const resolved: BenchTarget[] = [];
|
|
444
444
|
const errors: string[] = [];
|
|
445
445
|
for (const selector of selectors) {
|
|
446
|
-
const result = resolveCliModel({ cliModel: selector, modelRegistry, preferences });
|
|
446
|
+
const result = resolveCliModel({ cliModel: selector, modelRegistry, settings, preferences });
|
|
447
447
|
if (result.error) {
|
|
448
448
|
errors.push(`${selector}: ${result.error}`);
|
|
449
449
|
continue;
|
|
@@ -454,7 +454,13 @@ function resolveBenchModels(
|
|
|
454
454
|
}
|
|
455
455
|
if (result.warning) writeStderr(`${chalk.yellow(`Warning: ${result.warning}`)}\n`);
|
|
456
456
|
let model = result.model;
|
|
457
|
-
const
|
|
457
|
+
const authSelector = result.configuredPatterns?.[result.configuredPatternIndex ?? 0] ?? selector;
|
|
458
|
+
const authenticated = resolveAuthenticatedAlternative(
|
|
459
|
+
authSelector,
|
|
460
|
+
model,
|
|
461
|
+
modelRegistry,
|
|
462
|
+
preferences.providerOrder,
|
|
463
|
+
);
|
|
458
464
|
if (authenticated) {
|
|
459
465
|
writeStderr(
|
|
460
466
|
`${chalk.yellow(
|