@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
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": "16.0.
|
|
4
|
+
"version": "16.0.3",
|
|
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",
|
|
@@ -47,17 +47,17 @@
|
|
|
47
47
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
48
48
|
"@babel/parser": "^7.29.7",
|
|
49
49
|
"@mozilla/readability": "^0.6.0",
|
|
50
|
-
"@oh-my-pi/hashline": "16.0.
|
|
51
|
-
"@oh-my-pi/omp-stats": "16.0.
|
|
52
|
-
"@oh-my-pi/pi-agent-core": "16.0.
|
|
53
|
-
"@oh-my-pi/pi-ai": "16.0.
|
|
54
|
-
"@oh-my-pi/pi-catalog": "16.0.
|
|
55
|
-
"@oh-my-pi/pi-mnemopi": "16.0.
|
|
56
|
-
"@oh-my-pi/pi-natives": "16.0.
|
|
57
|
-
"@oh-my-pi/pi-tui": "16.0.
|
|
58
|
-
"@oh-my-pi/pi-utils": "16.0.
|
|
59
|
-
"@oh-my-pi/pi-wire": "16.0.
|
|
60
|
-
"@oh-my-pi/snapcompact": "16.0.
|
|
50
|
+
"@oh-my-pi/hashline": "16.0.3",
|
|
51
|
+
"@oh-my-pi/omp-stats": "16.0.3",
|
|
52
|
+
"@oh-my-pi/pi-agent-core": "16.0.3",
|
|
53
|
+
"@oh-my-pi/pi-ai": "16.0.3",
|
|
54
|
+
"@oh-my-pi/pi-catalog": "16.0.3",
|
|
55
|
+
"@oh-my-pi/pi-mnemopi": "16.0.3",
|
|
56
|
+
"@oh-my-pi/pi-natives": "16.0.3",
|
|
57
|
+
"@oh-my-pi/pi-tui": "16.0.3",
|
|
58
|
+
"@oh-my-pi/pi-utils": "16.0.3",
|
|
59
|
+
"@oh-my-pi/pi-wire": "16.0.3",
|
|
60
|
+
"@oh-my-pi/snapcompact": "16.0.3",
|
|
61
61
|
"@opentelemetry/api": "^1.9.1",
|
|
62
62
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
63
63
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
|
@@ -8,10 +8,12 @@ import {
|
|
|
8
8
|
ADVISOR_READONLY_TOOL_NAMES,
|
|
9
9
|
AdviseTool,
|
|
10
10
|
type AdvisorAgent,
|
|
11
|
+
type AdvisorNote,
|
|
11
12
|
AdvisorRuntime,
|
|
12
13
|
type AdvisorRuntimeHost,
|
|
13
14
|
formatAdvisorBatchContent,
|
|
14
15
|
isInterruptingSeverity,
|
|
16
|
+
resolveAdvisorDeliveryChannel,
|
|
15
17
|
} from "..";
|
|
16
18
|
|
|
17
19
|
describe("advisor", () => {
|
|
@@ -51,7 +53,7 @@ describe("advisor", () => {
|
|
|
51
53
|
},
|
|
52
54
|
scheduleIdleFlush: () => {},
|
|
53
55
|
});
|
|
54
|
-
yq.register<
|
|
56
|
+
yq.register<AdvisorNote>("advisor", {
|
|
55
57
|
build: entries =>
|
|
56
58
|
entries.length === 0
|
|
57
59
|
? null
|
|
@@ -61,9 +63,7 @@ describe("advisor", () => {
|
|
|
61
63
|
display: true,
|
|
62
64
|
attribution: "agent",
|
|
63
65
|
timestamp: Date.now(),
|
|
64
|
-
content:
|
|
65
|
-
"Advisor (a senior reviewer watching your work — weigh it, don't blindly obey):\n" +
|
|
66
|
-
entries.map(e => `- ${e.severity ? `[${e.severity}] ` : ""}${e.note}`).join("\n"),
|
|
66
|
+
content: formatAdvisorBatchContent(entries),
|
|
67
67
|
} as AgentMessage),
|
|
68
68
|
});
|
|
69
69
|
|
|
@@ -76,8 +76,9 @@ describe("advisor", () => {
|
|
|
76
76
|
expect(msg.role).toBe("custom");
|
|
77
77
|
expect(msg.customType).toBe("advisor");
|
|
78
78
|
expect(msg.display).toBe(true);
|
|
79
|
-
expect(msg.content).toContain("
|
|
80
|
-
expect(msg.content).toContain("
|
|
79
|
+
expect(msg.content).toContain("second note");
|
|
80
|
+
expect(msg.content).toContain('severity="blocker"');
|
|
81
|
+
expect(msg.content).toContain("first note");
|
|
81
82
|
});
|
|
82
83
|
|
|
83
84
|
it("skipIdleFlush prevents idle scheduling", () => {
|
|
@@ -123,15 +124,21 @@ describe("advisor", () => {
|
|
|
123
124
|
expect(isInterruptingSeverity(undefined)).toBe(false);
|
|
124
125
|
});
|
|
125
126
|
|
|
126
|
-
it("
|
|
127
|
+
it("wraps each note in an advisory tag with severity as an attribute and escapes the body", () => {
|
|
127
128
|
const content = formatAdvisorBatchContent([
|
|
128
129
|
{ note: "first note" },
|
|
129
|
-
{ note: "second note", severity: "blocker" },
|
|
130
|
+
{ note: "second <note> & more", severity: "blocker" },
|
|
130
131
|
]);
|
|
131
|
-
|
|
132
|
-
expect(
|
|
133
|
-
|
|
134
|
-
expect(
|
|
132
|
+
// No-severity note: bare advisory tag (no severity attribute).
|
|
133
|
+
expect(content).toMatch(/<advisory guidance="[^"]*">\nfirst note\n<\/advisory>/);
|
|
134
|
+
// Severity rides an attribute, not an inline `[blocker]` tag or a bullet.
|
|
135
|
+
expect(content).toMatch(/<advisory severity="blocker" guidance="[^"]*">/);
|
|
136
|
+
expect(content).not.toContain("[blocker]");
|
|
137
|
+
expect(content).not.toContain("- first note");
|
|
138
|
+
// XML-significant characters in the body are escaped so they can't break the tag.
|
|
139
|
+
expect(content).toContain("second <note> & more");
|
|
140
|
+
// Exactly one severity attribute (only the blocker note carries one).
|
|
141
|
+
expect(content.split('severity="').length - 1).toBe(1);
|
|
135
142
|
});
|
|
136
143
|
});
|
|
137
144
|
|
|
@@ -278,6 +285,56 @@ describe("advisor", () => {
|
|
|
278
285
|
expect(promptInputs[0]).not.toContain("note");
|
|
279
286
|
});
|
|
280
287
|
|
|
288
|
+
it("renders the watched delta with a heading, watched-role labels, and no inner ## headings", () => {
|
|
289
|
+
const promptInputs: string[] = [];
|
|
290
|
+
const agent = makeAgent(promptInputs);
|
|
291
|
+
const messages: AgentMessage[] = [
|
|
292
|
+
{ role: "user", content: "do the thing", timestamp: 1 } as AgentMessage,
|
|
293
|
+
{
|
|
294
|
+
role: "assistant",
|
|
295
|
+
content: [{ type: "toolCall", id: "a", name: "read", arguments: { path: "x.ts" } }],
|
|
296
|
+
timestamp: 2,
|
|
297
|
+
} as unknown as AgentMessage,
|
|
298
|
+
{
|
|
299
|
+
role: "toolResult",
|
|
300
|
+
toolCallId: "a",
|
|
301
|
+
toolName: "read",
|
|
302
|
+
content: [{ type: "text", text: "ok" }],
|
|
303
|
+
isError: false,
|
|
304
|
+
timestamp: 3,
|
|
305
|
+
} as AgentMessage,
|
|
306
|
+
{
|
|
307
|
+
role: "assistant",
|
|
308
|
+
content: [{ type: "toolCall", id: "b", name: "search", arguments: { pattern: "y" } }],
|
|
309
|
+
timestamp: 4,
|
|
310
|
+
} as unknown as AgentMessage,
|
|
311
|
+
{
|
|
312
|
+
role: "toolResult",
|
|
313
|
+
toolCallId: "b",
|
|
314
|
+
toolName: "search",
|
|
315
|
+
content: [{ type: "text", text: "ok" }],
|
|
316
|
+
isError: false,
|
|
317
|
+
timestamp: 5,
|
|
318
|
+
} as AgentMessage,
|
|
319
|
+
];
|
|
320
|
+
const host: AdvisorRuntimeHost = {
|
|
321
|
+
snapshotMessages: () => messages,
|
|
322
|
+
enqueueAdvice: () => {},
|
|
323
|
+
};
|
|
324
|
+
const runtime = new AdvisorRuntime(agent, host);
|
|
325
|
+
runtime.onTurnEnd();
|
|
326
|
+
expect(promptInputs).toHaveLength(1);
|
|
327
|
+
const prompt = promptInputs[0];
|
|
328
|
+
expect(prompt).toContain("### Session update");
|
|
329
|
+
expect(prompt).toContain("**user**:");
|
|
330
|
+
expect(prompt).toContain("**agent**:");
|
|
331
|
+
// Inner role headings would collide with the advisor's own turns in the dump.
|
|
332
|
+
expect(prompt).not.toContain("## assistant");
|
|
333
|
+
expect(prompt).not.toContain("## user");
|
|
334
|
+
// Consecutive assistant tool-call messages collapse under a single label.
|
|
335
|
+
expect(prompt.split("**agent**:").length - 1).toBe(1);
|
|
336
|
+
});
|
|
337
|
+
|
|
281
338
|
it("handles compaction shrink without prompting", () => {
|
|
282
339
|
const promptInputs: string[] = [];
|
|
283
340
|
const agent = makeAgent(promptInputs);
|
|
@@ -583,4 +640,91 @@ describe("advisor", () => {
|
|
|
583
640
|
expect(text).toContain("truncated.");
|
|
584
641
|
});
|
|
585
642
|
});
|
|
643
|
+
|
|
644
|
+
// Regression: the advisor must not withhold interrupting advice from a turn
|
|
645
|
+
// that is actively streaming again after a user interrupt. The latch only
|
|
646
|
+
// guards auto-resume of a stopped/idle run; parking a note mid-stream stranded
|
|
647
|
+
// it (the agent never heard it) and dumped the backlog as one burst at the next
|
|
648
|
+
// user prompt. See the 7-concern same-instant burst in session 019ed1dd.
|
|
649
|
+
//
|
|
650
|
+
// `streaming` here means the live agent-CORE loop (agent.state.isStreaming) —
|
|
651
|
+
// NOT session `isStreaming`, which also counts `#promptInFlightCount` during
|
|
652
|
+
// post-turn unwind. Only a running core loop consumes a steer; in the unwind
|
|
653
|
+
// window (`streaming: false`) a suppressed note must `preserve`, never `steer`,
|
|
654
|
+
// or it strands and #drainStrandedQueuedMessages auto-resumes it. Do not swap
|
|
655
|
+
// the call site back to session `isStreaming`.
|
|
656
|
+
describe("resolveAdvisorDeliveryChannel", () => {
|
|
657
|
+
it("routes a non-interrupting nit to the aside queue regardless of state", () => {
|
|
658
|
+
expect(
|
|
659
|
+
resolveAdvisorDeliveryChannel({
|
|
660
|
+
severity: "nit",
|
|
661
|
+
autoResumeSuppressed: true,
|
|
662
|
+
streaming: true,
|
|
663
|
+
aborting: true,
|
|
664
|
+
}),
|
|
665
|
+
).toBe("aside");
|
|
666
|
+
expect(
|
|
667
|
+
resolveAdvisorDeliveryChannel({
|
|
668
|
+
severity: undefined,
|
|
669
|
+
autoResumeSuppressed: false,
|
|
670
|
+
streaming: false,
|
|
671
|
+
aborting: false,
|
|
672
|
+
}),
|
|
673
|
+
).toBe("aside");
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it("steers concern/blocker when no user interrupt is in effect", () => {
|
|
677
|
+
for (const severity of ["concern", "blocker"] as const) {
|
|
678
|
+
for (const streaming of [true, false]) {
|
|
679
|
+
expect(
|
|
680
|
+
resolveAdvisorDeliveryChannel({
|
|
681
|
+
severity,
|
|
682
|
+
autoResumeSuppressed: false,
|
|
683
|
+
streaming,
|
|
684
|
+
aborting: false,
|
|
685
|
+
}),
|
|
686
|
+
).toBe("steer");
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it("preserves an interrupting note while suppressed AND idle (no auto-resume of a stopped run)", () => {
|
|
692
|
+
for (const severity of ["concern", "blocker"] as const) {
|
|
693
|
+
expect(
|
|
694
|
+
resolveAdvisorDeliveryChannel({
|
|
695
|
+
severity,
|
|
696
|
+
autoResumeSuppressed: true,
|
|
697
|
+
streaming: false,
|
|
698
|
+
aborting: false,
|
|
699
|
+
}),
|
|
700
|
+
).toBe("preserve");
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it("preserves an interrupting note while suppressed AND aborting, even though the turn still reports streaming", () => {
|
|
705
|
+
// Mid-abort teardown: steering would land after #extractQueuedAdvisorCards
|
|
706
|
+
// and could auto-resume on the stranded steer. Keep parking it.
|
|
707
|
+
expect(
|
|
708
|
+
resolveAdvisorDeliveryChannel({
|
|
709
|
+
severity: "blocker",
|
|
710
|
+
autoResumeSuppressed: true,
|
|
711
|
+
streaming: true,
|
|
712
|
+
aborting: true,
|
|
713
|
+
}),
|
|
714
|
+
).toBe("preserve");
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it("steers an interrupting note while suppressed once a turn is streaming again and not aborting (the fix)", () => {
|
|
718
|
+
for (const severity of ["concern", "blocker"] as const) {
|
|
719
|
+
expect(
|
|
720
|
+
resolveAdvisorDeliveryChannel({
|
|
721
|
+
severity,
|
|
722
|
+
autoResumeSuppressed: true,
|
|
723
|
+
streaming: true,
|
|
724
|
+
aborting: false,
|
|
725
|
+
}),
|
|
726
|
+
).toBe("steer");
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
});
|
|
586
730
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { escapeXmlText } from "@oh-my-pi/pi-utils";
|
|
2
3
|
import { z } from "zod/v4";
|
|
3
4
|
import adviseDescription from "../prompts/advisor/advise-tool.md" with { type: "text" };
|
|
4
5
|
|
|
@@ -33,15 +34,26 @@ export interface AdvisorMessageDetails {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* Behavioral framing for the watched agent — advice, not orders. Carried as a
|
|
38
|
+
* tag attribute (rather than a prose header) so the rendered agent-facing output
|
|
39
|
+
* stays a clean `<advisory>` block. The primary agent's system prompt never
|
|
40
|
+
* mentions advisories, so this is its only cue for how to treat them.
|
|
39
41
|
*/
|
|
40
|
-
const
|
|
42
|
+
const ADVISOR_GUIDANCE = "weigh, don't blindly obey";
|
|
41
43
|
|
|
42
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Render a batch of advisor notes as the agent-facing message body: one
|
|
46
|
+
* `<advisory>` element per note, severity as an attribute. Shared by the
|
|
47
|
+
* non-interrupting YieldQueue dispatcher and the interrupting steer path so both
|
|
48
|
+
* build byte-identical content.
|
|
49
|
+
*/
|
|
43
50
|
export function formatAdvisorBatchContent(notes: readonly AdvisorNote[]): string {
|
|
44
|
-
return
|
|
51
|
+
return notes
|
|
52
|
+
.map(n => {
|
|
53
|
+
const severity = n.severity ? ` severity="${n.severity}"` : "";
|
|
54
|
+
return `<advisory${severity} guidance="${ADVISOR_GUIDANCE}">\n${escapeXmlText(n.note)}\n</advisory>`;
|
|
55
|
+
})
|
|
56
|
+
.join("\n");
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
/**
|
|
@@ -54,6 +66,36 @@ export function isInterruptingSeverity(severity: AdvisorSeverity | undefined): b
|
|
|
54
66
|
return severity === "concern" || severity === "blocker";
|
|
55
67
|
}
|
|
56
68
|
|
|
69
|
+
/** How an advisor note is routed to the primary. */
|
|
70
|
+
export type AdvisorDeliveryChannel = "aside" | "steer" | "preserve";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Decide how one advisor note reaches the primary agent.
|
|
74
|
+
*
|
|
75
|
+
* - A non-interrupting `nit` always rides the non-interrupting aside queue.
|
|
76
|
+
* - An interrupting `concern`/`blocker` is normally steered into the agent: into
|
|
77
|
+
* the live turn while one is streaming, or (when idle) a triggered turn so the
|
|
78
|
+
* advice is acted on immediately.
|
|
79
|
+
* - After a deliberate user interrupt (`autoResumeSuppressed`) the advisor must
|
|
80
|
+
* not auto-resume the stopped run. While the agent is idle — or still tearing
|
|
81
|
+
* the interrupted turn down (`aborting`) — the note is preserved as a visible
|
|
82
|
+
* card instead of restarting the run. But once a turn is actively streaming
|
|
83
|
+
* again (a resume the user already drove), steering the note in does NOT
|
|
84
|
+
* auto-resume anything, so it is delivered live. Parking it during an active
|
|
85
|
+
* run instead strands it (it never reaches the running agent) and the withheld
|
|
86
|
+
* notes dump as one burst at the next user prompt — the bug this guards.
|
|
87
|
+
*/
|
|
88
|
+
export function resolveAdvisorDeliveryChannel(opts: {
|
|
89
|
+
severity: AdvisorSeverity | undefined;
|
|
90
|
+
autoResumeSuppressed: boolean;
|
|
91
|
+
streaming: boolean;
|
|
92
|
+
aborting: boolean;
|
|
93
|
+
}): AdvisorDeliveryChannel {
|
|
94
|
+
if (!isInterruptingSeverity(opts.severity)) return "aside";
|
|
95
|
+
if (opts.autoResumeSuppressed && (opts.aborting || !opts.streaming)) return "preserve";
|
|
96
|
+
return "steer";
|
|
97
|
+
}
|
|
98
|
+
|
|
57
99
|
/**
|
|
58
100
|
* Side-effect-free investigation tools handed to the advisor agent so it can
|
|
59
101
|
* inspect the workspace before weighing in. Names match the primary session's
|
package/src/advisor/runtime.ts
CHANGED
|
@@ -157,8 +157,13 @@ export class AdvisorRuntime {
|
|
|
157
157
|
.filter(m => !(m.role === "custom" && (m as { customType?: string }).customType === "advisor"));
|
|
158
158
|
this.#lastCount = all.length;
|
|
159
159
|
if (delta.length === 0) return null;
|
|
160
|
-
const md = formatSessionHistoryMarkdown(delta, {
|
|
161
|
-
|
|
160
|
+
const md = formatSessionHistoryMarkdown(delta, {
|
|
161
|
+
includeThinking: true,
|
|
162
|
+
includeToolIntent: true,
|
|
163
|
+
watchedRoles: true,
|
|
164
|
+
});
|
|
165
|
+
if (!md.trim()) return null;
|
|
166
|
+
return `### Session update\n\n${md}`;
|
|
162
167
|
}
|
|
163
168
|
|
|
164
169
|
#notifyWaiters(): void {
|
|
@@ -182,7 +187,9 @@ export class AdvisorRuntime {
|
|
|
182
187
|
try {
|
|
183
188
|
while (!this.disposed && this.#pending.length) {
|
|
184
189
|
const popped = this.#pending.splice(0);
|
|
185
|
-
|
|
190
|
+
// Each delta already opens with a `### Session update` heading, so
|
|
191
|
+
// join with a blank line rather than a `---` rule.
|
|
192
|
+
const candidateBatch = popped.map(b => b.text).join("\n\n");
|
|
186
193
|
const turnsCovered = popped.reduce((sum, b) => sum + b.turns, 0);
|
|
187
194
|
const incomingTokens = estimateTokens({
|
|
188
195
|
role: "user",
|
|
@@ -22,7 +22,11 @@ import type { Settings } from "../config/settings";
|
|
|
22
22
|
import difficultySystemPrompt from "../prompts/system/auto-thinking-difficulty.md" with { type: "text" };
|
|
23
23
|
import difficultyLocalPrompt from "../prompts/system/auto-thinking-difficulty-local.md" with { type: "text" };
|
|
24
24
|
import { clampAutoThinkingEffort } from "../thinking";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
isTinyMemoryLocalModelKey,
|
|
27
|
+
isTinyMemoryReasoningModelKey,
|
|
28
|
+
ONLINE_AUTO_THINKING_MODEL_KEY,
|
|
29
|
+
} from "../tiny/models";
|
|
26
30
|
import { tinyModelClient } from "../tiny/title-client";
|
|
27
31
|
|
|
28
32
|
const DIFFICULTY_SYSTEM_PROMPT = prompt.render(difficultySystemPrompt);
|
|
@@ -31,8 +35,10 @@ const DIFFICULTY_SYSTEM_PROMPT = prompt.render(difficultySystemPrompt);
|
|
|
31
35
|
const MAX_INPUT_CHARS = 6000;
|
|
32
36
|
const HEAD_CHARS = 4000;
|
|
33
37
|
const TAIL_CHARS = 2000;
|
|
34
|
-
/** The answer is a single word; keep budgets tiny for non-reasoning backends. */
|
|
38
|
+
/** The online answer is a single word; keep budgets tiny for non-reasoning backends. */
|
|
35
39
|
const ANSWER_MAX_TOKENS = 8;
|
|
40
|
+
/** Local classifiers occasionally need more room for chat-template boilerplate. */
|
|
41
|
+
const LOCAL_ANSWER_MAX_TOKENS = 16;
|
|
36
42
|
/**
|
|
37
43
|
* Reasoning backends ignore `disableReasoning` on some providers, so reserve
|
|
38
44
|
* enough output room for the keyword to still land after unavoidable thinking.
|
|
@@ -107,9 +113,12 @@ async function classifyLocal(input: string, modelKey: string, deps: ClassifyDiff
|
|
|
107
113
|
if (!isTinyMemoryLocalModelKey(modelKey)) {
|
|
108
114
|
throw new Error(`auto-thinking: unsupported local classifier model: ${modelKey}`);
|
|
109
115
|
}
|
|
116
|
+
const maxTokens = isTinyMemoryReasoningModelKey(modelKey)
|
|
117
|
+
? Math.max(LOCAL_ANSWER_MAX_TOKENS, REASONING_SAFE_MAX_TOKENS)
|
|
118
|
+
: LOCAL_ANSWER_MAX_TOKENS;
|
|
110
119
|
const builtPrompt = prompt.render(difficultyLocalPrompt, { prompt: input });
|
|
111
120
|
const text = await tinyModelClient.complete(modelKey, builtPrompt, {
|
|
112
|
-
maxTokens
|
|
121
|
+
maxTokens,
|
|
113
122
|
signal: deps.signal,
|
|
114
123
|
});
|
|
115
124
|
if (!text) {
|
package/src/cli/args.ts
CHANGED
|
@@ -280,6 +280,7 @@ export function getExtraHelpText(): string {
|
|
|
280
280
|
MISTRAL_API_KEY - Mistral models
|
|
281
281
|
ZAI_API_KEY - z.ai models (ZhipuAI/GLM)
|
|
282
282
|
UMANS_AI_CODING_PLAN_API_KEY - Umans AI Coding Plan models
|
|
283
|
+
UMANS_WEBSEARCH_PROVIDER - Umans gateway web search backend (native or exa)
|
|
283
284
|
MINIMAX_API_KEY - MiniMax models
|
|
284
285
|
OPENCODE_API_KEY - OpenCode Zen/OpenCode Go models
|
|
285
286
|
CURSOR_ACCESS_TOKEN - Cursor AI models
|
package/src/cli.ts
CHANGED
|
@@ -109,8 +109,8 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
|
|
|
109
109
|
// this dispatch completes — so anything the parent posted right after
|
|
110
110
|
// spawning (the smoke ping, the first parse request) would be dropped.
|
|
111
111
|
// Park early events and replay them once the module's handler is live.
|
|
112
|
-
//
|
|
113
|
-
//
|
|
112
|
+
// Worker-thread entries using `parentPort` need the same sync-prefix
|
|
113
|
+
// buffering; the tab/eval cases install that inbox below before import.
|
|
114
114
|
const scope = globalThis as unknown as { onmessage: ((event: MessageEvent) => void) | null };
|
|
115
115
|
const pending: MessageEvent[] = [];
|
|
116
116
|
const buffer = (event: MessageEvent): void => {
|
package/src/commands/install.ts
CHANGED
|
@@ -28,13 +28,13 @@ import { initTheme } from "../modes/theme/theme";
|
|
|
28
28
|
* Heuristic used to decide whether `omp install <target>` should `link` a
|
|
29
29
|
* local directory or `install` a remote spec. Exported for tests.
|
|
30
30
|
*/
|
|
31
|
-
export function looksLikeLocalPath(target: string): boolean {
|
|
31
|
+
export function looksLikeLocalPath(target: string, cwd?: string): boolean {
|
|
32
32
|
if (target.startsWith(".") || target.startsWith("/") || target.startsWith("~")) return true;
|
|
33
33
|
// Windows drive prefix (e.g. `C:\foo`).
|
|
34
34
|
if (/^[a-zA-Z]:[\\/]/.test(target)) return true;
|
|
35
|
-
// Bare names that happen to exist as a local directory.
|
|
35
|
+
// Bare names that happen to exist as a local directory (relative to `cwd`).
|
|
36
36
|
try {
|
|
37
|
-
return existsSync(path.resolve(target));
|
|
37
|
+
return existsSync(cwd ? path.resolve(cwd, target) : path.resolve(target));
|
|
38
38
|
} catch {
|
|
39
39
|
return false;
|
|
40
40
|
}
|
|
@@ -34,17 +34,36 @@ import { isAuthenticated, kNoAuth, type ModelRegistry } from "./model-registry";
|
|
|
34
34
|
import { MODEL_ROLE_IDS, type ModelRole } from "./model-roles";
|
|
35
35
|
import type { Settings } from "./settings";
|
|
36
36
|
|
|
37
|
+
function isKnownProvider(provider: string): provider is KnownProvider {
|
|
38
|
+
return provider in DEFAULT_MODEL_PER_PROVIDER;
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
/**
|
|
38
|
-
* Pick the first
|
|
39
|
-
*
|
|
42
|
+
* Pick the first provider-default model in availability order.
|
|
43
|
+
*
|
|
44
|
+
* If multiple providers expose that same default id, rank only that shared-id
|
|
45
|
+
* group by canonical provider priority so native/OAuth transports beat mirrors
|
|
46
|
+
* without changing unrelated provider fallback precedence.
|
|
40
47
|
*/
|
|
41
|
-
function pickDefaultAvailableModel(availableModels: Model<Api>[]): Model<Api> | undefined {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
export function pickDefaultAvailableModel(availableModels: Model<Api>[]): Model<Api> | undefined {
|
|
49
|
+
const firstDefault = availableModels.find(
|
|
50
|
+
model => isKnownProvider(model.provider) && DEFAULT_MODEL_PER_PROVIDER[model.provider] === model.id,
|
|
51
|
+
);
|
|
52
|
+
if (!firstDefault) return availableModels[0];
|
|
53
|
+
|
|
54
|
+
const providerPriority = buildModelProviderPriorityRank();
|
|
55
|
+
const sharedDefaultMatches = availableModels.filter(
|
|
56
|
+
model =>
|
|
57
|
+
model.id === firstDefault.id &&
|
|
58
|
+
isKnownProvider(model.provider) &&
|
|
59
|
+
DEFAULT_MODEL_PER_PROVIDER[model.provider] === model.id,
|
|
60
|
+
);
|
|
61
|
+
return [...sharedDefaultMatches].sort((a, b) => {
|
|
62
|
+
const aRank = providerPriority.get(a.provider.toLowerCase()) ?? Number.POSITIVE_INFINITY;
|
|
63
|
+
const bRank = providerPriority.get(b.provider.toLowerCase()) ?? Number.POSITIVE_INFINITY;
|
|
64
|
+
if (aRank !== bRank) return aRank - bRank;
|
|
65
|
+
return availableModels.indexOf(a) - availableModels.indexOf(b);
|
|
66
|
+
})[0];
|
|
48
67
|
}
|
|
49
68
|
|
|
50
69
|
export interface ScopedModel {
|
|
@@ -92,6 +111,33 @@ export function formatModelString(model: Model<Api>): string {
|
|
|
92
111
|
return `${model.provider}/${model.id}`;
|
|
93
112
|
}
|
|
94
113
|
|
|
114
|
+
function getSingleRoutingOnly(routing: unknown): string | undefined {
|
|
115
|
+
if (!routing || typeof routing !== "object" || !("only" in routing) || !Array.isArray(routing.only)) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
if (routing.only.length !== 1) return undefined;
|
|
119
|
+
const upstream = routing.only[0];
|
|
120
|
+
return typeof upstream === "string" && upstream ? upstream : undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getSingleUpstreamRoute(model: Model<Api>): string | undefined {
|
|
124
|
+
const compat = model.compat;
|
|
125
|
+
if (!compat || typeof compat !== "object") return undefined;
|
|
126
|
+
if (modelMatchesHost(model, "vercelAIGateway") && "vercelGatewayRouting" in compat) {
|
|
127
|
+
return getSingleRoutingOnly(compat.vercelGatewayRouting);
|
|
128
|
+
}
|
|
129
|
+
if (modelMatchesHost(model, "openrouter") && "openRouterRouting" in compat) {
|
|
130
|
+
return getSingleRoutingOnly(compat.openRouterRouting);
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function formatModelStringWithRouting(model: Model<Api>): string {
|
|
136
|
+
const selector = formatModelString(model);
|
|
137
|
+
const upstream = getSingleUpstreamRoute(model);
|
|
138
|
+
return upstream ? `${selector}@${upstream}` : selector;
|
|
139
|
+
}
|
|
140
|
+
|
|
95
141
|
export function formatModelSelectorValue(selector: string, thinkingLevel: ThinkingLevel | undefined): string {
|
|
96
142
|
return thinkingLevel && thinkingLevel !== ThinkingLevel.Inherit ? `${selector}:${thinkingLevel}` : selector;
|
|
97
143
|
}
|
|
@@ -161,7 +207,7 @@ const UPSTREAM_ROUTING_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i;
|
|
|
161
207
|
* `@` or the suffix is not a bare provider slug, so model ids that legitimately
|
|
162
208
|
* contain `@` (`claude-opus-4-8@default`, `workers-ai/@cf/...`) are never split.
|
|
163
209
|
*/
|
|
164
|
-
function splitUpstreamRouting(pattern: string): { base: string; upstream: string } | undefined {
|
|
210
|
+
export function splitUpstreamRouting(pattern: string): { base: string; upstream: string } | undefined {
|
|
165
211
|
const at = pattern.lastIndexOf("@");
|
|
166
212
|
if (at <= 0) return undefined;
|
|
167
213
|
const rest = pattern.slice(at + 1);
|
|
@@ -437,12 +483,10 @@ function matchModel(
|
|
|
437
483
|
context: ModelPreferenceContext,
|
|
438
484
|
options?: { modelRegistry?: CanonicalModelRegistry },
|
|
439
485
|
): Model<Api> | undefined {
|
|
440
|
-
// Explicit provider/model selectors always bypass canonical coalescing.
|
|
441
486
|
const exactRefMatch = findExactModelReferenceMatch(modelPattern, availableModels);
|
|
442
487
|
if (exactRefMatch) {
|
|
443
488
|
return exactRefMatch;
|
|
444
489
|
}
|
|
445
|
-
|
|
446
490
|
// Exact canonical ids coalesce provider variants before bare-id matching.
|
|
447
491
|
const exactCanonicalMatch = findExactCanonicalModelMatch(modelPattern, availableModels, options?.modelRegistry);
|
|
448
492
|
if (exactCanonicalMatch) {
|
|
@@ -481,6 +525,13 @@ function matchModel(
|
|
|
481
525
|
// The prefix is not a known provider in this candidate set, so treat the
|
|
482
526
|
// slash as part of the raw model ID and continue with generic matching.
|
|
483
527
|
} else {
|
|
528
|
+
// Let the routing fallback apply `@upstream` before fuzzy matching can consume the
|
|
529
|
+
// slug — but only for aggregator providers (OpenRouter / Vercel Gateway). Other
|
|
530
|
+
// providers have ids that legitimately end in `@` (Vertex `claude-opus-4-8@default`),
|
|
531
|
+
// and the fallback never routes them, so they must keep fuzzy matching.
|
|
532
|
+
if (splitUpstreamRouting(modelId) && providerModels.some(supportsUpstreamRouting)) {
|
|
533
|
+
return undefined;
|
|
534
|
+
}
|
|
484
535
|
const scored = providerModels
|
|
485
536
|
.map(model => ({ model, match: fuzzyMatch(modelId, model.id) }))
|
|
486
537
|
.filter(entry => entry.match.matches);
|
|
@@ -3070,17 +3070,6 @@ export const SETTINGS_SCHEMA = {
|
|
|
3070
3070
|
|
|
3071
3071
|
// Optional tools
|
|
3072
3072
|
|
|
3073
|
-
"renderMermaid.enabled": {
|
|
3074
|
-
type: "boolean",
|
|
3075
|
-
default: false,
|
|
3076
|
-
ui: {
|
|
3077
|
-
tab: "tools",
|
|
3078
|
-
group: "Available Tools",
|
|
3079
|
-
label: "Render Mermaid",
|
|
3080
|
-
description: "Enable the render_mermaid tool for Mermaid-to-ASCII rendering",
|
|
3081
|
-
},
|
|
3082
|
-
},
|
|
3083
|
-
|
|
3084
3073
|
"debug.enabled": {
|
|
3085
3074
|
type: "boolean",
|
|
3086
3075
|
default: true,
|