@oh-my-pi/pi-coding-agent 15.7.3 → 15.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/dist/types/config/settings-schema.d.ts +3 -22
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/local-protocol.d.ts +19 -9
- package/dist/types/internal-urls/types.d.ts +14 -0
- package/dist/types/lsp/client.d.ts +3 -0
- package/dist/types/mcp/manager.d.ts +14 -5
- package/dist/types/modes/controllers/command-controller.d.ts +2 -3
- package/dist/types/session/agent-session.d.ts +2 -6
- package/dist/types/session/shake-types.d.ts +3 -3
- package/dist/types/task/repair-args.d.ts +52 -0
- package/dist/types/tiny/models.d.ts +0 -14
- package/dist/types/tiny/title-client.d.ts +28 -2
- package/dist/types/tiny/title-protocol.d.ts +8 -9
- package/dist/types/tools/find.d.ts +1 -1
- package/dist/types/tools/path-utils.d.ts +7 -0
- package/dist/types/tui/output-block.d.ts +7 -7
- package/package.json +9 -9
- package/scripts/build-binary.ts +0 -1
- package/src/cli.ts +59 -0
- package/src/config/settings-schema.ts +3 -24
- package/src/config/settings.ts +10 -0
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/internal-urls/local-protocol.ts +23 -11
- package/src/internal-urls/types.ts +15 -0
- package/src/lsp/client.ts +28 -5
- package/src/mcp/manager.ts +87 -4
- package/src/modes/controllers/command-controller.ts +7 -39
- package/src/modes/controllers/event-controller.ts +33 -26
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/prompts/system/project-prompt.md +3 -2
- package/src/prompts/system/subagent-system-prompt.md +12 -8
- package/src/prompts/system/system-prompt.md +8 -6
- package/src/session/agent-session.ts +11 -93
- package/src/session/shake-types.ts +4 -5
- package/src/slash-commands/builtin-registry.ts +2 -4
- package/src/task/executor.ts +14 -4
- package/src/task/index.ts +3 -2
- package/src/task/repair-args.ts +117 -0
- package/src/tiny/models.ts +0 -28
- package/src/tiny/title-client.ts +133 -43
- package/src/tiny/title-protocol.ts +11 -16
- package/src/tiny/worker.ts +6 -61
- package/src/tools/ast-edit.ts +3 -0
- package/src/tools/ast-grep.ts +3 -0
- package/src/tools/find.ts +20 -6
- package/src/tools/gh.ts +1 -0
- package/src/tools/path-utils.ts +13 -2
- package/src/tools/read.ts +1 -0
- package/src/tools/search.ts +12 -1
- package/src/tui/output-block.ts +37 -75
- package/src/utils/git.ts +9 -3
|
@@ -51,11 +51,8 @@ import {
|
|
|
51
51
|
prepareCompaction,
|
|
52
52
|
type ShakeConfig,
|
|
53
53
|
type ShakeRegion,
|
|
54
|
-
type ShakeSummaryComplete,
|
|
55
|
-
type ShakeSummaryItem,
|
|
56
54
|
type SummaryOptions,
|
|
57
55
|
shouldCompact,
|
|
58
|
-
summarizeShakeRegions,
|
|
59
56
|
} from "@oh-my-pi/pi-agent-core/compaction";
|
|
60
57
|
import { DEFAULT_PRUNE_CONFIG, pruneToolOutputs } from "@oh-my-pi/pi-agent-core/compaction/pruning";
|
|
61
58
|
import type {
|
|
@@ -185,8 +182,7 @@ import {
|
|
|
185
182
|
resolveThinkingLevelForModel,
|
|
186
183
|
toReasoningEffort,
|
|
187
184
|
} from "../thinking";
|
|
188
|
-
import {
|
|
189
|
-
import { shutdownTinyTitleClient, tinyModelClient } from "../tiny/title-client";
|
|
185
|
+
import { shutdownTinyTitleClient } from "../tiny/title-client";
|
|
190
186
|
import {
|
|
191
187
|
buildDiscoverableToolSearchIndex,
|
|
192
188
|
collectDiscoverableTools,
|
|
@@ -241,11 +237,11 @@ export type AgentSessionEvent =
|
|
|
241
237
|
| {
|
|
242
238
|
type: "auto_compaction_start";
|
|
243
239
|
reason: "threshold" | "overflow" | "idle" | "incomplete";
|
|
244
|
-
action: "context-full" | "handoff" | "shake"
|
|
240
|
+
action: "context-full" | "handoff" | "shake";
|
|
245
241
|
}
|
|
246
242
|
| {
|
|
247
243
|
type: "auto_compaction_end";
|
|
248
|
-
action: "context-full" | "handoff" | "shake"
|
|
244
|
+
action: "context-full" | "handoff" | "shake";
|
|
249
245
|
result: CompactionResult | undefined;
|
|
250
246
|
aborted: boolean;
|
|
251
247
|
willRetry: boolean;
|
|
@@ -5659,10 +5655,6 @@ export class AgentSession {
|
|
|
5659
5655
|
* - `images` delegates to {@link dropImages}.
|
|
5660
5656
|
* - `elide` replaces whole tool-call results and large fenced/XML blocks
|
|
5661
5657
|
* with short placeholders that embed an `artifact://` recovery link.
|
|
5662
|
-
* - `summary` extractively compresses the same regions with the configured
|
|
5663
|
-
* local on-device model (`providers.shakeSummaryModel`), falling back to
|
|
5664
|
-
* the elide placeholder per region (or wholesale when the local model is
|
|
5665
|
-
* unavailable). Never calls a remote/cloud LLM.
|
|
5666
5658
|
*
|
|
5667
5659
|
* Mutates the branch in place, persists via `rewriteEntries`, replays the
|
|
5668
5660
|
* rebuilt context through the agent, and tears down provider sessions that
|
|
@@ -5683,29 +5675,7 @@ export class AgentSession {
|
|
|
5683
5675
|
}
|
|
5684
5676
|
|
|
5685
5677
|
const artifactId = await this.#saveShakeArtifact(regions);
|
|
5686
|
-
|
|
5687
|
-
if (mode === "summary") {
|
|
5688
|
-
// Manual `/shake summary` installs the compaction controller so Esc /
|
|
5689
|
-
// `abortCompaction()` can cancel the local-model pass; the auto-shake path
|
|
5690
|
-
// passes its own signal and manages `#autoCompactionAbortController`.
|
|
5691
|
-
let controller: AbortController | undefined;
|
|
5692
|
-
let signal = opts.signal;
|
|
5693
|
-
if (!signal) {
|
|
5694
|
-
if (this.#compactionAbortController) throw new Error("Compaction already in progress");
|
|
5695
|
-
controller = new AbortController();
|
|
5696
|
-
this.#compactionAbortController = controller;
|
|
5697
|
-
signal = controller.signal;
|
|
5698
|
-
}
|
|
5699
|
-
try {
|
|
5700
|
-
replacements = await this.#buildShakeSummaryReplacements(regions, artifactId, signal);
|
|
5701
|
-
} finally {
|
|
5702
|
-
if (controller && this.#compactionAbortController === controller) {
|
|
5703
|
-
this.#compactionAbortController = undefined;
|
|
5704
|
-
}
|
|
5705
|
-
}
|
|
5706
|
-
} else {
|
|
5707
|
-
replacements = regions.map((region, index) => this.#shakeElidePlaceholder(region, index, artifactId));
|
|
5708
|
-
}
|
|
5678
|
+
const replacements = regions.map((region, index) => this.#shakeElidePlaceholder(region, index, artifactId));
|
|
5709
5679
|
|
|
5710
5680
|
let toolResultsDropped = 0;
|
|
5711
5681
|
let blocksDropped = 0;
|
|
@@ -5762,56 +5732,6 @@ export class AgentSession {
|
|
|
5762
5732
|
}
|
|
5763
5733
|
}
|
|
5764
5734
|
|
|
5765
|
-
/**
|
|
5766
|
-
* Build per-region replacements for summary mode using the configured local
|
|
5767
|
-
* on-device model (`providers.shakeSummaryModel`) via {@link tinyModelClient}.
|
|
5768
|
-
* Shake summary never calls a remote/cloud LLM. When the configured model is
|
|
5769
|
-
* not a known local key, every region falls back to the elide placeholder.
|
|
5770
|
-
* Otherwise compresses via {@link summarizeShakeRegions}; per region, uses
|
|
5771
|
-
* the parsed summary (with a recovery footer) or the elide placeholder when
|
|
5772
|
-
* the local model omitted it / was unavailable. Any thrown failure degrades
|
|
5773
|
-
* the whole batch to elide so the reduction still happens.
|
|
5774
|
-
*/
|
|
5775
|
-
async #buildShakeSummaryReplacements(
|
|
5776
|
-
regions: ShakeRegion[],
|
|
5777
|
-
artifactId: string | undefined,
|
|
5778
|
-
signal: AbortSignal | undefined,
|
|
5779
|
-
): Promise<string[]> {
|
|
5780
|
-
const elide = (): string[] =>
|
|
5781
|
-
regions.map((region, index) => this.#shakeElidePlaceholder(region, index, artifactId));
|
|
5782
|
-
|
|
5783
|
-
const modelKey = this.settings.get("providers.shakeSummaryModel");
|
|
5784
|
-
if (!isTinyMemoryLocalModelKey(modelKey)) return elide();
|
|
5785
|
-
|
|
5786
|
-
const items: ShakeSummaryItem[] = regions.map((region, index) => ({
|
|
5787
|
-
index,
|
|
5788
|
-
label: region.label,
|
|
5789
|
-
text: region.originalText,
|
|
5790
|
-
}));
|
|
5791
|
-
|
|
5792
|
-
const complete: ShakeSummaryComplete = (promptText, opts) =>
|
|
5793
|
-
tinyModelClient.complete(modelKey, promptText, { maxTokens: opts.maxTokens, signal: opts.signal });
|
|
5794
|
-
|
|
5795
|
-
let summaries: Map<number, string>;
|
|
5796
|
-
try {
|
|
5797
|
-
summaries = await summarizeShakeRegions(items, complete, { signal });
|
|
5798
|
-
} catch (error) {
|
|
5799
|
-
logger.warn("Shake summary compression failed; falling back to elide", {
|
|
5800
|
-
error: error instanceof Error ? error.message : String(error),
|
|
5801
|
-
});
|
|
5802
|
-
return elide();
|
|
5803
|
-
}
|
|
5804
|
-
|
|
5805
|
-
return regions.map((region, index) => {
|
|
5806
|
-
const summary = summaries.get(index);
|
|
5807
|
-
if (!summary) return this.#shakeElidePlaceholder(region, index, artifactId);
|
|
5808
|
-
if (artifactId) {
|
|
5809
|
-
return `${summary}\n\n[recover full: artifact://${artifactId} (region ${index + 1})]`;
|
|
5810
|
-
}
|
|
5811
|
-
return summary;
|
|
5812
|
-
});
|
|
5813
|
-
}
|
|
5814
|
-
|
|
5815
5735
|
/**
|
|
5816
5736
|
* Manually compact the session context.
|
|
5817
5737
|
* Aborts current agent operation first.
|
|
@@ -7025,11 +6945,11 @@ export class AgentSession {
|
|
|
7025
6945
|
if (reason !== "idle" && !compactionSettings.enabled) return false;
|
|
7026
6946
|
const generation = this.#promptGeneration;
|
|
7027
6947
|
|
|
7028
|
-
// Shake
|
|
7029
|
-
//
|
|
7030
|
-
//
|
|
7031
|
-
if (compactionSettings.strategy === "shake"
|
|
7032
|
-
const outcome = await this.#runAutoShake(reason,
|
|
6948
|
+
// Shake runs inline (cheap, no remote LLM). On overflow recovery, if shake
|
|
6949
|
+
// reclaims nothing we fall through to the summary-compaction body below so
|
|
6950
|
+
// the oversized input still gets resolved.
|
|
6951
|
+
if (compactionSettings.strategy === "shake") {
|
|
6952
|
+
const outcome = await this.#runAutoShake(reason, willRetry, generation);
|
|
7033
6953
|
if (outcome !== "fallback") return false;
|
|
7034
6954
|
}
|
|
7035
6955
|
// "overflow" and "incomplete" force inline execution because they are recovery
|
|
@@ -7426,12 +7346,10 @@ export class AgentSession {
|
|
|
7426
7346
|
*/
|
|
7427
7347
|
async #runAutoShake(
|
|
7428
7348
|
reason: "overflow" | "threshold" | "idle" | "incomplete",
|
|
7429
|
-
strategy: "shake" | "shake-summary",
|
|
7430
7349
|
willRetry: boolean,
|
|
7431
7350
|
generation: number,
|
|
7432
7351
|
): Promise<"handled" | "fallback"> {
|
|
7433
|
-
const action =
|
|
7434
|
-
const mode = strategy === "shake-summary" ? "summary" : "elide";
|
|
7352
|
+
const action = "shake";
|
|
7435
7353
|
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
7436
7354
|
this.#autoCompactionAbortController?.abort();
|
|
7437
7355
|
const controller = new AbortController();
|
|
@@ -7439,7 +7357,7 @@ export class AgentSession {
|
|
|
7439
7357
|
const signal = controller.signal;
|
|
7440
7358
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
7441
7359
|
try {
|
|
7442
|
-
const result = await this.shake(
|
|
7360
|
+
const result = await this.shake("elide", { config: DEFAULT_SHAKE_CONFIG, signal });
|
|
7443
7361
|
if (signal.aborted) {
|
|
7444
7362
|
await this.#emitSessionEvent({
|
|
7445
7363
|
type: "auto_compaction_end",
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/** Mode selector for `AgentSession.shake`. */
|
|
9
|
-
export type ShakeMode = "elide" | "
|
|
9
|
+
export type ShakeMode = "elide" | "images";
|
|
10
10
|
|
|
11
11
|
/** Outcome of an `AgentSession.shake` run. */
|
|
12
12
|
export interface ShakeResult {
|
|
13
13
|
mode: ShakeMode;
|
|
14
|
-
/** Whole tool-call results dropped
|
|
14
|
+
/** Whole tool-call results dropped. */
|
|
15
15
|
toolResultsDropped: number;
|
|
16
|
-
/** Large fenced/XML blocks dropped
|
|
16
|
+
/** Large fenced/XML blocks dropped. */
|
|
17
17
|
blocksDropped: number;
|
|
18
18
|
/** Image blocks removed (images mode only). */
|
|
19
19
|
imagesDropped?: number;
|
|
@@ -39,6 +39,5 @@ export function formatShakeSummary(result: ShakeResult): string {
|
|
|
39
39
|
parts.push(`${result.blocksDropped} block${result.blocksDropped === 1 ? "" : "s"}`);
|
|
40
40
|
}
|
|
41
41
|
if (parts.length === 0) return "Nothing to shake.";
|
|
42
|
-
|
|
43
|
-
return `${verb} ${parts.join(" + ")} (~${result.tokensFreed} tokens freed).`;
|
|
42
|
+
return `Shook ${parts.join(" + ")} (~${result.tokensFreed} tokens freed).`;
|
|
44
43
|
}
|
|
@@ -62,9 +62,8 @@ const shutdownHandlerTui = (_command: ParsedSlashCommand, runtime: TuiSlashComma
|
|
|
62
62
|
function parseShakeMode(args: string): ShakeMode | { error: string } {
|
|
63
63
|
const verb = args.trim().toLowerCase();
|
|
64
64
|
if (verb === "" || verb === "elide") return "elide";
|
|
65
|
-
if (verb === "summary") return "summary";
|
|
66
65
|
if (verb === "images") return "images";
|
|
67
|
-
return { error: `Unknown /shake mode "${verb}". Use elide
|
|
66
|
+
return { error: `Unknown /shake mode "${verb}". Use elide or images.` };
|
|
68
67
|
}
|
|
69
68
|
|
|
70
69
|
const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
@@ -826,10 +825,9 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
826
825
|
acpDescription: "Shake heavy content out of the conversation context",
|
|
827
826
|
subcommands: [
|
|
828
827
|
{ name: "elide", description: "Strip tool results + large blocks (default)" },
|
|
829
|
-
{ name: "summary", description: "Compress heavy regions with a local on-device model" },
|
|
830
828
|
{ name: "images", description: "Strip image blocks" },
|
|
831
829
|
],
|
|
832
|
-
acpInputHint: "[elide|
|
|
830
|
+
acpInputHint: "[elide|images]",
|
|
833
831
|
allowArgs: true,
|
|
834
832
|
handle: async (command, runtime) => {
|
|
835
833
|
const mode = parseShakeMode(command.args);
|
package/src/task/executor.ts
CHANGED
|
@@ -633,7 +633,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
633
633
|
if (atMaxDepth && toolNames?.includes("task")) {
|
|
634
634
|
toolNames = toolNames.filter(name => name !== "task");
|
|
635
635
|
}
|
|
636
|
-
// IRC is always available; the
|
|
636
|
+
// IRC is always available; the COOP prompt section advertises it, so a restricted
|
|
637
637
|
// whitelist must still carry `irc` for the subagent to actually use it.
|
|
638
638
|
if (toolNames && !toolNames.includes("irc")) {
|
|
639
639
|
toolNames = [...toolNames, "irc"];
|
|
@@ -1446,9 +1446,19 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1446
1446
|
);
|
|
1447
1447
|
await awaitAbortable(session.waitForIdle());
|
|
1448
1448
|
} catch (err) {
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1449
|
+
if (abortSignal.aborted || err instanceof ToolAbortError) {
|
|
1450
|
+
// Benign control-flow exit — user cancel (^C) or compaction aborting
|
|
1451
|
+
// pending operations both surface here as ToolAbortError. The outer
|
|
1452
|
+
// catch and finally already mark the run aborted; logging at ERROR
|
|
1453
|
+
// would spam operator dashboards with non-failures.
|
|
1454
|
+
logger.debug("Subagent prompt aborted", {
|
|
1455
|
+
reason: abortReason ?? "signal",
|
|
1456
|
+
});
|
|
1457
|
+
} else {
|
|
1458
|
+
logger.error("Subagent prompt failed", {
|
|
1459
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1452
1462
|
}
|
|
1453
1463
|
}
|
|
1454
1464
|
|
package/src/task/index.ts
CHANGED
|
@@ -48,6 +48,7 @@ import { runSubprocess } from "./executor";
|
|
|
48
48
|
import { AgentOutputManager } from "./output-manager";
|
|
49
49
|
import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
|
|
50
50
|
import { renderResult, renderCall as renderTaskCall } from "./render";
|
|
51
|
+
import { repairTaskParams } from "./repair-args";
|
|
51
52
|
import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
|
|
52
53
|
import {
|
|
53
54
|
applyNestedPatches,
|
|
@@ -247,7 +248,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
247
248
|
}
|
|
248
249
|
|
|
249
250
|
renderCall(args: unknown, options: Parameters<typeof renderTaskCall>[1], theme: Theme) {
|
|
250
|
-
return renderTaskCall(args as TaskParams, options, theme);
|
|
251
|
+
return renderTaskCall(repairTaskParams(args as TaskParams), options, theme);
|
|
251
252
|
}
|
|
252
253
|
|
|
253
254
|
/** Dynamic description that reflects current disabled-agent settings */
|
|
@@ -292,7 +293,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
292
293
|
signal?: AbortSignal,
|
|
293
294
|
onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
|
|
294
295
|
): Promise<AgentToolResult<TaskToolDetails>> {
|
|
295
|
-
const params = rawParams as TaskParams;
|
|
296
|
+
const params = repairTaskParams(rawParams as TaskParams);
|
|
296
297
|
const simpleMode = this.#getTaskSimpleMode();
|
|
297
298
|
const validationError = validateTaskModeParams(simpleMode, params);
|
|
298
299
|
if (validationError) {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repair double-encoded JSON string arguments for the task tool.
|
|
3
|
+
*
|
|
4
|
+
* Models occasionally JSON-escape a string value twice when emitting a
|
|
5
|
+
* `task` tool call, so a `context`/`assignment` that should read
|
|
6
|
+
*
|
|
7
|
+
* # Role
|
|
8
|
+
* You are a judge … "describe this" … return —
|
|
9
|
+
*
|
|
10
|
+
* arrives — after the one JSON decode the provider already applied — as the
|
|
11
|
+
* literal text
|
|
12
|
+
*
|
|
13
|
+
* # Role\nYou are a judge … \"describe this\" … return \u2014
|
|
14
|
+
*
|
|
15
|
+
* i.e. every newline, quote, and unicode character is still backslash-escaped.
|
|
16
|
+
* The subagent then receives that garbled prompt, and the call preview renders
|
|
17
|
+
* one long blob with visible `\n` / `\"` / `\uXXXX`.
|
|
18
|
+
*
|
|
19
|
+
* The *whole-arguments* form of this quirk (the entire `arguments` blob is a
|
|
20
|
+
* JSON string) is already auto-corrected by the validator's JSON-string
|
|
21
|
+
* coercion. This module handles the *per-field* form, where the object parses
|
|
22
|
+
* fine but an individual string value is double-encoded — the validator never
|
|
23
|
+
* fires there because a double-encoded string is still a structurally valid
|
|
24
|
+
* string.
|
|
25
|
+
*
|
|
26
|
+
* This is deliberately scoped to the task tool's natural-language fields
|
|
27
|
+
* (`context`, `assignment`, `description`). It is NOT applied to code-bearing
|
|
28
|
+
* tools (write/edit/bash/search), where a backslash or quote is load-bearing
|
|
29
|
+
* and a false-positive unescape would silently corrupt a file or command.
|
|
30
|
+
*/
|
|
31
|
+
import type { TaskItem, TaskParams } from "./types";
|
|
32
|
+
|
|
33
|
+
/** A backslash that escapes a structural char — `\"`, `\\`, `\/`, or `\uXXXX`. */
|
|
34
|
+
const STRUCTURAL_ESCAPE = /\\(?:["\\/]|u[0-9a-fA-F]{4})/;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether `value` carries the signature of whole-string double-encoding rather
|
|
38
|
+
* than an incidental escape mention. A lone `\n`/`\t` in an instruction (e.g.
|
|
39
|
+
* "split lines on \n") is far more likely a literal mention than a
|
|
40
|
+
* double-encoded document, so it is left alone; a structural escape (`\"`,
|
|
41
|
+
* `\\`, `\uXXXX`) or two-plus escape sequences indicates a re-escaped payload.
|
|
42
|
+
*/
|
|
43
|
+
function hasDoubleEncodeSignature(value: string): boolean {
|
|
44
|
+
if (STRUCTURAL_ESCAPE.test(value)) return true;
|
|
45
|
+
let count = 0;
|
|
46
|
+
for (let i = 0; i < value.length; i++) {
|
|
47
|
+
if (value.charCodeAt(i) === 0x5c /* \ */) {
|
|
48
|
+
count += 1;
|
|
49
|
+
if (count >= 2) return true;
|
|
50
|
+
i += 1; // skip the escaped char so `\\` counts once
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Return the once-unescaped string when `value` is uniformly double-encoded
|
|
58
|
+
* JSON (a well-formed JSON string body that decodes to a different string);
|
|
59
|
+
* otherwise return `value` unchanged.
|
|
60
|
+
*
|
|
61
|
+
* The `JSON.parse(\`"${value}"\`)` round-trip is the safety net: it only
|
|
62
|
+
* succeeds when *every* backslash begins a valid JSON escape and no bare
|
|
63
|
+
* double-quote exists — exactly the signature of double-encoding. Genuine
|
|
64
|
+
* prose with a Windows path (`C:\Users`), a regex (`\d+`), an embedded quote,
|
|
65
|
+
* or a real (already-decoded) newline makes the parse throw, so the value is
|
|
66
|
+
* returned untouched.
|
|
67
|
+
*/
|
|
68
|
+
export function repairDoubleEncodedJsonString(value: string): string {
|
|
69
|
+
// Fast path: no backslash → nothing was escaped → the parse can never differ.
|
|
70
|
+
if (!value.includes("\\")) return value;
|
|
71
|
+
if (!hasDoubleEncodeSignature(value)) return value;
|
|
72
|
+
let decoded: unknown;
|
|
73
|
+
try {
|
|
74
|
+
decoded = JSON.parse(`"${value}"`);
|
|
75
|
+
} catch {
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
return typeof decoded === "string" && decoded !== value ? decoded : value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Repair a single (possibly partial) task item's prose fields. */
|
|
82
|
+
function repairTaskItem(task: TaskItem): TaskItem {
|
|
83
|
+
if (task === null || typeof task !== "object") return task;
|
|
84
|
+
const assignment =
|
|
85
|
+
typeof task.assignment === "string" ? repairDoubleEncodedJsonString(task.assignment) : task.assignment;
|
|
86
|
+
const description =
|
|
87
|
+
typeof task.description === "string" ? repairDoubleEncodedJsonString(task.description) : task.description;
|
|
88
|
+
if (assignment === task.assignment && description === task.description) return task;
|
|
89
|
+
return { ...task, assignment, description };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Repair double-encoded prose in task-tool params (`context` and each task's
|
|
94
|
+
* `assignment`/`description`). Returns the same reference when nothing changed
|
|
95
|
+
* so callers can cheaply skip work. Defensive against partially-streamed args
|
|
96
|
+
* (missing/undefined fields, partial task arrays) so it is safe on the render
|
|
97
|
+
* path as well as on execution.
|
|
98
|
+
*/
|
|
99
|
+
export function repairTaskParams(params: TaskParams): TaskParams {
|
|
100
|
+
if (params === null || typeof params !== "object") return params;
|
|
101
|
+
|
|
102
|
+
const context = typeof params.context === "string" ? repairDoubleEncodedJsonString(params.context) : params.context;
|
|
103
|
+
|
|
104
|
+
let tasks = params.tasks;
|
|
105
|
+
if (Array.isArray(params.tasks)) {
|
|
106
|
+
let changed = false;
|
|
107
|
+
const repaired = params.tasks.map(task => {
|
|
108
|
+
const next = repairTaskItem(task);
|
|
109
|
+
if (next !== task) changed = true;
|
|
110
|
+
return next;
|
|
111
|
+
});
|
|
112
|
+
if (changed) tasks = repaired;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (context === params.context && tasks === params.tasks) return params;
|
|
116
|
+
return { ...params, context, tasks };
|
|
117
|
+
}
|
package/src/tiny/models.ts
CHANGED
|
@@ -196,34 +196,6 @@ export function getTinyMemoryModelSpec(key: TinyMemoryLocalModelKey): (typeof TI
|
|
|
196
196
|
return spec;
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
/**
|
|
200
|
-
* Shake-summary models. Shake's `summary` mode (and the `shake-summary`
|
|
201
|
-
* compaction strategy) compress heavy regions strictly on-device — there is no
|
|
202
|
-
* online/remote option, so this registry reuses the local memory models only.
|
|
203
|
-
*/
|
|
204
|
-
export const SHAKE_SUMMARY_MODEL_VALUES = [
|
|
205
|
-
"qwen3-1.7b",
|
|
206
|
-
"gemma-3-1b",
|
|
207
|
-
"qwen2.5-1.5b",
|
|
208
|
-
"lfm2-1.2b",
|
|
209
|
-
] as const satisfies readonly TinyMemoryLocalModelKey[];
|
|
210
|
-
|
|
211
|
-
export type ShakeSummaryModelKey = (typeof SHAKE_SUMMARY_MODEL_VALUES)[number];
|
|
212
|
-
|
|
213
|
-
// Guard: every local memory model is offered for shake summary (catches drift).
|
|
214
|
-
type MissingShakeSummaryValue = Exclude<TinyMemoryLocalModelKey, ShakeSummaryModelKey>;
|
|
215
|
-
const SHAKE_SUMMARY_MODEL_VALUES_MATCH_REGISTRY: MissingShakeSummaryValue extends never ? true : never = true;
|
|
216
|
-
void SHAKE_SUMMARY_MODEL_VALUES_MATCH_REGISTRY;
|
|
217
|
-
|
|
218
|
-
export const SHAKE_SUMMARY_MODEL_OPTIONS = TINY_MEMORY_LOCAL_MODELS.map(model => ({
|
|
219
|
-
value: model.key,
|
|
220
|
-
label: model.label,
|
|
221
|
-
description: model.description,
|
|
222
|
-
})) satisfies ReadonlyArray<{ value: ShakeSummaryModelKey; label: string; description: string }>;
|
|
223
|
-
|
|
224
|
-
/** Default shake-summary local model when none is named. */
|
|
225
|
-
export const DEFAULT_SHAKE_SUMMARY_MODEL_KEY: ShakeSummaryModelKey = DEFAULT_MEMORY_LOCAL_MODEL_KEY;
|
|
226
|
-
|
|
227
199
|
/** Any local model key (title or memory), used by the shared inference worker. */
|
|
228
200
|
export type TinyLocalModelKey = TinyTitleLocalModelKey | TinyMemoryLocalModelKey;
|
|
229
201
|
|