@oh-my-pi/pi-coding-agent 16.2.1 → 16.2.2
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 +23 -0
- package/dist/cli.js +2648 -2662
- package/dist/types/advisor/runtime.d.ts +15 -1
- package/dist/types/config/model-roles.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +32 -12
- package/dist/types/discovery/omp-extension-roots.d.ts +3 -3
- package/dist/types/edit/index.d.ts +18 -0
- package/dist/types/edit/streaming.d.ts +30 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +0 -11
- package/dist/types/modes/components/status-line/component.d.ts +0 -2
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +26 -2
- package/dist/types/session/messages.d.ts +6 -7
- package/dist/types/session/settings-stream-fn.d.ts +21 -0
- package/dist/types/session/turn-persistence.d.ts +88 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +196 -0
- package/src/advisor/runtime.ts +65 -2
- package/src/auto-thinking/classifier.ts +2 -2
- package/src/config/model-resolver.ts +5 -1
- package/src/config/model-roles.ts +3 -3
- package/src/config/settings-schema.ts +30 -8
- package/src/discovery/omp-extension-roots.ts +38 -13
- package/src/edit/index.ts +21 -0
- package/src/edit/streaming.ts +170 -0
- package/src/extensibility/custom-tools/types.ts +1 -0
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +18 -1
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +59 -2
- package/src/extensibility/plugins/manager.ts +74 -4
- package/src/extensibility/shared-events.ts +1 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mcp/oauth-discovery.ts +5 -29
- package/src/mcp/transports/http.ts +3 -1
- package/src/mnemopi/backend.ts +2 -2
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/components/assistant-message.ts +5 -5
- package/src/modes/components/status-line/component.ts +1 -9
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/controllers/event-controller.ts +8 -11
- package/src/modes/interactive-mode.ts +0 -5
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/utils/transcript-render-helpers.ts +2 -2
- package/src/modes/utils/ui-helpers.ts +5 -4
- package/src/sdk.ts +12 -26
- package/src/session/agent-session.ts +319 -219
- package/src/session/messages.ts +20 -12
- package/src/session/session-persistence.ts +1 -2
- package/src/session/settings-stream-fn.ts +49 -0
- package/src/session/turn-persistence.ts +142 -0
- package/src/session/unexpected-stop-classifier.ts +2 -2
- package/src/slash-commands/helpers/mcp.ts +2 -1
- package/src/tiny/models.ts +8 -6
- package/src/tiny/text.ts +14 -7
- package/src/tools/image-gen.ts +2 -1
- package/src/tools/tts.ts +2 -1
- package/src/utils/title-generator.ts +1 -1
- package/src/web/search/providers/tavily.ts +36 -19
package/src/session/messages.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
TextContent,
|
|
19
19
|
UserMessage,
|
|
20
20
|
} from "@oh-my-pi/pi-ai";
|
|
21
|
+
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
21
22
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
22
23
|
import userInterjectionTemplate from "../prompts/steering/user-interjection.md" with { type: "text" };
|
|
23
24
|
|
|
@@ -70,11 +71,10 @@ export interface SkillPromptDetails {
|
|
|
70
71
|
* (fallback error emission) read it via `isSilentAbort`. */
|
|
71
72
|
export const SILENT_ABORT_MARKER = "__omp.silent_abort__";
|
|
72
73
|
|
|
73
|
-
/** Type-guard for
|
|
74
|
-
*
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return errorMessage === SILENT_ABORT_MARKER;
|
|
74
|
+
/** Type-guard for silent aborts. Renderers MUST call this helper so structured
|
|
75
|
+
* `errorId` and legacy persisted marker messages stay in lockstep. */
|
|
76
|
+
export function isSilentAbort(message: Pick<AssistantMessage, "errorId" | "errorMessage">): boolean {
|
|
77
|
+
return AIError.is(message.errorId, AIError.Flag.SilentAbort) || message.errorMessage === SILENT_ABORT_MARKER;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/** Reason threaded through `AbortController.abort(reason)` when the user aborts
|
|
@@ -84,12 +84,12 @@ export function isSilentAbort(errorMessage: string | undefined): boolean {
|
|
|
84
84
|
* abort, but interactive renderers suppress this redundant transcript line. */
|
|
85
85
|
export const USER_INTERRUPT_LABEL = "Interrupted by user";
|
|
86
86
|
|
|
87
|
-
export function isUserInterruptAbort(
|
|
88
|
-
return errorMessage === USER_INTERRUPT_LABEL;
|
|
87
|
+
export function isUserInterruptAbort(message: Pick<AssistantMessage, "errorId" | "errorMessage">): boolean {
|
|
88
|
+
return AIError.is(message.errorId, AIError.Flag.UserInterrupt) || message.errorMessage === USER_INTERRUPT_LABEL;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
export function shouldRenderAbortReason(
|
|
92
|
-
return !isSilentAbort(
|
|
91
|
+
export function shouldRenderAbortReason(message: Pick<AssistantMessage, "errorId" | "errorMessage">): boolean {
|
|
92
|
+
return !isSilentAbort(message) && !isUserInterruptAbort(message);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/** Sentinel `errorMessage` the agent stamps on any abort that carried no custom
|
|
@@ -101,9 +101,17 @@ export const GENERIC_ABORT_SENTINEL = "Request was aborted";
|
|
|
101
101
|
* no threaded reason fall back to the retry-aware generic label. Call
|
|
102
102
|
* `shouldRenderAbortReason` before rendering when user interrupts should stay
|
|
103
103
|
* visually quiet. */
|
|
104
|
-
export function resolveAbortLabel(
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
export function resolveAbortLabel(
|
|
105
|
+
message: Pick<AssistantMessage, "errorId" | "errorMessage">,
|
|
106
|
+
retryAttempt = 0,
|
|
107
|
+
): string {
|
|
108
|
+
const genericAbort =
|
|
109
|
+
AIError.is(message.errorId, AIError.Flag.Abort) ||
|
|
110
|
+
!message.errorMessage ||
|
|
111
|
+
message.errorMessage === GENERIC_ABORT_SENTINEL ||
|
|
112
|
+
isSilentAbort(message);
|
|
113
|
+
if (!genericAbort) {
|
|
114
|
+
return message.errorMessage!;
|
|
107
115
|
}
|
|
108
116
|
if (retryAttempt > 0) {
|
|
109
117
|
return `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`;
|
|
@@ -110,9 +110,8 @@ function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?: string
|
|
|
110
110
|
const entries: Array<readonly [string, unknown]> = [];
|
|
111
111
|
for (const [childKey, value] of Object.entries(obj)) {
|
|
112
112
|
// Strip transient/redundant properties that shouldn't be persisted.
|
|
113
|
-
// - partialJson: streaming accumulator for tool call JSON parsing
|
|
114
113
|
// - jsonlEvents: raw subprocess streaming events (already saved to artifact files)
|
|
115
|
-
if (childKey === "
|
|
114
|
+
if (childKey === "jsonlEvents") {
|
|
116
115
|
changed = true;
|
|
117
116
|
continue;
|
|
118
117
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings-aware stream wrapper shared by the main agent (sdk.ts) and the
|
|
3
|
+
* advisor agent (AgentSession.#buildAdvisorRuntime).
|
|
4
|
+
*
|
|
5
|
+
* Reads OpenRouter / Antigravity routing variants, Responses-family text
|
|
6
|
+
* verbosity, per-provider in-flight caps, and the loop guard out of `Settings`
|
|
7
|
+
* per request, layering them onto whatever options the caller passed. Before
|
|
8
|
+
* this helper existed, advisor turns called bare `streamSimple` while the main
|
|
9
|
+
* turn went through an inline closure that read these settings — so an advisor on
|
|
10
|
+
* OpenRouter never saw `providers.openrouterVariant`, breaking sticky routing
|
|
11
|
+
* and OpenRouter response-cache hits across advisor calls.
|
|
12
|
+
*/
|
|
13
|
+
import type { StreamFn } from "@oh-my-pi/pi-agent-core";
|
|
14
|
+
import { type SimpleStreamOptions, streamSimple } from "@oh-my-pi/pi-ai";
|
|
15
|
+
import { type Settings, validateProviderMaxInFlightRequests } from "../config/settings";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build a {@link StreamFn} that reads provider routing/guard settings from
|
|
19
|
+
* `settings` per call and forwards to `base` (defaults to `streamSimple`).
|
|
20
|
+
*
|
|
21
|
+
* Caller-supplied `streamOptions` always win — the helper only fills holes.
|
|
22
|
+
*/
|
|
23
|
+
export function createSettingsAwareStreamFn(settings: Settings, base: StreamFn = streamSimple): StreamFn {
|
|
24
|
+
return (model, context, streamOptions) => {
|
|
25
|
+
const openrouterRoutingPreset = settings.get("providers.openrouterVariant");
|
|
26
|
+
const openrouterVariant =
|
|
27
|
+
openrouterRoutingPreset && openrouterRoutingPreset !== "default" ? openrouterRoutingPreset : undefined;
|
|
28
|
+
const antigravityEndpointMode = settings.get("providers.antigravityEndpoint");
|
|
29
|
+
const textVerbosity =
|
|
30
|
+
model.api === "openai-codex-responses" || model.api === "openai-responses"
|
|
31
|
+
? settings.get("textVerbosity")
|
|
32
|
+
: undefined;
|
|
33
|
+
const merged: SimpleStreamOptions = {
|
|
34
|
+
...streamOptions,
|
|
35
|
+
openrouterVariant: streamOptions?.openrouterVariant ?? openrouterVariant,
|
|
36
|
+
antigravityEndpointMode: streamOptions?.antigravityEndpointMode ?? antigravityEndpointMode,
|
|
37
|
+
textVerbosity: streamOptions?.textVerbosity ?? textVerbosity,
|
|
38
|
+
maxInFlightRequests: validateProviderMaxInFlightRequests(
|
|
39
|
+
streamOptions?.maxInFlightRequests ?? settings.get("providers.maxInFlightRequests"),
|
|
40
|
+
),
|
|
41
|
+
loopGuard: {
|
|
42
|
+
enabled: settings.get("model.loopGuard.enabled"),
|
|
43
|
+
checkAssistantContent: settings.get("model.loopGuard.checkAssistantContent"),
|
|
44
|
+
...streamOptions?.loopGuard,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
return base(model, context, merged);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers that share one cheap, structural identity for messages — both during
|
|
3
|
+
* incremental persistence and for the mid-run-compaction ordering check.
|
|
4
|
+
*
|
|
5
|
+
* Previously `AgentSession` carried two near-duplicate routines
|
|
6
|
+
* (`#sessionMessagesReferToSameTurn` + `#messageValueSignature`) that
|
|
7
|
+
* reconstructed the branch path on every check (O(n²) `unshift`) and
|
|
8
|
+
* `JSON.stringify`-compared the full message content on every pairwise hit.
|
|
9
|
+
* Long-running sessions with many subagents fired this thousands of times per
|
|
10
|
+
* minute and froze the TUI loop (see issue #3629). The persistence key already
|
|
11
|
+
* encodes a stable logical identity — timestamp + role-specific discriminators
|
|
12
|
+
* — so the structural compare is now the rare collision tiebreaker (e.g. two
|
|
13
|
+
* provider responses at the same millisecond with `undefined` responseId),
|
|
14
|
+
* not the load-bearing check.
|
|
15
|
+
*
|
|
16
|
+
* The helpers here keep that identity in one place and expose the planner so
|
|
17
|
+
* the persistence-ordering logic is unit-testable without standing up an
|
|
18
|
+
* `AgentSession`.
|
|
19
|
+
*/
|
|
20
|
+
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Stable identity for messages that pass through {@link AgentSession}'s
|
|
24
|
+
* incremental persistence path.
|
|
25
|
+
*
|
|
26
|
+
* The discriminators chosen per role are precisely the fields that uniquely
|
|
27
|
+
* identify a single logical message instance:
|
|
28
|
+
*
|
|
29
|
+
* - `assistant` — timestamp + provider + model + responseId + stopReason
|
|
30
|
+
* (responseId is the canonical provider-side id when available; the rest
|
|
31
|
+
* disambiguate when it is not, e.g. local/dev models).
|
|
32
|
+
* - `toolResult` — timestamp + toolCallId + toolName (toolCallId is unique
|
|
33
|
+
* per execution; toolName guards against synthetic reuse).
|
|
34
|
+
* - `user` / `developer` — timestamp + attribution (attribution distinguishes
|
|
35
|
+
* user-typed vs hook-injected at the same wall-clock millisecond).
|
|
36
|
+
* - `fileMention` — timestamp.
|
|
37
|
+
*
|
|
38
|
+
* Returns `undefined` for message roles that are not persisted through this
|
|
39
|
+
* path (e.g. `hookMessage`, `custom`, `bashExecution`) — those follow other
|
|
40
|
+
* append paths in `SessionManager`.
|
|
41
|
+
*/
|
|
42
|
+
export function sessionMessagePersistenceKey(message: AgentMessage): string | undefined {
|
|
43
|
+
switch (message.role) {
|
|
44
|
+
case "assistant":
|
|
45
|
+
return [
|
|
46
|
+
"assistant",
|
|
47
|
+
message.timestamp,
|
|
48
|
+
message.provider,
|
|
49
|
+
message.model,
|
|
50
|
+
message.responseId ?? "",
|
|
51
|
+
message.stopReason,
|
|
52
|
+
].join(":");
|
|
53
|
+
case "toolResult":
|
|
54
|
+
return `toolResult:${message.timestamp}:${message.toolCallId}:${message.toolName}`;
|
|
55
|
+
case "user":
|
|
56
|
+
case "developer":
|
|
57
|
+
return `${message.role}:${message.timestamp}:${message.attribution ?? ""}`;
|
|
58
|
+
case "fileMention":
|
|
59
|
+
return `fileMention:${message.timestamp}`;
|
|
60
|
+
default:
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Slow-path content equality check used when two messages collide on
|
|
67
|
+
* {@link sessionMessagePersistenceKey}. Only the role's content fields are
|
|
68
|
+
* compared (no timestamps, no metadata) because the key already pinned all of
|
|
69
|
+
* those down.
|
|
70
|
+
*
|
|
71
|
+
* Most calls into the persistence path never reach this — keys are unique
|
|
72
|
+
* enough in production that the snapshot lookup short-circuits at the key
|
|
73
|
+
* level. Restoring the structural compare here preserves the pre-#3629
|
|
74
|
+
* contract that two messages with the same metadata BUT different content are
|
|
75
|
+
* distinct (e.g. two assistant turns with `undefined` responseId emitted in
|
|
76
|
+
* the same wall-clock millisecond, which is exactly how the in-memory test
|
|
77
|
+
* harness crafts streamed responses).
|
|
78
|
+
*/
|
|
79
|
+
export function sameMessageContent(left: AgentMessage, right: AgentMessage): boolean {
|
|
80
|
+
if (left === right) return true;
|
|
81
|
+
if (left.role !== right.role) return false;
|
|
82
|
+
// `JSON.stringify` is the slow-path serializer here on purpose: nothing on
|
|
83
|
+
// the hot persistence-check path reaches it (key lookup short-circuits
|
|
84
|
+
// first), so a stable lexicographic compare beats hand-rolling structural
|
|
85
|
+
// equality for content arrays that mix text / tool blocks / file refs.
|
|
86
|
+
const leftRaw = left.role === "fileMention" ? left.files : "content" in left ? left.content : undefined;
|
|
87
|
+
const rightRaw = right.role === "fileMention" ? right.files : "content" in right ? right.content : undefined;
|
|
88
|
+
if (leftRaw === undefined || rightRaw === undefined) return false;
|
|
89
|
+
return (JSON.stringify(leftRaw) ?? "undefined") === (JSON.stringify(rightRaw) ?? "undefined");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Outcome of {@link planTurnPersistence}.
|
|
94
|
+
*
|
|
95
|
+
* `ok` lists the turn-message indices that still need to be appended (in
|
|
96
|
+
* order). `out-of-order` reports the first message whose later sibling is
|
|
97
|
+
* already persisted — the caller bails so it does not silently splice a
|
|
98
|
+
* stale message between newer entries on the live branch.
|
|
99
|
+
*/
|
|
100
|
+
export type TurnPersistencePlan =
|
|
101
|
+
| { kind: "ok"; toPersist: readonly number[] }
|
|
102
|
+
| { kind: "out-of-order"; messageIndex: number };
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Decide what to do with a turn's messages relative to what's already on the
|
|
106
|
+
* branch, in a single pass over the pre-computed keys.
|
|
107
|
+
*
|
|
108
|
+
* @param turnKeys persistence keys for each turn message, in the order the
|
|
109
|
+
* agent loop emitted them. `undefined` slots represent messages with no
|
|
110
|
+
* persistence key (skipped silently).
|
|
111
|
+
* @param persistedKeys the snapshot of persistence keys currently on the
|
|
112
|
+
* branch (built once per call from {@link sessionMessagePersistenceKey} for
|
|
113
|
+
* each persisted message entry).
|
|
114
|
+
*
|
|
115
|
+
* The check is O(n²) over turn messages — but `n` here is the size of one
|
|
116
|
+
* turn (a handful of tool results), not the size of the branch. That's the
|
|
117
|
+
* point of this refactor: the expensive O(branch) work happens exactly once,
|
|
118
|
+
* inside the caller's snapshot loop, not per-comparison.
|
|
119
|
+
*/
|
|
120
|
+
export function planTurnPersistence(
|
|
121
|
+
turnKeys: readonly (string | undefined)[],
|
|
122
|
+
persistedKeys: ReadonlySet<string>,
|
|
123
|
+
): TurnPersistencePlan {
|
|
124
|
+
const toPersist: number[] = [];
|
|
125
|
+
for (let index = 0; index < turnKeys.length; index++) {
|
|
126
|
+
const key = turnKeys[index];
|
|
127
|
+
// Slots without a persistence key (non-persistent roles like `custom` /
|
|
128
|
+
// `hookMessage`) take other branches in `SessionManager` — they are not
|
|
129
|
+
// our responsibility to append, and they cannot violate ordering because
|
|
130
|
+
// they have no identity on the branch.
|
|
131
|
+
if (key === undefined) continue;
|
|
132
|
+
if (persistedKeys.has(key)) continue;
|
|
133
|
+
for (let later = index + 1; later < turnKeys.length; later++) {
|
|
134
|
+
const laterKey = turnKeys[later];
|
|
135
|
+
if (laterKey !== undefined && persistedKeys.has(laterKey)) {
|
|
136
|
+
return { kind: "out-of-order", messageIndex: index };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
toPersist.push(index);
|
|
140
|
+
}
|
|
141
|
+
return { kind: "ok", toPersist };
|
|
142
|
+
}
|
|
@@ -64,10 +64,10 @@ export async function classifyUnexpectedStop(
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
async function classifyOnline(text: string, deps: ClassifyUnexpectedStopDeps): Promise<boolean | undefined> {
|
|
67
|
-
const resolved = resolveRoleSelection(["smol"], deps.settings, deps.registry.getAvailable(), deps.registry);
|
|
67
|
+
const resolved = resolveRoleSelection(["tiny", "smol"], deps.settings, deps.registry.getAvailable(), deps.registry);
|
|
68
68
|
const model = resolved?.model;
|
|
69
69
|
if (!model) {
|
|
70
|
-
throw new Error("unexpected-stop: no smol model available for classification");
|
|
70
|
+
throw new Error("unexpected-stop: no tiny/smol model available for classification");
|
|
71
71
|
}
|
|
72
72
|
const apiKey = await deps.registry.getApiKey(model, deps.sessionId);
|
|
73
73
|
if (!apiKey) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
1
2
|
import { getMCPConfigPath, logger } from "@oh-my-pi/pi-utils";
|
|
2
3
|
import { connectToServer, disconnectServer, listPrompts, listResources, listTools } from "../../mcp/client";
|
|
3
4
|
import {
|
|
@@ -350,7 +351,7 @@ async function handleSmitherySearchCommand(rest: string, runtime: SlashCommandRu
|
|
|
350
351
|
return commandConsumed();
|
|
351
352
|
} catch (err) {
|
|
352
353
|
const message = errorMessage(err);
|
|
353
|
-
if (
|
|
354
|
+
if (AIError.is(AIError.classify(err), AIError.Flag.AuthFailed)) {
|
|
354
355
|
return usage(
|
|
355
356
|
"Smithery authentication required. Run /mcp smithery-login in the TUI client or add an API key to ~/.omp/agent/smithery.json.",
|
|
356
357
|
runtime,
|
package/src/tiny/models.ts
CHANGED
|
@@ -87,8 +87,9 @@ void TINY_TITLE_MODEL_VALUES_MATCH_REGISTRY;
|
|
|
87
87
|
export const TINY_TITLE_MODEL_OPTIONS = [
|
|
88
88
|
{
|
|
89
89
|
value: ONLINE_TINY_TITLE_MODEL_KEY,
|
|
90
|
-
label: "Online (pi/smol)",
|
|
91
|
-
description:
|
|
90
|
+
label: "Online (TINY role, else pi/smol)",
|
|
91
|
+
description:
|
|
92
|
+
"Online title generation: the TINY model role (set one in /models) when assigned, otherwise the online fallback (commit role, then pi/smol). No local download or on-device inference.",
|
|
92
93
|
},
|
|
93
94
|
...TINY_TITLE_LOCAL_MODELS.map(model => ({
|
|
94
95
|
value: model.key,
|
|
@@ -183,9 +184,9 @@ void TINY_MEMORY_MODEL_VALUES_MATCH_REGISTRY;
|
|
|
183
184
|
export const TINY_MEMORY_MODEL_OPTIONS = [
|
|
184
185
|
{
|
|
185
186
|
value: ONLINE_MEMORY_MODEL_KEY,
|
|
186
|
-
label: "Online (smol
|
|
187
|
+
label: "Online (TINY role, else smol)",
|
|
187
188
|
description:
|
|
188
|
-
"Use the
|
|
189
|
+
"Use the online model: the TINY role from /models when set, otherwise pi/smol. No local model download or on-device inference.",
|
|
189
190
|
},
|
|
190
191
|
...TINY_MEMORY_LOCAL_MODELS.map(model => ({
|
|
191
192
|
value: model.key,
|
|
@@ -245,8 +246,9 @@ export type AutoThinkingModelKey = TinyMemoryModelKey;
|
|
|
245
246
|
export const AUTO_THINKING_MODEL_OPTIONS = [
|
|
246
247
|
{
|
|
247
248
|
value: ONLINE_AUTO_THINKING_MODEL_KEY,
|
|
248
|
-
label: "Online (smol)",
|
|
249
|
-
description:
|
|
249
|
+
label: "Online (TINY role, else smol)",
|
|
250
|
+
description:
|
|
251
|
+
"Classify prompt difficulty online with the TINY role model (set one in /models) or pi/smol; no local download or on-device inference.",
|
|
250
252
|
},
|
|
251
253
|
...TINY_MEMORY_LOCAL_MODELS.map(model => ({
|
|
252
254
|
value: model.key,
|
package/src/tiny/text.ts
CHANGED
|
@@ -173,14 +173,15 @@ export function normalizeGeneratedTitle(value: string | null | undefined, source
|
|
|
173
173
|
* (`TinyVMM` → `tinyvmm`). The user's message is the source of truth, so per
|
|
174
174
|
* title token:
|
|
175
175
|
* 1. typed verbatim in the message → keep it (the user established the casing);
|
|
176
|
-
* 2. else the message has the same word with *distinctive* casing
|
|
177
|
-
* (`TinyVMM`, `iOS`, `
|
|
176
|
+
* 2. else the message has the same word with *distinctive* mixed casing
|
|
177
|
+
* (`TinyVMM`, `iOS`, `IDs`) → adopt the user's casing (restoration);
|
|
178
178
|
* 3. else it's a camelCase artifact (lowercase word + stray interior capital,
|
|
179
179
|
* `dAemon`) the user never wrote → lowercase it;
|
|
180
180
|
* 4. else leave it — preserves model-cased proper nouns like `GitHub`, `OAuth`.
|
|
181
181
|
*
|
|
182
|
-
* Restoration is limited to distinctively cased source tokens
|
|
183
|
-
* merely *starts* with `For` can't force a mid-title `for` to `For
|
|
182
|
+
* Restoration is limited to distinctively *mixed*-cased source tokens: a sentence
|
|
183
|
+
* that merely *starts* with `For` can't force a mid-title `for` to `For`, and
|
|
184
|
+
* emphatic all-caps (`ALL ERROR HANDLING`) is never re-shouted over sentence case.
|
|
184
185
|
*/
|
|
185
186
|
function reconcileTitleCasing(title: string, sourceText: string): string {
|
|
186
187
|
const verbatim = new Set<string>();
|
|
@@ -200,10 +201,16 @@ function reconcileTitleCasing(title: string, sourceText: string): string {
|
|
|
200
201
|
});
|
|
201
202
|
}
|
|
202
203
|
|
|
203
|
-
/**
|
|
204
|
-
*
|
|
204
|
+
/** Mixed-case identifier the user cased deliberately (`TinyVMM`, `iOS`, `IDs`):
|
|
205
|
+
* an interior/repeated capital plus at least one lowercase letter. Only these
|
|
206
|
+
* are restored when the model flattens them.
|
|
207
|
+
*
|
|
208
|
+
* Pure all-caps is intentionally excluded. The model preserves its own acronyms
|
|
209
|
+
* verbatim regardless, so restoring all-caps from the source would only ever
|
|
210
|
+
* re-shout emphatic input (`ALL ERROR HANDLING`, `FIX THE BUG`) over the
|
|
211
|
+
* sentence case the prompt asks for. */
|
|
205
212
|
function isDistinctiveCasing(token: string): boolean {
|
|
206
|
-
return /\p{L}\p{Lu}/u.test(token);
|
|
213
|
+
return /\p{Ll}/u.test(token) && /\p{L}\p{Lu}/u.test(token);
|
|
207
214
|
}
|
|
208
215
|
|
|
209
216
|
/** A lowercase word carrying a stray interior capital (`dAemon`, `cReate`): the
|
package/src/tools/image-gen.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as os from "node:os";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { type ApiKey, type FetchImpl, getEnvApiKey, type Model,
|
|
3
|
+
import { type ApiKey, type FetchImpl, getEnvApiKey, type Model, withAuth } from "@oh-my-pi/pi-ai";
|
|
4
|
+
import { ProviderHttpError } from "@oh-my-pi/pi-ai/error";
|
|
4
5
|
import {
|
|
5
6
|
CODEX_BASE_URL,
|
|
6
7
|
getCodexAccountId,
|
package/src/tools/tts.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// the `providers.tts` switch.
|
|
5
5
|
|
|
6
6
|
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
7
|
-
import { type ApiKey,
|
|
7
|
+
import { type ApiKey, withAuth } from "@oh-my-pi/pi-ai";
|
|
8
|
+
import { ProviderHttpError } from "@oh-my-pi/pi-ai/error";
|
|
8
9
|
import { type } from "arktype";
|
|
9
10
|
import { settings } from "../config/settings";
|
|
10
11
|
import type { CustomTool, CustomToolContext } from "../extensibility/custom-tools/types";
|
|
@@ -72,7 +72,7 @@ function getTitleModel(registry: ModelRegistry, settings: Settings, currentModel
|
|
|
72
72
|
const availableModels = registry.getAvailable();
|
|
73
73
|
if (availableModels.length === 0) return undefined;
|
|
74
74
|
|
|
75
|
-
const titleModel = resolveRoleSelection(["
|
|
75
|
+
const titleModel = resolveRoleSelection(["tiny", "commit", "smol"], settings, availableModels, registry)?.model;
|
|
76
76
|
if (titleModel) return titleModel;
|
|
77
77
|
|
|
78
78
|
if (currentModel) return currentModel;
|
|
@@ -120,25 +120,7 @@ async function callTavilySearch(apiKey: string, params: TavilySearchParams): Pro
|
|
|
120
120
|
return (await response.json()) as TavilySearchResponse;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
export async function searchTavily(params: SearchParams): Promise<SearchResponse> {
|
|
125
|
-
const tavilyParams: TavilySearchParams = {
|
|
126
|
-
query: params.query,
|
|
127
|
-
num_results: params.numSearchResults ?? params.limit,
|
|
128
|
-
recency: params.recency,
|
|
129
|
-
signal: params.signal,
|
|
130
|
-
fetch: params.fetch,
|
|
131
|
-
};
|
|
132
|
-
const keyOrResolver: ApiKey = params.authStorage.resolver("tavily", {
|
|
133
|
-
sessionId: params.sessionId,
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
const numResults = clampNumResults(tavilyParams.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
|
|
137
|
-
const response = await withAuth(keyOrResolver, key => callTavilySearch(key, tavilyParams), {
|
|
138
|
-
signal: params.signal,
|
|
139
|
-
missingKeyMessage:
|
|
140
|
-
'Tavily credentials not found. Set TAVILY_API_KEY or configure an API key for provider "tavily".',
|
|
141
|
-
});
|
|
123
|
+
function toSearchResponse(response: TavilySearchResponse, numResults: number): SearchResponse {
|
|
142
124
|
const sources: SearchSource[] = [];
|
|
143
125
|
|
|
144
126
|
for (const result of response.results ?? []) {
|
|
@@ -161,6 +143,41 @@ export async function searchTavily(params: SearchParams): Promise<SearchResponse
|
|
|
161
143
|
};
|
|
162
144
|
}
|
|
163
145
|
|
|
146
|
+
function hasRenderableResponse(response: SearchResponse): boolean {
|
|
147
|
+
if (response.answer?.trim()) return true;
|
|
148
|
+
return response.sources.length > 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Execute Tavily web search. */
|
|
152
|
+
export async function searchTavily(params: SearchParams): Promise<SearchResponse> {
|
|
153
|
+
const tavilyParams: TavilySearchParams = {
|
|
154
|
+
query: params.query,
|
|
155
|
+
num_results: params.numSearchResults ?? params.limit,
|
|
156
|
+
recency: params.recency,
|
|
157
|
+
signal: params.signal,
|
|
158
|
+
fetch: params.fetch,
|
|
159
|
+
};
|
|
160
|
+
const keyOrResolver: ApiKey = params.authStorage.resolver("tavily", {
|
|
161
|
+
sessionId: params.sessionId,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const numResults = clampNumResults(tavilyParams.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
|
|
165
|
+
const authOptions = {
|
|
166
|
+
signal: params.signal,
|
|
167
|
+
missingKeyMessage:
|
|
168
|
+
'Tavily credentials not found. Set TAVILY_API_KEY or configure an API key for provider "tavily".',
|
|
169
|
+
};
|
|
170
|
+
const callWithAuth = (searchParams: TavilySearchParams) =>
|
|
171
|
+
withAuth(keyOrResolver, key => callTavilySearch(key, searchParams), authOptions);
|
|
172
|
+
|
|
173
|
+
const response = toSearchResponse(await callWithAuth(tavilyParams), numResults);
|
|
174
|
+
if (!tavilyParams.recency || hasRenderableResponse(response)) {
|
|
175
|
+
return response;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return toSearchResponse(await callWithAuth({ ...tavilyParams, recency: undefined }), numResults);
|
|
179
|
+
}
|
|
180
|
+
|
|
164
181
|
/** Search provider for Tavily web search. */
|
|
165
182
|
export class TavilyProvider extends SearchProvider {
|
|
166
183
|
readonly id = "tavily";
|