@oh-my-pi/pi-coding-agent 15.9.0 → 15.9.1
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 +39 -0
- package/dist/types/cli/update-cli.d.ts +15 -1
- package/dist/types/config/append-only-context-mode.d.ts +8 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +15 -0
- package/dist/types/config/settings-schema.d.ts +3 -3
- package/dist/types/exa/mcp-client.d.ts +2 -1
- package/dist/types/mcp/json-rpc.d.ts +6 -1
- package/dist/types/mcp/tool-bridge.d.ts +4 -0
- package/dist/types/mnemopi/state.d.ts +2 -2
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
- package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +40 -8
- package/dist/types/modes/components/session-selector.d.ts +8 -3
- package/dist/types/modes/components/settings-selector.d.ts +1 -1
- package/dist/types/modes/utils/keybinding-matchers.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +4 -1
- package/dist/types/session/history-storage.d.ts +3 -4
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +1 -0
- package/dist/types/slash-commands/types.d.ts +17 -4
- package/dist/types/tiny/text.d.ts +17 -0
- package/dist/types/web/search/providers/base.d.ts +14 -0
- package/dist/types/web/search/providers/exa.d.ts +9 -0
- package/dist/types/web/search/providers/perplexity.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +2 -1
- package/package.json +9 -9
- package/src/cli/session-picker.ts +1 -0
- package/src/cli/update-cli.ts +54 -2
- package/src/commands/completions.ts +1 -1
- package/src/config/append-only-context-mode.ts +37 -0
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +2 -2
- package/src/exa/mcp-client.ts +11 -5
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/main.ts +4 -2
- package/src/mcp/json-rpc.ts +8 -0
- package/src/mcp/render.ts +3 -0
- package/src/mcp/tool-bridge.ts +10 -2
- package/src/mcp/transports/http.ts +33 -16
- package/src/mnemopi/state.ts +4 -4
- package/src/modes/acp/acp-agent.ts +168 -3
- package/src/modes/components/agent-dashboard.ts +103 -31
- package/src/modes/components/extensions/extension-dashboard.ts +56 -10
- package/src/modes/components/history-search.ts +128 -14
- package/src/modes/components/plugin-settings.ts +270 -36
- package/src/modes/components/session-selector.ts +45 -14
- package/src/modes/components/settings-selector.ts +1 -1
- package/src/modes/components/tips.txt +5 -1
- package/src/modes/components/transcript-container.ts +22 -4
- package/src/modes/controllers/command-controller.ts +4 -3
- package/src/modes/controllers/input-controller.ts +10 -5
- package/src/modes/controllers/selector-controller.ts +30 -19
- package/src/modes/interactive-mode.ts +38 -3
- package/src/modes/utils/keybinding-matchers.ts +10 -0
- package/src/prompts/steering/user-interjection.md +10 -0
- package/src/prompts/system/agent-creation-architect.md +1 -26
- package/src/prompts/system/system-prompt.md +143 -145
- package/src/prompts/system/title-system.md +3 -2
- package/src/prompts/tools/browser.md +29 -29
- package/src/prompts/tools/render-mermaid.md +2 -2
- package/src/sdk.ts +5 -21
- package/src/session/agent-session.ts +30 -7
- package/src/session/history-storage.ts +11 -18
- package/src/session/messages.ts +80 -0
- package/src/session/session-manager.ts +7 -1
- package/src/slash-commands/types.ts +27 -10
- package/src/tiny/text.ts +112 -1
- package/src/tools/memory-recall.ts +1 -1
- package/src/tools/memory-reflect.ts +1 -1
- package/src/tools/ssh.ts +26 -10
- package/src/tools/write.ts +14 -2
- package/src/tui/status-line.ts +15 -4
- package/src/utils/title-generator.ts +9 -2
- package/src/web/search/index.ts +3 -1
- package/src/web/search/provider.ts +1 -1
- package/src/web/search/providers/base.ts +17 -0
- package/src/web/search/providers/exa.ts +111 -7
- package/src/web/search/providers/perplexity.ts +8 -4
- package/src/web/search/types.ts +2 -1
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
Drives
|
|
1
|
+
Drives real Chromium tab; full puppeteer access via JS execution.
|
|
2
2
|
|
|
3
3
|
<instruction>
|
|
4
|
-
- For static web content (articles, docs, issues/PRs, JSON, PDFs, feeds), prefer
|
|
4
|
+
- For static web content (articles, docs, issues/PRs, JSON, PDFs, feeds), prefer `read` tool with URL — reader-mode text without spinning up browser. Use this tool when Need JS execution, authentication, or interactive actions.
|
|
5
5
|
- Three actions only:
|
|
6
|
-
- `open` — acquire
|
|
7
|
-
- `close` — release
|
|
8
|
-
- `run` — execute JS against
|
|
6
|
+
- `open` — acquire or reuse named tab. `name` defaults `"main"`. Optional `url` navigates after tab ready. Optional `viewport` sets dimensions. Optional `dialogs: "accept" | "dismiss"` auto-handles `alert`/`confirm`/`beforeunload` so navigation/clicks don't hang (default: leave dialogs unhandled — page hangs until caller wires `page.on('dialog', …)`).
|
|
7
|
+
- `close` — release tab by `name`, or every tab with `all: true`. For spawned-app browsers, set `kill: true` to terminate process tree (default leaves running).
|
|
8
|
+
- `run` — execute JS against existing tab. `code` is body of async function with `page`, `browser`, `tab`, `display`, `assert`, `wait` in scope. Function's return value JSON-stringified into tool result; multiple `display(value)` calls accumulate text/images.
|
|
9
9
|
- Tabs survive across `run` calls and across in-process subagents. Open once, reuse many times.
|
|
10
|
-
- Browser kinds, selected by
|
|
10
|
+
- Browser kinds, selected by `app` field on `open`:
|
|
11
11
|
- default (no `app`) → headless Chromium with stealth patches.
|
|
12
|
-
- `app.path` → spawn
|
|
13
|
-
- `app.cdp_url` → connect to
|
|
14
|
-
- `app.target` (with `path`/`cdp_url`) — substring matched against url+title to pick
|
|
15
|
-
- Inside `run`, `tab` exposes high-level helpers; reach for `page` (raw puppeteer Page) when
|
|
16
|
-
- `tab.goto(url, { waitUntil? })` — clears
|
|
17
|
-
- `tab.observe({ includeAll?, viewportOnly? })` — accessibility snapshot. Returns `{ url, title, viewport, scroll, elements: [{ id, role, name, value, states, … }] }`. Element ids
|
|
18
|
-
- `tab.id(n)` — resolves
|
|
12
|
+
- `app.path` → spawn absolute binary (Electron/CDP). If running instance already exposes CDP port, reused; otherwise stale instances killed, fresh one spawned. No stealth patches — NEVER tamper with real desktop app.
|
|
13
|
+
- `app.cdp_url` → connect to existing CDP endpoint (e.g. `http://127.0.0.1:9222`).
|
|
14
|
+
- `app.target` (with `path`/`cdp_url`) — substring matched against url+title to pick BrowserWindow when app exposes several.
|
|
15
|
+
- Inside `run`, `tab` exposes high-level helpers; reach for `page` (raw puppeteer Page) when Need anything they don't cover.
|
|
16
|
+
- `tab.goto(url, { waitUntil? })` — clears element cache and navigates.
|
|
17
|
+
- `tab.observe({ includeAll?, viewportOnly? })` — accessibility snapshot. Returns `{ url, title, viewport, scroll, elements: [{ id, role, name, value, states, … }] }`. Element ids stable until next observe/goto.
|
|
18
|
+
- `tab.id(n)` — resolves element id from most recent observe to real `ElementHandle` you can `.click()`, `.type()`, etc.
|
|
19
19
|
- `tab.click(selector)` / `tab.type(selector, text)` / `tab.fill(selector, value)` / `tab.press(key, { selector? })` / `tab.scroll(dx, dy)` — selector-based actions.
|
|
20
|
-
- `tab.waitFor(selector)` — waits until
|
|
21
|
-
- `tab.drag(from, to)` — drag from one point to another. Each endpoint
|
|
22
|
-
- `tab.scrollIntoView(selector)` — scroll
|
|
23
|
-
- `tab.select(selector, …values)` — set
|
|
24
|
-
- `tab.uploadFile(selector, …filePaths)` — attach files to
|
|
25
|
-
- `tab.waitForUrl(pattern, { timeout? })` — pattern
|
|
26
|
-
- `tab.waitForResponse(pattern, { timeout? })` — pattern
|
|
27
|
-
- `tab.evaluate(fn, …args)` — sugar for `page.evaluate` with
|
|
28
|
-
- `tab.screenshot({ selector?, fullPage?, save?, silent? })` — captures
|
|
20
|
+
- `tab.waitFor(selector)` — waits until selector attached, returns resolved `ElementHandle` for chaining (e.g. `const btn = await tab.waitFor('text/Submit'); await btn.click();`).
|
|
21
|
+
- `tab.drag(from, to)` — drag from one point to another. Each endpoint either selector string (drag center-to-center) or `{ x, y }` viewport-coordinate point (for canvases, sliders).
|
|
22
|
+
- `tab.scrollIntoView(selector)` — scroll matching element to center of viewport (use before clicking off-screen elements).
|
|
23
|
+
- `tab.select(selector, …values)` — set selected option(s) on `<select>`. Returns values that ended up selected. `tab.fill` NEVER works for selects.
|
|
24
|
+
- `tab.uploadFile(selector, …filePaths)` — attach files to `<input type="file">`. Paths resolve relative to cwd.
|
|
25
|
+
- `tab.waitForUrl(pattern, { timeout? })` — pattern substring or `RegExp`. Polls `location.href` so works for SPA pushState navigations, not just real navigations. Returns matched URL.
|
|
26
|
+
- `tab.waitForResponse(pattern, { timeout? })` — pattern substring, `RegExp`, or `(response) => boolean`. Returns raw puppeteer `HTTPResponse` (call `.text()` / `.json()` / `.status()` / `.headers()` on it).
|
|
27
|
+
- `tab.evaluate(fn, …args)` — sugar for `page.evaluate` with abort signal already wired. Use this instead of dropping to `page.evaluate` for ad-hoc DOM reads.
|
|
28
|
+
- `tab.screenshot({ selector?, fullPage?, save?, silent? })` — captures screenshot and **auto-attaches to tool output for you to view** (unless `silent: true`). `save` is **strictly optional**: OMIT when you just want to look at page — downscaled image shown regardless, full-res capture written to temp file automatically. Pass `save` (a path) ONLY when deliberately need to keep full-res copy on disk for later use; `browser.screenshotDir` does same for every shot. NEVER invent `save` path for throwaway/temporal screenshot.
|
|
29
29
|
- `tab.extract(format = "markdown")` — Readability-extracted page content.
|
|
30
|
-
- Selectors accept CSS
|
|
31
|
-
- Default
|
|
30
|
+
- Selectors accept CSS plus puppeteer query handlers: `aria/Sign in`, `text/Continue`, `xpath/…`, `pierce/…`. Playwright-style `p-aria/[name="…"]`, `p-text/…` normalized.
|
|
31
|
+
- Default `tab.observe()` over `tab.screenshot()` for page state. Screenshot only when visual appearance matters.
|
|
32
32
|
</instruction>
|
|
33
33
|
|
|
34
34
|
<critical>
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
- After
|
|
38
|
-
- `code` runs with full Node access. Treat
|
|
35
|
+
- MUST call `open` before `run`. `run` does not implicitly create tab.
|
|
36
|
+
- NEVER screenshot just to "see what's on page" — `tab.observe()` returns structured data with element ids you can act on immediately.
|
|
37
|
+
- After `tab.goto()` or any navigation, prior element ids from `tab.observe()` invalidated. Re-observe before referencing them.
|
|
38
|
+
- `code` runs with full Node access. Treat as your code, not sandboxed code.
|
|
39
39
|
</critical>
|
|
40
40
|
|
|
41
41
|
<examples>
|
|
@@ -69,5 +69,5 @@ Drives a real Chromium tab with full puppeteer access via JS execution.
|
|
|
69
69
|
</examples>
|
|
70
70
|
|
|
71
71
|
<output>
|
|
72
|
-
- Per call: any `display(value)` outputs (text/images) followed by
|
|
72
|
+
- Per call: any `display(value)` outputs (text/images) followed by JSON-stringified return value of `code` function. `run` always produces at least status line.
|
|
73
73
|
</output>
|
|
@@ -5,5 +5,5 @@ Parameters:
|
|
|
5
5
|
- `config` (optional): JSON render configuration (spacing and layout options).
|
|
6
6
|
Behavior:
|
|
7
7
|
- Returns ASCII diagram text.
|
|
8
|
-
- Saves full output to `artifact://<id>` when storage
|
|
9
|
-
- Returns error when Mermaid input
|
|
8
|
+
- Saves full output to `artifact://<id>` when storage available.
|
|
9
|
+
- Returns error when Mermaid input invalid or rendering fails.
|
package/src/sdk.ts
CHANGED
|
@@ -39,6 +39,7 @@ import { createAutoresearchExtension } from "./autoresearch";
|
|
|
39
39
|
import { loadCapability } from "./capability";
|
|
40
40
|
import { type Rule, ruleCapability, setActiveRules } from "./capability/rule";
|
|
41
41
|
import { bucketRules } from "./capability/rule-buckets";
|
|
42
|
+
import { shouldEnableAppendOnlyContext } from "./config/append-only-context-mode";
|
|
42
43
|
import { ModelRegistry } from "./config/model-registry";
|
|
43
44
|
import {
|
|
44
45
|
formatModelString,
|
|
@@ -101,7 +102,7 @@ import {
|
|
|
101
102
|
import { AgentSession } from "./session/agent-session";
|
|
102
103
|
import { resolveAuthBrokerConfig } from "./session/auth-broker-config";
|
|
103
104
|
import { AuthBrokerClient, AuthStorage, RemoteAuthCredentialStore } from "./session/auth-storage";
|
|
104
|
-
import { type CustomMessage, convertToLlm } from "./session/messages";
|
|
105
|
+
import { type CustomMessage, convertToLlm, wrapSteeringForModel } from "./session/messages";
|
|
105
106
|
import { getRestorableSessionModels, SessionManager } from "./session/session-manager";
|
|
106
107
|
import { closeAllConnections } from "./ssh/connection-manager";
|
|
107
108
|
import { unmountAll } from "./ssh/sshfs-mount";
|
|
@@ -646,24 +647,6 @@ function registerPythonCleanup(): void {
|
|
|
646
647
|
postmortem.register("python-cleanup", disposeAllKernelSessions);
|
|
647
648
|
}
|
|
648
649
|
|
|
649
|
-
/**
|
|
650
|
-
* Resolve whether to enable append-only context mode based on the setting and provider.
|
|
651
|
-
*
|
|
652
|
-
* - `"on"` → always enable
|
|
653
|
-
* - `"off"` → never enable
|
|
654
|
-
* - `"auto"` → enable for DeepSeek (prefix-caching provider)
|
|
655
|
-
*/
|
|
656
|
-
function resolveAppendOnlyMode(setting: "auto" | "on" | "off" | undefined, provider: string): boolean {
|
|
657
|
-
switch (setting ?? "auto") {
|
|
658
|
-
case "on":
|
|
659
|
-
return true;
|
|
660
|
-
case "off":
|
|
661
|
-
return false;
|
|
662
|
-
default:
|
|
663
|
-
return provider === "deepseek";
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
650
|
function customToolToDefinition(tool: CustomTool): ToolDefinition {
|
|
668
651
|
const definition: ToolDefinition & { [TOOL_DEFINITION_MARKER]: true } = {
|
|
669
652
|
name: tool.name,
|
|
@@ -1905,7 +1888,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1905
1888
|
return obfuscateMessages(obfuscator, converted);
|
|
1906
1889
|
};
|
|
1907
1890
|
const transformContext = async (messages: AgentMessage[], _signal?: AbortSignal) => {
|
|
1908
|
-
|
|
1891
|
+
const withContext = await extensionRunner.emitContext(messages);
|
|
1892
|
+
return wrapSteeringForModel(withContext);
|
|
1909
1893
|
};
|
|
1910
1894
|
const onPayload = async (payload: unknown, _model?: Model) => {
|
|
1911
1895
|
return await extensionRunner.emitBeforeProviderRequest(payload);
|
|
@@ -2028,7 +2012,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2028
2012
|
getToolChoice: () => session?.nextToolChoice(),
|
|
2029
2013
|
telemetry: options.telemetry,
|
|
2030
2014
|
appendOnlyContext: model
|
|
2031
|
-
?
|
|
2015
|
+
? shouldEnableAppendOnlyContext(settings.get("provider.appendOnlyContext"), model)
|
|
2032
2016
|
? new AppendOnlyContextManager()
|
|
2033
2017
|
: undefined
|
|
2034
2018
|
: undefined,
|
|
@@ -100,6 +100,7 @@ import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../a
|
|
|
100
100
|
import { classifyDifficulty } from "../auto-thinking/classifier";
|
|
101
101
|
import { reset as resetCapabilities } from "../capability";
|
|
102
102
|
import type { Rule } from "../capability/rule";
|
|
103
|
+
import { shouldEnableAppendOnlyContext } from "../config/append-only-context-mode";
|
|
103
104
|
import { MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry";
|
|
104
105
|
import {
|
|
105
106
|
extractExplicitThinkingSelector,
|
|
@@ -232,7 +233,7 @@ import type {
|
|
|
232
233
|
SessionContext,
|
|
233
234
|
SessionManager,
|
|
234
235
|
} from "./session-manager";
|
|
235
|
-
import { getLatestCompactionEntry, getRestorableSessionModels } from "./session-manager";
|
|
236
|
+
import { EPHEMERAL_MODEL_CHANGE_ROLE, getLatestCompactionEntry, getRestorableSessionModels } from "./session-manager";
|
|
236
237
|
import type { ShakeMode, ShakeResult } from "./shake-types";
|
|
237
238
|
import { ToolChoiceQueue } from "./tool-choice-queue";
|
|
238
239
|
import { YieldQueue } from "./yield-queue";
|
|
@@ -1293,6 +1294,12 @@ export class AgentSession {
|
|
|
1293
1294
|
this.#standingResolveHandler = handler ?? undefined;
|
|
1294
1295
|
}
|
|
1295
1296
|
|
|
1297
|
+
#sessionSwitchReconciler: (() => Promise<void>) | undefined;
|
|
1298
|
+
|
|
1299
|
+
setSessionSwitchReconciler(reconciler: (() => Promise<void>) | null): void {
|
|
1300
|
+
this.#sessionSwitchReconciler = reconciler ?? undefined;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1296
1303
|
/** Provider-scoped mutable state store for transport/session caches. */
|
|
1297
1304
|
get providerSessionState(): Map<string, ProviderSessionState> {
|
|
1298
1305
|
return this.#providerSessionState;
|
|
@@ -4639,6 +4646,7 @@ export class AgentSession {
|
|
|
4639
4646
|
this.agent.steer({
|
|
4640
4647
|
role: "user",
|
|
4641
4648
|
content,
|
|
4649
|
+
steering: true,
|
|
4642
4650
|
attribution: "user",
|
|
4643
4651
|
timestamp: Date.now(),
|
|
4644
4652
|
});
|
|
@@ -5225,7 +5233,11 @@ export class AgentSession {
|
|
|
5225
5233
|
* Validates API key, saves to session log but NOT to settings.
|
|
5226
5234
|
* @throws Error if no API key available for the model
|
|
5227
5235
|
*/
|
|
5228
|
-
async setModelTemporary(
|
|
5236
|
+
async setModelTemporary(
|
|
5237
|
+
model: Model,
|
|
5238
|
+
thinkingLevel?: ThinkingLevel,
|
|
5239
|
+
options?: { ephemeral?: boolean },
|
|
5240
|
+
): Promise<void> {
|
|
5229
5241
|
const previousEditMode = this.#resolveActiveEditMode();
|
|
5230
5242
|
const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
|
|
5231
5243
|
if (!apiKey) {
|
|
@@ -5234,7 +5246,10 @@ export class AgentSession {
|
|
|
5234
5246
|
|
|
5235
5247
|
this.#clearActiveRetryFallback();
|
|
5236
5248
|
this.#setModelWithProviderSessionReset(model);
|
|
5237
|
-
this.sessionManager.appendModelChange(
|
|
5249
|
+
this.sessionManager.appendModelChange(
|
|
5250
|
+
`${model.provider}/${model.id}`,
|
|
5251
|
+
options?.ephemeral ? EPHEMERAL_MODEL_CHANGE_ROLE : "temporary",
|
|
5252
|
+
);
|
|
5238
5253
|
this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
5239
5254
|
|
|
5240
5255
|
// Apply explicit thinking level if given; otherwise prefer the model's
|
|
@@ -6682,7 +6697,7 @@ export class AgentSession {
|
|
|
6682
6697
|
if (!targetModel) return false;
|
|
6683
6698
|
|
|
6684
6699
|
try {
|
|
6685
|
-
await this.setModelTemporary(targetModel);
|
|
6700
|
+
await this.setModelTemporary(targetModel, undefined, { ephemeral: true });
|
|
6686
6701
|
logger.debug("Context promotion switched model on overflow", {
|
|
6687
6702
|
from: `${currentModel.provider}/${currentModel.id}`,
|
|
6688
6703
|
to: `${targetModel.provider}/${targetModel.id}`,
|
|
@@ -6735,8 +6750,8 @@ export class AgentSession {
|
|
|
6735
6750
|
*/
|
|
6736
6751
|
#syncAppendOnlyContext(model: Model | null | undefined): void {
|
|
6737
6752
|
const setting = this.settings.get("provider.appendOnlyContext") ?? "auto";
|
|
6753
|
+
const enable = shouldEnableAppendOnlyContext(setting, model);
|
|
6738
6754
|
const providerId = model?.provider;
|
|
6739
|
-
const enable = setting === "on" || (setting === "auto" && providerId === "deepseek");
|
|
6740
6755
|
const prev = this.#lastAppendOnlyResolution;
|
|
6741
6756
|
if (prev && prev.enable === enable && prev.providerId === providerId) return;
|
|
6742
6757
|
this.#lastAppendOnlyResolution = { enable, providerId };
|
|
@@ -7848,7 +7863,7 @@ export class AgentSession {
|
|
|
7848
7863
|
const nextThinkingLevel = selector.thinkingLevel ?? currentThinkingLevel;
|
|
7849
7864
|
|
|
7850
7865
|
this.#setModelWithProviderSessionReset(candidate);
|
|
7851
|
-
this.sessionManager.appendModelChange(`${candidate.provider}/${candidate.id}`,
|
|
7866
|
+
this.sessionManager.appendModelChange(`${candidate.provider}/${candidate.id}`, EPHEMERAL_MODEL_CHANGE_ROLE);
|
|
7852
7867
|
this.settings.getStorage()?.recordModelUsage(`${candidate.provider}/${candidate.id}`);
|
|
7853
7868
|
this.setThinkingLevel(nextThinkingLevel);
|
|
7854
7869
|
if (!this.#activeRetryFallback) {
|
|
@@ -7921,7 +7936,7 @@ export class AgentSession {
|
|
|
7921
7936
|
const thinkingToApply =
|
|
7922
7937
|
currentThinkingLevel === lastAppliedFallbackThinkingLevel ? originalThinkingLevel : currentThinkingLevel;
|
|
7923
7938
|
this.#setModelWithProviderSessionReset(primaryModel);
|
|
7924
|
-
this.sessionManager.appendModelChange(`${primaryModel.provider}/${primaryModel.id}`,
|
|
7939
|
+
this.sessionManager.appendModelChange(`${primaryModel.provider}/${primaryModel.id}`, EPHEMERAL_MODEL_CHANGE_ROLE);
|
|
7925
7940
|
this.settings.getStorage()?.recordModelUsage(`${primaryModel.provider}/${primaryModel.id}`);
|
|
7926
7941
|
this.setThinkingLevel(thinkingToApply);
|
|
7927
7942
|
this.#clearActiveRetryFallback();
|
|
@@ -8948,6 +8963,14 @@ export class AgentSession {
|
|
|
8948
8963
|
this.#resetMnemopiConversationTrackingIfMnemopi();
|
|
8949
8964
|
}
|
|
8950
8965
|
this.#reconnectToAgent();
|
|
8966
|
+
try {
|
|
8967
|
+
await this.#sessionSwitchReconciler?.();
|
|
8968
|
+
} catch (error) {
|
|
8969
|
+
logger.warn("Failed to reconcile session mode after switch", {
|
|
8970
|
+
targetSessionFile: sessionPath,
|
|
8971
|
+
error: String(error),
|
|
8972
|
+
});
|
|
8973
|
+
}
|
|
8951
8974
|
return true;
|
|
8952
8975
|
} catch (error) {
|
|
8953
8976
|
this.sessionManager.restoreState(previousSessionState);
|
|
@@ -209,10 +209,6 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
|
|
|
209
209
|
logger.debug("HistoryStorage FTS query failed, using substring only", { error: String(error) });
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
if (ftsRows.length >= safeLimit) {
|
|
213
|
-
return ftsRows.map(row => this.#toEntry(row));
|
|
214
|
-
}
|
|
215
|
-
|
|
216
212
|
// 2. Substring fallback (token-AND LIKE). Catches infix matches FTS5's
|
|
217
213
|
// prefix-only wildcard cannot reach (e.g. "mit" -> "commit"). Bounded
|
|
218
214
|
// by safeLimit, ordered by recency - no full-table load into JS.
|
|
@@ -227,27 +223,24 @@ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
|
|
|
227
223
|
return subRows.map(row => this.#toEntry(row));
|
|
228
224
|
}
|
|
229
225
|
|
|
230
|
-
const
|
|
231
|
-
const merged: HistoryEntry[] = [];
|
|
226
|
+
const rowsById = new Map<number, HistoryRow>();
|
|
232
227
|
for (const row of ftsRows) {
|
|
233
|
-
|
|
234
|
-
seen.add(row.id);
|
|
235
|
-
merged.push(this.#toEntry(row));
|
|
228
|
+
rowsById.set(row.id, row);
|
|
236
229
|
}
|
|
237
230
|
for (const row of subRows) {
|
|
238
|
-
if (
|
|
239
|
-
if (seen.has(row.id)) continue;
|
|
240
|
-
seen.add(row.id);
|
|
241
|
-
merged.push(this.#toEntry(row));
|
|
231
|
+
if (!rowsById.has(row.id)) rowsById.set(row.id, row);
|
|
242
232
|
}
|
|
243
|
-
|
|
233
|
+
|
|
234
|
+
return [...rowsById.values()]
|
|
235
|
+
.sort((a, b) => b.created_at - a.created_at || b.id - a.id)
|
|
236
|
+
.slice(0, safeLimit)
|
|
237
|
+
.map(row => this.#toEntry(row));
|
|
244
238
|
}
|
|
245
239
|
|
|
246
240
|
/**
|
|
247
|
-
* IDs of the sessions whose stored prompts match `query`, ordered by
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
* resume picker with prompts that the 4KB session-list prefix never sees.
|
|
241
|
+
* IDs of the sessions whose stored prompts match `query`, ordered by prompt
|
|
242
|
+
* recency and de-duplicated. Used to augment session ranking in the resume
|
|
243
|
+
* picker with prompts that the 4KB session-list prefix never sees.
|
|
251
244
|
*/
|
|
252
245
|
matchingSessionIds(query: string, limit = 500): string[] {
|
|
253
246
|
const seen = new Set<string>();
|
package/src/session/messages.ts
CHANGED
|
@@ -18,7 +18,10 @@ import type {
|
|
|
18
18
|
MessageAttribution,
|
|
19
19
|
TextContent,
|
|
20
20
|
ToolResultMessage,
|
|
21
|
+
UserMessage,
|
|
21
22
|
} from "@oh-my-pi/pi-ai";
|
|
23
|
+
import { prompt } from "@oh-my-pi/pi-utils";
|
|
24
|
+
import userInterjectionTemplate from "../prompts/steering/user-interjection.md" with { type: "text" };
|
|
22
25
|
|
|
23
26
|
export {
|
|
24
27
|
type BranchSummaryMessage,
|
|
@@ -105,6 +108,83 @@ export function stripInternalDetailsFields<T>(details: T | undefined): T | undef
|
|
|
105
108
|
return cleaned as T;
|
|
106
109
|
}
|
|
107
110
|
|
|
111
|
+
function isSteeringUserMessage(message: AgentMessage | undefined): message is UserMessage & { steering: true } {
|
|
112
|
+
return message?.role === "user" && message.steering === true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function userMessageWithoutSteering(message: UserMessage): UserMessage {
|
|
116
|
+
const { steering, ...rest } = message;
|
|
117
|
+
void steering;
|
|
118
|
+
return rest;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function renderSteeringEnvelope(message: string): string {
|
|
122
|
+
return prompt.render(userInterjectionTemplate, { message });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getArrayContentText(content: (TextContent | ImageContent)[]): string {
|
|
126
|
+
let firstText: string | undefined;
|
|
127
|
+
let textParts: string[] | undefined;
|
|
128
|
+
for (const part of content) {
|
|
129
|
+
if (part.type !== "text") continue;
|
|
130
|
+
if (firstText === undefined) {
|
|
131
|
+
firstText = part.text;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (textParts === undefined) {
|
|
135
|
+
textParts = [firstText];
|
|
136
|
+
}
|
|
137
|
+
textParts.push(part.text);
|
|
138
|
+
}
|
|
139
|
+
return textParts === undefined ? (firstText ?? "") : textParts.join("\n");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getArrayContentImages(content: (TextContent | ImageContent)[]): ImageContent[] {
|
|
143
|
+
let images: ImageContent[] | undefined;
|
|
144
|
+
for (const part of content) {
|
|
145
|
+
if (part.type !== "image") continue;
|
|
146
|
+
if (images === undefined) images = [];
|
|
147
|
+
images.push(part);
|
|
148
|
+
}
|
|
149
|
+
return images ?? [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function wrapSteeringUserMessage(message: UserMessage): UserMessage {
|
|
153
|
+
if (typeof message.content === "string") {
|
|
154
|
+
if (message.content.length === 0) return message;
|
|
155
|
+
return { ...userMessageWithoutSteering(message), content: renderSteeringEnvelope(message.content) };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const text = getArrayContentText(message.content);
|
|
159
|
+
if (text.length === 0) return message;
|
|
160
|
+
const content: (TextContent | ImageContent)[] = [{ type: "text", text: renderSteeringEnvelope(text) }];
|
|
161
|
+
content.push(...getArrayContentImages(message.content));
|
|
162
|
+
return { ...userMessageWithoutSteering(message), content };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function wrapSteeringForModel(messages: AgentMessage[]): AgentMessage[] {
|
|
166
|
+
const last = messages[messages.length - 1];
|
|
167
|
+
if (!isSteeringUserMessage(last)) return messages;
|
|
168
|
+
|
|
169
|
+
let firstSteer = messages.length - 1;
|
|
170
|
+
while (firstSteer > 0 && isSteeringUserMessage(messages[firstSteer - 1])) {
|
|
171
|
+
firstSteer--;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let wrappedMessages: AgentMessage[] | undefined;
|
|
175
|
+
for (let i = firstSteer; i < messages.length; i++) {
|
|
176
|
+
const message = messages[i];
|
|
177
|
+
if (!isSteeringUserMessage(message)) continue;
|
|
178
|
+
const wrappedMessage = wrapSteeringUserMessage(message);
|
|
179
|
+
if (wrappedMessage === message) continue;
|
|
180
|
+
if (wrappedMessages === undefined) {
|
|
181
|
+
wrappedMessages = messages.slice();
|
|
182
|
+
}
|
|
183
|
+
wrappedMessages[i] = wrappedMessage;
|
|
184
|
+
}
|
|
185
|
+
return wrappedMessages ?? messages;
|
|
186
|
+
}
|
|
187
|
+
|
|
108
188
|
function getPrunedToolResultContent(message: ToolResultMessage): (TextContent | ImageContent)[] {
|
|
109
189
|
if (message.prunedAt === undefined) {
|
|
110
190
|
return message.content;
|
|
@@ -254,13 +254,19 @@ export interface SessionContext {
|
|
|
254
254
|
modeData?: Record<string, unknown>;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
export const EPHEMERAL_MODEL_CHANGE_ROLE = "fallback";
|
|
258
|
+
|
|
257
259
|
/** Lists session model strings to try when restoring, in fallback order. */
|
|
258
260
|
export function getRestorableSessionModels(
|
|
259
261
|
models: Readonly<Record<string, string>>,
|
|
260
262
|
lastModelChangeRole: string | undefined,
|
|
261
263
|
): string[] {
|
|
262
264
|
const defaultModel = models.default;
|
|
263
|
-
if (
|
|
265
|
+
if (
|
|
266
|
+
!lastModelChangeRole ||
|
|
267
|
+
lastModelChangeRole === "default" ||
|
|
268
|
+
lastModelChangeRole === EPHEMERAL_MODEL_CHANGE_ROLE
|
|
269
|
+
) {
|
|
264
270
|
return defaultModel ? [defaultModel] : [];
|
|
265
271
|
}
|
|
266
272
|
|
|
@@ -31,7 +31,11 @@ export interface ParsedSlashCommand {
|
|
|
31
31
|
/**
|
|
32
32
|
* Result returned by a slash-command handler.
|
|
33
33
|
*
|
|
34
|
-
* - `
|
|
34
|
+
* - `undefined` (and the implicit `void` return) — command was handled and
|
|
35
|
+
* consumed; no further input. Handlers may simply omit a `return` rather than
|
|
36
|
+
* building `{ consumed: true }`; `void` is accepted in the handler signatures
|
|
37
|
+
* below so the contract typechecks under TypeScript 5.x (which does not
|
|
38
|
+
* coerce `() => void` to `() => T | undefined`) as well as 6.x / tsgo.
|
|
35
39
|
* - `{ consumed: true }` — explicit equivalent of the above (ACP shape).
|
|
36
40
|
* - `{ prompt: string }` — command handled, pass `prompt` through as the new
|
|
37
41
|
* user input (e.g. `/force <tool> <prompt>` keeps `<prompt>` as the message).
|
|
@@ -100,20 +104,33 @@ export interface SlashCommandSpec extends BuiltinSlashCommand {
|
|
|
100
104
|
/**
|
|
101
105
|
* Text/ACP-mode handler. The same body is invoked from the ACP dispatcher
|
|
102
106
|
* and, via the TUI adapter, when no `handleTui` override is provided.
|
|
107
|
+
*
|
|
108
|
+
* Expressed as a union of two function types — one returning a
|
|
109
|
+
* `SlashCommandResult`, one returning `void` — so handlers that simply
|
|
110
|
+
* `return` (or omit the return) typecheck under TypeScript 5.x as well as
|
|
111
|
+
* 6.x / tsgo. TS 5.x does not coerce a `() => void` value into a
|
|
112
|
+
* `() => T | undefined` slot, so the two return shapes must be siblings
|
|
113
|
+
* rather than a `T | void` union inside one function type (which Biome's
|
|
114
|
+
* `noConfusingVoidType` also rejects).
|
|
103
115
|
*/
|
|
104
|
-
handle?:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
116
|
+
handle?:
|
|
117
|
+
| ((
|
|
118
|
+
command: ParsedSlashCommand,
|
|
119
|
+
runtime: SlashCommandRuntime,
|
|
120
|
+
) => SlashCommandResult | Promise<SlashCommandResult>)
|
|
121
|
+
| ((command: ParsedSlashCommand, runtime: SlashCommandRuntime) => void | Promise<void>);
|
|
108
122
|
/**
|
|
109
123
|
* TUI-only handler that supersedes `handle` when both are present. Use for
|
|
110
124
|
* selectors, wizards, dashboards, and anything else that requires
|
|
111
|
-
* `InteractiveModeContext`.
|
|
125
|
+
* `InteractiveModeContext`. See `handle` for the rationale behind the
|
|
126
|
+
* function-type union shape.
|
|
112
127
|
*/
|
|
113
|
-
handleTui?:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
128
|
+
handleTui?:
|
|
129
|
+
| ((
|
|
130
|
+
command: ParsedSlashCommand,
|
|
131
|
+
runtime: TuiSlashCommandRuntime,
|
|
132
|
+
) => SlashCommandResult | Promise<SlashCommandResult>)
|
|
133
|
+
| ((command: ParsedSlashCommand, runtime: TuiSlashCommandRuntime) => void | Promise<void>);
|
|
117
134
|
}
|
|
118
135
|
|
|
119
136
|
/** Result returned by `executeAcpBuiltinSlashCommand`. */
|
package/src/tiny/text.ts
CHANGED
|
@@ -43,6 +43,116 @@ export function formatTitleUserMessage(message: string): string {
|
|
|
43
43
|
return `<user-message>\n${prepareTitleInput(message)}\n</user-message>`;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Greeting / acknowledgement / filler tokens. A first user message composed
|
|
48
|
+
* entirely of these (or of bare numbers / punctuation / emoji) carries no
|
|
49
|
+
* concrete task, so titling is deferred to a later message instead of latching
|
|
50
|
+
* onto "hi". See {@link isLowSignalTitleInput}.
|
|
51
|
+
*/
|
|
52
|
+
const FILLER_TITLE_TOKENS = new Set<string>([
|
|
53
|
+
// greetings
|
|
54
|
+
"hi",
|
|
55
|
+
"hii",
|
|
56
|
+
"hiii",
|
|
57
|
+
"hiya",
|
|
58
|
+
"hey",
|
|
59
|
+
"heya",
|
|
60
|
+
"hello",
|
|
61
|
+
"helo",
|
|
62
|
+
"hullo",
|
|
63
|
+
"yo",
|
|
64
|
+
"ya",
|
|
65
|
+
"sup",
|
|
66
|
+
"wassup",
|
|
67
|
+
"whatsup",
|
|
68
|
+
"howdy",
|
|
69
|
+
"greetings",
|
|
70
|
+
"hola",
|
|
71
|
+
"ciao",
|
|
72
|
+
"aloha",
|
|
73
|
+
"gm",
|
|
74
|
+
"gn",
|
|
75
|
+
"good",
|
|
76
|
+
"morning",
|
|
77
|
+
"afternoon",
|
|
78
|
+
"evening",
|
|
79
|
+
"night",
|
|
80
|
+
"day",
|
|
81
|
+
// politeness / acknowledgement
|
|
82
|
+
"thanks",
|
|
83
|
+
"thank",
|
|
84
|
+
"thx",
|
|
85
|
+
"ty",
|
|
86
|
+
"tysm",
|
|
87
|
+
"cheers",
|
|
88
|
+
"please",
|
|
89
|
+
"pls",
|
|
90
|
+
"plz",
|
|
91
|
+
"ok",
|
|
92
|
+
"okay",
|
|
93
|
+
"okey",
|
|
94
|
+
"k",
|
|
95
|
+
"kk",
|
|
96
|
+
"yep",
|
|
97
|
+
"yes",
|
|
98
|
+
"yeah",
|
|
99
|
+
"yup",
|
|
100
|
+
"nope",
|
|
101
|
+
"no",
|
|
102
|
+
"nah",
|
|
103
|
+
"sure",
|
|
104
|
+
"cool",
|
|
105
|
+
"nice",
|
|
106
|
+
"great",
|
|
107
|
+
"awesome",
|
|
108
|
+
"perfect",
|
|
109
|
+
"lol",
|
|
110
|
+
"lmao",
|
|
111
|
+
"haha",
|
|
112
|
+
"hehe",
|
|
113
|
+
// poking the agent / fillers
|
|
114
|
+
"test",
|
|
115
|
+
"tests",
|
|
116
|
+
"testing",
|
|
117
|
+
"ping",
|
|
118
|
+
"pong",
|
|
119
|
+
"there",
|
|
120
|
+
"you",
|
|
121
|
+
"u",
|
|
122
|
+
"hmm",
|
|
123
|
+
"hmmm",
|
|
124
|
+
"um",
|
|
125
|
+
"uh",
|
|
126
|
+
"so",
|
|
127
|
+
"well",
|
|
128
|
+
"anyway",
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
const TITLE_WORD = /[\p{L}\p{N}]+/gu;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* True when a first user message is too low-signal to title (greeting, ack,
|
|
135
|
+
* bare number, or empty once code/punctuation/emoji are stripped).
|
|
136
|
+
*
|
|
137
|
+
* Deterministic pre-filter: the default tiny title model (~350M local) cannot
|
|
138
|
+
* reliably follow a "respond with none" instruction and tends to hallucinate a
|
|
139
|
+
* title for trivial input, so we never ask it — the caller defers titling to
|
|
140
|
+
* the next message instead.
|
|
141
|
+
*/
|
|
142
|
+
export function isLowSignalTitleInput(message: string): boolean {
|
|
143
|
+
const tokens = stripCodeBlocks(message).toLowerCase().match(TITLE_WORD);
|
|
144
|
+
if (!tokens) return true;
|
|
145
|
+
return tokens.every(token => FILLER_TITLE_TOKENS.has(token) || /^\d+$/.test(token));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Sentinel a capable title model may emit when a message carries no concrete
|
|
150
|
+
* task. Treated as "no title yet" so the caller can defer titling. Backstop for
|
|
151
|
+
* the deterministic {@link isLowSignalTitleInput} filter; kept in sync with the
|
|
152
|
+
* `none` instruction in `prompts/system/title-system.md`.
|
|
153
|
+
*/
|
|
154
|
+
export const NO_TITLE_SENTINEL = "none";
|
|
155
|
+
|
|
46
156
|
export function normalizeGeneratedTitle(value: string | null | undefined): string | null {
|
|
47
157
|
const firstLine = value?.trim().split(/\r?\n/, 1)[0]?.trim();
|
|
48
158
|
if (!firstLine) return null;
|
|
@@ -50,5 +160,6 @@ export function normalizeGeneratedTitle(value: string | null | undefined): strin
|
|
|
50
160
|
.replace(/^["']|["']$/g, "")
|
|
51
161
|
.replace(/[.!?]$/, "")
|
|
52
162
|
.trim();
|
|
53
|
-
|
|
163
|
+
if (!title || title.toLowerCase() === NO_TITLE_SENTINEL) return null;
|
|
164
|
+
return title;
|
|
54
165
|
}
|
|
@@ -38,7 +38,7 @@ export class MemoryRecallTool implements AgentTool<typeof memoryRecallSchema> {
|
|
|
38
38
|
throw new Error("Mnemopi backend is not initialised for this session.");
|
|
39
39
|
}
|
|
40
40
|
try {
|
|
41
|
-
const results = state.recallResultsScoped(params.query);
|
|
41
|
+
const results = await state.recallResultsScoped(params.query);
|
|
42
42
|
if (results.length === 0) {
|
|
43
43
|
return {
|
|
44
44
|
content: [{ type: "text", text: "No relevant memories found." }],
|
|
@@ -43,7 +43,7 @@ export class MemoryReflectTool implements AgentTool<typeof memoryReflectSchema>
|
|
|
43
43
|
const query = params.context?.trim()
|
|
44
44
|
? `${params.query.trim()}\n\nAdditional context:\n${params.context.trim()}`
|
|
45
45
|
: params.query;
|
|
46
|
-
const results = state.recallResultsScoped(query);
|
|
46
|
+
const results = await state.recallResultsScoped(query);
|
|
47
47
|
if (results.length === 0) {
|
|
48
48
|
return {
|
|
49
49
|
content: [{ type: "text", text: "No relevant information found to reflect on." }],
|