@oh-my-pi/pi-coding-agent 16.0.2 → 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 +45 -0
- package/README.md +0 -1
- package/dist/cli.js +217 -276
- 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 +8 -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/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/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/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +150 -50
- 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.ts +2 -2
- package/src/commands/install.ts +3 -3
- package/src/config/model-resolver.ts +28 -11
- package/src/config/settings-schema.ts +0 -11
- 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/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/goals/runtime.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +4 -6
- 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 +246 -78
- package/src/session/artifacts.ts +19 -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/task/executor.ts +18 -2
- 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/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
|
@@ -2,17 +2,18 @@ import { describe, expect, it, vi } from "bun:test";
|
|
|
2
2
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import { createAdvisorMessageCard } from "../../modes/components/advisor-message";
|
|
4
4
|
import { getThemeByName } from "../../modes/theme/theme";
|
|
5
|
-
import { formatSessionDumpText } from "../../session/session-dump-format";
|
|
6
5
|
import { formatSessionHistoryMarkdown } from "../../session/session-history-format";
|
|
7
6
|
import { YieldQueue } from "../../session/yield-queue";
|
|
8
7
|
import {
|
|
9
8
|
ADVISOR_READONLY_TOOL_NAMES,
|
|
10
9
|
AdviseTool,
|
|
11
10
|
type AdvisorAgent,
|
|
11
|
+
type AdvisorNote,
|
|
12
12
|
AdvisorRuntime,
|
|
13
13
|
type AdvisorRuntimeHost,
|
|
14
14
|
formatAdvisorBatchContent,
|
|
15
15
|
isInterruptingSeverity,
|
|
16
|
+
resolveAdvisorDeliveryChannel,
|
|
16
17
|
} from "..";
|
|
17
18
|
|
|
18
19
|
describe("advisor", () => {
|
|
@@ -52,7 +53,7 @@ describe("advisor", () => {
|
|
|
52
53
|
},
|
|
53
54
|
scheduleIdleFlush: () => {},
|
|
54
55
|
});
|
|
55
|
-
yq.register<
|
|
56
|
+
yq.register<AdvisorNote>("advisor", {
|
|
56
57
|
build: entries =>
|
|
57
58
|
entries.length === 0
|
|
58
59
|
? null
|
|
@@ -62,9 +63,7 @@ describe("advisor", () => {
|
|
|
62
63
|
display: true,
|
|
63
64
|
attribution: "agent",
|
|
64
65
|
timestamp: Date.now(),
|
|
65
|
-
content:
|
|
66
|
-
"Advisor (a senior reviewer watching your work — weigh it, don't blindly obey):\n" +
|
|
67
|
-
entries.map(e => `- ${e.severity ? `[${e.severity}] ` : ""}${e.note}`).join("\n"),
|
|
66
|
+
content: formatAdvisorBatchContent(entries),
|
|
68
67
|
} as AgentMessage),
|
|
69
68
|
});
|
|
70
69
|
|
|
@@ -77,8 +76,9 @@ describe("advisor", () => {
|
|
|
77
76
|
expect(msg.role).toBe("custom");
|
|
78
77
|
expect(msg.customType).toBe("advisor");
|
|
79
78
|
expect(msg.display).toBe(true);
|
|
80
|
-
expect(msg.content).toContain("
|
|
81
|
-
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");
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
it("skipIdleFlush prevents idle scheduling", () => {
|
|
@@ -124,15 +124,21 @@ describe("advisor", () => {
|
|
|
124
124
|
expect(isInterruptingSeverity(undefined)).toBe(false);
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
-
it("
|
|
127
|
+
it("wraps each note in an advisory tag with severity as an attribute and escapes the body", () => {
|
|
128
128
|
const content = formatAdvisorBatchContent([
|
|
129
129
|
{ note: "first note" },
|
|
130
|
-
{ note: "second note", severity: "blocker" },
|
|
130
|
+
{ note: "second <note> & more", severity: "blocker" },
|
|
131
131
|
]);
|
|
132
|
-
|
|
133
|
-
expect(
|
|
134
|
-
|
|
135
|
-
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);
|
|
136
142
|
});
|
|
137
143
|
});
|
|
138
144
|
|
|
@@ -279,6 +285,56 @@ describe("advisor", () => {
|
|
|
279
285
|
expect(promptInputs[0]).not.toContain("note");
|
|
280
286
|
});
|
|
281
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
|
+
|
|
282
338
|
it("handles compaction shrink without prompting", () => {
|
|
283
339
|
const promptInputs: string[] = [];
|
|
284
340
|
const agent = makeAgent(promptInputs);
|
|
@@ -584,47 +640,91 @@ describe("advisor", () => {
|
|
|
584
640
|
expect(text).toContain("truncated.");
|
|
585
641
|
});
|
|
586
642
|
});
|
|
587
|
-
describe("formatSessionDumpText raw thinking", () => {
|
|
588
|
-
it("does not nest literal thinking envelopes", () => {
|
|
589
|
-
const md = formatSessionDumpText({
|
|
590
|
-
messages: [
|
|
591
|
-
{
|
|
592
|
-
role: "assistant",
|
|
593
|
-
content: [
|
|
594
|
-
{
|
|
595
|
-
type: "thinking",
|
|
596
|
-
thinking: "<thinking>\nCheck logs before accepting container health.\n</thinking>",
|
|
597
|
-
},
|
|
598
|
-
],
|
|
599
|
-
timestamp: Date.now(),
|
|
600
|
-
} as AgentMessage,
|
|
601
|
-
],
|
|
602
|
-
thinkingLevel: "high",
|
|
603
|
-
});
|
|
604
643
|
|
|
605
|
-
|
|
606
|
-
|
|
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");
|
|
607
674
|
});
|
|
608
675
|
|
|
609
|
-
it("
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
+
});
|
|
625
716
|
|
|
626
|
-
|
|
627
|
-
|
|
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
|
+
}
|
|
628
728
|
});
|
|
629
729
|
});
|
|
630
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.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 {
|
|
@@ -464,12 +483,10 @@ function matchModel(
|
|
|
464
483
|
context: ModelPreferenceContext,
|
|
465
484
|
options?: { modelRegistry?: CanonicalModelRegistry },
|
|
466
485
|
): Model<Api> | undefined {
|
|
467
|
-
// Explicit provider/model selectors always bypass canonical coalescing.
|
|
468
486
|
const exactRefMatch = findExactModelReferenceMatch(modelPattern, availableModels);
|
|
469
487
|
if (exactRefMatch) {
|
|
470
488
|
return exactRefMatch;
|
|
471
489
|
}
|
|
472
|
-
|
|
473
490
|
// Exact canonical ids coalesce provider variants before bare-id matching.
|
|
474
491
|
const exactCanonicalMatch = findExactCanonicalModelMatch(modelPattern, availableModels, options?.modelRegistry);
|
|
475
492
|
if (exactCanonicalMatch) {
|
|
@@ -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,
|
package/src/eval/agent-bridge.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { resolveAgentModelPatterns } from "../config/model-resolver";
|
|
|
10
10
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
11
11
|
import { MCPManager } from "../mcp/manager";
|
|
12
12
|
import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" };
|
|
13
|
+
import { MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
13
14
|
import * as taskDiscovery from "../task/discovery";
|
|
14
15
|
import * as taskExecutor from "../task/executor";
|
|
15
16
|
import { AgentOutputManager } from "../task/output-manager";
|
|
@@ -288,6 +289,7 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
|
|
|
288
289
|
parentHindsightSessionState: options.session.getHindsightSessionState?.(),
|
|
289
290
|
parentMnemopiSessionState: options.session.getMnemopiSessionState?.(),
|
|
290
291
|
parentTelemetry: options.session.getTelemetry?.(),
|
|
292
|
+
parentAgentId: options.session.getAgentId?.() ?? MAIN_AGENT_ID,
|
|
291
293
|
// Deliberately omit parentEvalSessionId: the parent's Python kernel is
|
|
292
294
|
// blocked on this bridge call, so sharing the eval session would deadlock
|
|
293
295
|
// (subagent queues behind the parent's in-flight execution, parent waits
|
|
@@ -564,7 +564,7 @@ function spawnInlineWorker(): WorkerHandle {
|
|
|
564
564
|
},
|
|
565
565
|
close: () => {},
|
|
566
566
|
};
|
|
567
|
-
new WorkerCore(workerTransport);
|
|
567
|
+
const core = new WorkerCore(workerTransport);
|
|
568
568
|
return {
|
|
569
569
|
mode: "inline",
|
|
570
570
|
send: msg =>
|
|
@@ -600,6 +600,7 @@ function spawnInlineWorker(): WorkerHandle {
|
|
|
600
600
|
async terminate() {
|
|
601
601
|
hostListeners.clear();
|
|
602
602
|
workerListeners.clear();
|
|
603
|
+
core.dispose();
|
|
603
604
|
},
|
|
604
605
|
};
|
|
605
606
|
}
|