@oh-my-pi/pi-coding-agent 15.7.2 → 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 +114 -6
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/cli/extension-flags.d.ts +36 -0
- package/dist/types/config/config-file.d.ts +4 -0
- package/dist/types/config/file-lock.d.ts +23 -0
- package/dist/types/config/keybindings.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +69 -65
- package/dist/types/edit/hashline/diff.d.ts +3 -3
- package/dist/types/eval/__tests__/agent-bridge.test.d.ts +1 -0
- package/dist/types/eval/__tests__/budget-bridge.test.d.ts +1 -0
- package/dist/types/eval/__tests__/idle-timeout.test.d.ts +1 -0
- package/dist/types/eval/agent-bridge.d.ts +25 -0
- package/dist/types/eval/backend.d.ts +17 -2
- package/dist/types/eval/budget-bridge.d.ts +29 -0
- package/dist/types/eval/idle-timeout.d.ts +28 -0
- package/dist/types/eval/js/executor.d.ts +8 -0
- package/dist/types/eval/js/tool-bridge.d.ts +2 -1
- package/dist/types/eval/py/executor.d.ts +13 -0
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +7 -0
- package/dist/types/extensibility/plugins/git-url.d.ts +11 -1
- package/dist/types/extensibility/plugins/manager.d.ts +12 -1
- 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/memory-backend/index.d.ts +1 -1
- package/dist/types/memory-backend/resolve.d.ts +1 -1
- package/dist/types/memory-backend/types.d.ts +3 -3
- package/dist/types/mnemopi/backend.d.ts +4 -0
- package/dist/types/mnemopi/config.d.ts +29 -0
- package/dist/types/mnemopi/state.d.ts +72 -0
- package/dist/types/modes/components/custom-editor.d.ts +2 -2
- package/dist/types/modes/components/omfg-panel.d.ts +19 -0
- package/dist/types/modes/controllers/command-controller.d.ts +6 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -3
- package/dist/types/modes/controllers/omfg-controller.d.ts +10 -0
- package/dist/types/modes/controllers/omfg-rule.d.ts +26 -0
- package/dist/types/modes/gradient-highlight.d.ts +5 -1
- package/dist/types/modes/interactive-mode.d.ts +7 -3
- package/dist/types/modes/magic-keywords.d.ts +14 -0
- package/dist/types/modes/markdown-prose.d.ts +27 -0
- package/dist/types/modes/orchestrate.d.ts +7 -2
- package/dist/types/modes/shared.d.ts +1 -1
- package/dist/types/modes/turn-budget.d.ts +18 -0
- package/dist/types/modes/types.d.ts +7 -3
- package/dist/types/modes/ultrathink.d.ts +7 -2
- package/dist/types/modes/workflow.d.ts +15 -0
- package/dist/types/sdk.d.ts +13 -3
- package/dist/types/session/agent-session.d.ts +36 -17
- package/dist/types/session/session-manager.d.ts +18 -0
- package/dist/types/session/session-storage.d.ts +6 -0
- package/dist/types/session/shake-types.d.ts +24 -0
- package/dist/types/task/executor.d.ts +2 -2
- package/dist/types/task/repair-args.d.ts +52 -0
- package/dist/types/tiny/models.d.ts +1 -1
- package/dist/types/tiny/title-client.d.ts +28 -2
- package/dist/types/tiny/title-protocol.d.ts +8 -5
- package/dist/types/tools/find.d.ts +1 -1
- package/dist/types/tools/index.d.ts +19 -3
- package/dist/types/tools/memory-edit.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/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/package.json +10 -10
- package/scripts/build-binary.ts +0 -1
- package/src/autoresearch/tools/run-experiment.ts +45 -113
- package/src/cli/args.ts +39 -16
- package/src/cli/extension-flags.ts +48 -0
- package/src/cli/plugin-cli.ts +11 -2
- package/src/cli.ts +59 -0
- package/src/config/config-file.ts +98 -13
- package/src/config/file-lock.ts +60 -17
- package/src/config/keybindings.ts +78 -27
- package/src/config/model-registry.ts +7 -1
- package/src/config/settings-schema.ts +73 -67
- package/src/config/settings.ts +22 -0
- package/src/edit/hashline/diff.ts +81 -24
- package/src/edit/renderer.ts +16 -12
- package/src/eval/__tests__/agent-bridge.test.ts +433 -0
- package/src/eval/__tests__/budget-bridge.test.ts +69 -0
- package/src/eval/__tests__/idle-timeout.test.ts +66 -0
- package/src/eval/__tests__/shared-executors.test.ts +21 -0
- package/src/eval/agent-bridge.ts +295 -0
- package/src/eval/backend.ts +17 -2
- package/src/eval/budget-bridge.ts +48 -0
- package/src/eval/idle-timeout.ts +80 -0
- package/src/eval/js/executor.ts +35 -7
- package/src/eval/js/index.ts +2 -1
- package/src/eval/js/shared/prelude.txt +85 -1
- package/src/eval/js/tool-bridge.ts +9 -0
- package/src/eval/py/executor.ts +41 -14
- package/src/eval/py/index.ts +2 -1
- package/src/eval/py/prelude.py +132 -1
- package/src/exec/bash-executor.ts +2 -3
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/runner.ts +12 -2
- package/src/extensibility/plugins/git-url.ts +90 -4
- package/src/extensibility/plugins/manager.ts +103 -7
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +88 -88
- 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/main.ts +44 -55
- package/src/mcp/manager.ts +87 -4
- package/src/memory-backend/index.ts +1 -1
- package/src/memory-backend/resolve.ts +3 -3
- package/src/memory-backend/types.ts +3 -3
- package/src/{mnemosyne → mnemopi}/backend.ts +70 -70
- package/src/{mnemosyne → mnemopi}/config.ts +36 -36
- package/src/{mnemosyne → mnemopi}/state.ts +67 -67
- package/src/modes/components/agent-dashboard.ts +6 -6
- package/src/modes/components/custom-editor.ts +4 -11
- package/src/modes/components/extensions/state-manager.ts +3 -4
- package/src/modes/components/footer.ts +8 -9
- package/src/modes/components/hook-selector.ts +86 -20
- package/src/modes/components/oauth-selector.ts +93 -21
- package/src/modes/components/omfg-panel.ts +141 -0
- package/src/modes/components/settings-defs.ts +2 -2
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +38 -19
- package/src/modes/components/tree-selector.ts +4 -3
- package/src/modes/components/user-message-selector.ts +94 -19
- package/src/modes/components/user-message.ts +8 -1
- package/src/modes/controllers/command-controller.ts +25 -0
- package/src/modes/controllers/event-controller.ts +68 -3
- package/src/modes/controllers/input-controller.ts +14 -11
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/controllers/omfg-controller.ts +283 -0
- package/src/modes/controllers/omfg-rule.ts +647 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/modes/gradient-highlight.ts +23 -6
- package/src/modes/interactive-mode.ts +41 -7
- package/src/modes/magic-keywords.ts +20 -0
- package/src/modes/markdown-prose.ts +247 -0
- package/src/modes/orchestrate.ts +17 -11
- package/src/modes/shared.ts +3 -11
- package/src/modes/turn-budget.ts +31 -0
- package/src/modes/types.ts +7 -1
- package/src/modes/ultrathink.ts +16 -10
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/modes/workflow.ts +42 -0
- package/src/prompts/system/omfg-user.md +51 -0
- 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 +9 -6
- package/src/prompts/system/workflow-notice.md +70 -0
- package/src/prompts/tools/eval.md +13 -1
- package/src/prompts/tools/memory-edit.md +1 -1
- package/src/sdk.ts +63 -33
- package/src/session/agent-session.ts +290 -55
- package/src/session/session-manager.ts +32 -0
- package/src/session/session-storage.ts +68 -8
- package/src/session/shake-types.ts +43 -0
- package/src/slash-commands/builtin-registry.ts +39 -16
- package/src/task/executor.ts +17 -7
- package/src/task/index.ts +9 -8
- package/src/task/repair-args.ts +117 -0
- package/src/tiny/models.ts +2 -2
- package/src/tiny/title-client.ts +133 -43
- package/src/tiny/title-protocol.ts +10 -5
- package/src/tiny/worker.ts +3 -46
- package/src/tools/ast-edit.ts +3 -0
- package/src/tools/ast-grep.ts +3 -0
- package/src/tools/eval.ts +202 -26
- package/src/tools/find.ts +20 -6
- package/src/tools/gh.ts +1 -0
- package/src/tools/grouped-file-output.ts +9 -2
- package/src/tools/index.ts +17 -5
- package/src/tools/memory-edit.ts +4 -4
- package/src/tools/memory-recall.ts +5 -5
- package/src/tools/memory-reflect.ts +5 -5
- package/src/tools/memory-retain.ts +4 -4
- package/src/tools/path-utils.ts +13 -2
- package/src/tools/read.ts +1 -0
- package/src/tools/render-utils.ts +2 -1
- package/src/tools/search.ts +491 -76
- package/src/tui/output-block.ts +37 -75
- package/src/utils/git.ts +9 -3
- package/dist/types/mnemosyne/backend.d.ts +0 -4
- package/dist/types/mnemosyne/config.d.ts +0 -29
- package/dist/types/mnemosyne/state.d.ts +0 -72
- /package/dist/types/{mnemosyne → mnemopi}/index.d.ts +0 -0
- /package/src/{mnemosyne → mnemopi}/index.ts +0 -0
|
@@ -272,8 +272,8 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
|
|
|
272
272
|
if (this.#closed) throw new Error("Writer closed");
|
|
273
273
|
if (this.#error) throw this.#error;
|
|
274
274
|
try {
|
|
275
|
-
|
|
276
|
-
this.#storage.
|
|
275
|
+
// O(1) chunked append — see MemorySessionStorage.appendChunkSync.
|
|
276
|
+
this.#storage.appendChunkSync(this.#path, line);
|
|
277
277
|
} catch (err) {
|
|
278
278
|
throw this.#recordError(err);
|
|
279
279
|
}
|
|
@@ -302,8 +302,26 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
|
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Mirror entry stored per path. Chunks accumulate via O(1) `push` on
|
|
307
|
+
* `appendChunkSync`; readers materialise into a single string lazily.
|
|
308
|
+
* `byteLen` is kept in sync so `statSync` is O(1) (and returns true UTF-8
|
|
309
|
+
* bytes, not character count).
|
|
310
|
+
*/
|
|
311
|
+
interface MirrorEntry {
|
|
312
|
+
chunks: string[];
|
|
313
|
+
byteLen: number;
|
|
314
|
+
mtimeMs: number;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function materialiseMirror(entry: MirrorEntry): string {
|
|
318
|
+
if (entry.chunks.length === 0) return "";
|
|
319
|
+
if (entry.chunks.length === 1) return entry.chunks[0];
|
|
320
|
+
return entry.chunks.join("");
|
|
321
|
+
}
|
|
322
|
+
|
|
305
323
|
export class MemorySessionStorage implements SessionStorage {
|
|
306
|
-
#files = new Map<string,
|
|
324
|
+
#files = new Map<string, MirrorEntry>();
|
|
307
325
|
|
|
308
326
|
ensureDirSync(_dir: string): void {
|
|
309
327
|
// No-op for in-memory storage.
|
|
@@ -314,20 +332,40 @@ export class MemorySessionStorage implements SessionStorage {
|
|
|
314
332
|
}
|
|
315
333
|
|
|
316
334
|
writeTextSync(path: string, content: string): void {
|
|
317
|
-
this.#files.set(path, {
|
|
335
|
+
this.#files.set(path, {
|
|
336
|
+
chunks: content.length === 0 ? [] : [content],
|
|
337
|
+
byteLen: Buffer.byteLength(content, "utf-8"),
|
|
338
|
+
mtimeMs: Date.now(),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Internal O(1) append used by {@link MemorySessionStorageWriter}. Lazily
|
|
344
|
+
* creates the entry. External callers should go through `openWriter()`
|
|
345
|
+
* rather than touching the mirror directly.
|
|
346
|
+
*/
|
|
347
|
+
appendChunkSync(path: string, chunk: string): void {
|
|
348
|
+
let entry = this.#files.get(path);
|
|
349
|
+
if (!entry) {
|
|
350
|
+
entry = { chunks: [], byteLen: 0, mtimeMs: Date.now() };
|
|
351
|
+
this.#files.set(path, entry);
|
|
352
|
+
}
|
|
353
|
+
entry.chunks.push(chunk);
|
|
354
|
+
entry.byteLen += Buffer.byteLength(chunk, "utf-8");
|
|
355
|
+
entry.mtimeMs = Date.now();
|
|
318
356
|
}
|
|
319
357
|
|
|
320
358
|
readTextSync(path: string): string {
|
|
321
359
|
const entry = this.#files.get(path);
|
|
322
360
|
if (!entry) throw new Error(`File not found: ${path}`);
|
|
323
|
-
return entry
|
|
361
|
+
return materialiseMirror(entry);
|
|
324
362
|
}
|
|
325
363
|
|
|
326
364
|
statSync(path: string): SessionStorageStat {
|
|
327
365
|
const entry = this.#files.get(path);
|
|
328
366
|
if (!entry) throw new Error(`File not found: ${path}`);
|
|
329
367
|
return {
|
|
330
|
-
size: entry.
|
|
368
|
+
size: entry.byteLen,
|
|
331
369
|
mtimeMs: entry.mtimeMs,
|
|
332
370
|
mtime: new Date(entry.mtimeMs),
|
|
333
371
|
};
|
|
@@ -353,13 +391,35 @@ export class MemorySessionStorage implements SessionStorage {
|
|
|
353
391
|
readText(path: string): Promise<string> {
|
|
354
392
|
const entry = this.#files.get(path);
|
|
355
393
|
if (!entry) return Promise.reject(new Error(`File not found: ${path}`));
|
|
356
|
-
return Promise.resolve(entry
|
|
394
|
+
return Promise.resolve(materialiseMirror(entry));
|
|
357
395
|
}
|
|
358
396
|
|
|
359
397
|
readTextPrefix(path: string, maxBytes: number): Promise<string> {
|
|
360
398
|
const entry = this.#files.get(path);
|
|
361
399
|
if (!entry) return Promise.reject(new Error(`File not found: ${path}`));
|
|
362
|
-
|
|
400
|
+
if (entry.chunks.length === 0 || maxBytes <= 0) return Promise.resolve("");
|
|
401
|
+
|
|
402
|
+
// Walk chunks until the byte budget is exhausted. Avoids materialising
|
|
403
|
+
// the full mirror just to slice a prefix — bounded work for big files.
|
|
404
|
+
let accumulatedBytes = 0;
|
|
405
|
+
const out: string[] = [];
|
|
406
|
+
for (const chunk of entry.chunks) {
|
|
407
|
+
const chunkBytes = Buffer.byteLength(chunk, "utf-8");
|
|
408
|
+
if (accumulatedBytes + chunkBytes <= maxBytes) {
|
|
409
|
+
out.push(chunk);
|
|
410
|
+
accumulatedBytes += chunkBytes;
|
|
411
|
+
if (accumulatedBytes === maxBytes) break;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
// Boundary chunk: slice in byte space and decode. Result MAY be
|
|
415
|
+
// shorter than the budget if a multi-byte codepoint straddles the
|
|
416
|
+
// boundary — matches `peekFile` semantics (partial decode at cap).
|
|
417
|
+
const remainingBytes = maxBytes - accumulatedBytes;
|
|
418
|
+
const utf8 = Buffer.from(chunk, "utf-8");
|
|
419
|
+
out.push(utf8Decoder.decode(utf8.subarray(0, remainingBytes)));
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
return Promise.resolve(out.join(""));
|
|
363
423
|
}
|
|
364
424
|
|
|
365
425
|
writeText(path: string, content: string): Promise<void> {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public shape of the `shake` operation, kept in a dependency-free leaf module
|
|
3
|
+
* so slash-command registries and controllers can import `formatShakeSummary`
|
|
4
|
+
* without pulling in the heavy `agent-session` module graph (which would form
|
|
5
|
+
* an import cycle through the slash-command registry).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Mode selector for `AgentSession.shake`. */
|
|
9
|
+
export type ShakeMode = "elide" | "images";
|
|
10
|
+
|
|
11
|
+
/** Outcome of an `AgentSession.shake` run. */
|
|
12
|
+
export interface ShakeResult {
|
|
13
|
+
mode: ShakeMode;
|
|
14
|
+
/** Whole tool-call results dropped. */
|
|
15
|
+
toolResultsDropped: number;
|
|
16
|
+
/** Large fenced/XML blocks dropped. */
|
|
17
|
+
blocksDropped: number;
|
|
18
|
+
/** Image blocks removed (images mode only). */
|
|
19
|
+
imagesDropped?: number;
|
|
20
|
+
/** Estimated context tokens reclaimed. */
|
|
21
|
+
tokensFreed: number;
|
|
22
|
+
/** Session artifact holding the dropped originals, when persisted. */
|
|
23
|
+
artifactId?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** One-line operator summary of a {@link ShakeResult} (shared by TUI + ACP). */
|
|
27
|
+
export function formatShakeSummary(result: ShakeResult): string {
|
|
28
|
+
if (result.mode === "images") {
|
|
29
|
+
const n = result.imagesDropped ?? 0;
|
|
30
|
+
return n === 0
|
|
31
|
+
? "No images found in this session."
|
|
32
|
+
: `Dropped ${n} image${n === 1 ? "" : "s"} from this session.`;
|
|
33
|
+
}
|
|
34
|
+
const parts: string[] = [];
|
|
35
|
+
if (result.toolResultsDropped > 0) {
|
|
36
|
+
parts.push(`${result.toolResultsDropped} tool result${result.toolResultsDropped === 1 ? "" : "s"}`);
|
|
37
|
+
}
|
|
38
|
+
if (result.blocksDropped > 0) {
|
|
39
|
+
parts.push(`${result.blocksDropped} block${result.blocksDropped === 1 ? "" : "s"}`);
|
|
40
|
+
}
|
|
41
|
+
if (parts.length === 0) return "Nothing to shake.";
|
|
42
|
+
return `Shook ${parts.join(" + ")} (~${result.tokensFreed} tokens freed).`;
|
|
43
|
+
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from "../extensibility/plugins/marketplace";
|
|
22
22
|
import { resolveMemoryBackend } from "../memory-backend";
|
|
23
23
|
import type { InteractiveModeContext } from "../modes/types";
|
|
24
|
+
import { formatShakeSummary, type ShakeMode } from "../session/shake-types";
|
|
24
25
|
import { getChangelogPath, parseChangelog } from "../utils/changelog";
|
|
25
26
|
import { buildContextReportText } from "./helpers/context-report";
|
|
26
27
|
import { formatDuration } from "./helpers/format";
|
|
@@ -57,6 +58,14 @@ const shutdownHandlerTui = (_command: ParsedSlashCommand, runtime: TuiSlashComma
|
|
|
57
58
|
return commandConsumed();
|
|
58
59
|
};
|
|
59
60
|
|
|
61
|
+
/** Parse the `/shake` subcommand into a {@link ShakeMode}; empty defaults to elide. */
|
|
62
|
+
function parseShakeMode(args: string): ShakeMode | { error: string } {
|
|
63
|
+
const verb = args.trim().toLowerCase();
|
|
64
|
+
if (verb === "" || verb === "elide") return "elide";
|
|
65
|
+
if (verb === "images") return "images";
|
|
66
|
+
return { error: `Unknown /shake mode "${verb}". Use elide or images.` };
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
61
70
|
{
|
|
62
71
|
name: "settings",
|
|
@@ -811,27 +820,30 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
811
820
|
},
|
|
812
821
|
},
|
|
813
822
|
{
|
|
814
|
-
name: "
|
|
815
|
-
description: "
|
|
816
|
-
acpDescription: "
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
823
|
+
name: "shake",
|
|
824
|
+
description: "Drop heavy content from context (tool results, large blocks)",
|
|
825
|
+
acpDescription: "Shake heavy content out of the conversation context",
|
|
826
|
+
subcommands: [
|
|
827
|
+
{ name: "elide", description: "Strip tool results + large blocks (default)" },
|
|
828
|
+
{ name: "images", description: "Strip image blocks" },
|
|
829
|
+
],
|
|
830
|
+
acpInputHint: "[elide|images]",
|
|
831
|
+
allowArgs: true,
|
|
832
|
+
handle: async (command, runtime) => {
|
|
833
|
+
const mode = parseShakeMode(command.args);
|
|
834
|
+
if (typeof mode !== "string") return usage(mode.error, runtime);
|
|
835
|
+
const result = await runtime.session.shake(mode);
|
|
836
|
+
await runtime.output(formatShakeSummary(result));
|
|
824
837
|
return commandConsumed();
|
|
825
838
|
},
|
|
826
|
-
handleTui: async (
|
|
839
|
+
handleTui: async (command, runtime) => {
|
|
827
840
|
runtime.ctx.editor.setText("");
|
|
828
|
-
const
|
|
829
|
-
if (
|
|
830
|
-
runtime.ctx.
|
|
841
|
+
const mode = parseShakeMode(command.args);
|
|
842
|
+
if (typeof mode !== "string") {
|
|
843
|
+
runtime.ctx.showWarning(mode.error);
|
|
831
844
|
return;
|
|
832
845
|
}
|
|
833
|
-
runtime.ctx.
|
|
834
|
-
runtime.ctx.showStatus(`Dropped ${removed} image${removed === 1 ? "" : "s"} from this session.`);
|
|
846
|
+
await runtime.ctx.handleShakeCommand(mode);
|
|
835
847
|
},
|
|
836
848
|
},
|
|
837
849
|
{
|
|
@@ -864,6 +876,17 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
864
876
|
await runtime.ctx.handleBtwCommand(question);
|
|
865
877
|
},
|
|
866
878
|
},
|
|
879
|
+
{
|
|
880
|
+
name: "omfg",
|
|
881
|
+
description: "Forge a TTSR rule from a complaint to stop a recurring behavior",
|
|
882
|
+
inlineHint: "<complaint>",
|
|
883
|
+
allowArgs: true,
|
|
884
|
+
handleTui: async (command, runtime) => {
|
|
885
|
+
const complaint = command.text.slice(`/${command.name}`.length).trim();
|
|
886
|
+
runtime.ctx.editor.setText("");
|
|
887
|
+
await runtime.ctx.handleOmfgCommand(complaint);
|
|
888
|
+
},
|
|
889
|
+
},
|
|
867
890
|
{
|
|
868
891
|
name: "retry",
|
|
869
892
|
description: "Retry the last failed agent turn",
|
package/src/task/executor.ts
CHANGED
|
@@ -21,7 +21,7 @@ import type { HindsightSessionState } from "../hindsight/state";
|
|
|
21
21
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
22
22
|
import { callTool } from "../mcp/client";
|
|
23
23
|
import type { MCPManager } from "../mcp/manager";
|
|
24
|
-
import type {
|
|
24
|
+
import type { MnemopiSessionState } from "../mnemopi/state";
|
|
25
25
|
import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
|
|
26
26
|
import submitReminderTemplate from "../prompts/system/subagent-yield-reminder.md" with { type: "text" };
|
|
27
27
|
import { AgentRegistry } from "../registry/agent-registry";
|
|
@@ -186,7 +186,7 @@ export interface ExecutorOptions {
|
|
|
186
186
|
*/
|
|
187
187
|
parentArtifactManager?: ArtifactManager;
|
|
188
188
|
parentHindsightSessionState?: HindsightSessionState;
|
|
189
|
-
|
|
189
|
+
parentMnemopiSessionState?: MnemopiSessionState;
|
|
190
190
|
/** Parent agent's eval executor session id. Subagents reuse it so eval state is shared. */
|
|
191
191
|
parentEvalSessionId?: string;
|
|
192
192
|
/**
|
|
@@ -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"];
|
|
@@ -1245,7 +1245,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1245
1245
|
spawns: spawnsEnv,
|
|
1246
1246
|
taskDepth: childDepth,
|
|
1247
1247
|
parentHindsightSessionState: options.parentHindsightSessionState,
|
|
1248
|
-
|
|
1248
|
+
parentMnemopiSessionState: options.parentMnemopiSessionState,
|
|
1249
1249
|
parentTaskPrefix: id,
|
|
1250
1250
|
agentId: id,
|
|
1251
1251
|
agentDisplayName: agent.name,
|
|
@@ -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) {
|
|
@@ -762,8 +763,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
762
763
|
const tempArtifactsDir = artifactsDir ? null : path.join(os.tmpdir(), `omp-task-${Snowflake.next()}`);
|
|
763
764
|
const effectiveArtifactsDir = artifactsDir || tempArtifactsDir!;
|
|
764
765
|
|
|
765
|
-
|
|
766
|
-
const localProtocolOptions: LocalProtocolOptions = {
|
|
766
|
+
const localProtocolOptions: LocalProtocolOptions = this.session.localProtocolOptions ?? {
|
|
767
767
|
getArtifactsDir: this.session.getArtifactsDir ?? (() => null),
|
|
768
768
|
getSessionId: this.session.getSessionId ?? (() => null),
|
|
769
769
|
};
|
|
@@ -864,6 +864,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
864
864
|
);
|
|
865
865
|
const promptTemplates = this.session.promptTemplates;
|
|
866
866
|
const parentEvalSessionId = this.session.getEvalSessionId?.() ?? undefined;
|
|
867
|
+
const mcpManager = this.session.mcpManager ?? MCPManager.instance();
|
|
867
868
|
|
|
868
869
|
// Initialize progress for all tasks
|
|
869
870
|
for (let i = 0; i < tasksWithUniqueIds.length; i++) {
|
|
@@ -921,7 +922,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
921
922
|
authStorage: this.session.authStorage,
|
|
922
923
|
modelRegistry: this.session.modelRegistry,
|
|
923
924
|
settings: this.session.settings,
|
|
924
|
-
mcpManager
|
|
925
|
+
mcpManager,
|
|
925
926
|
contextFiles,
|
|
926
927
|
skills: availableSkills,
|
|
927
928
|
autoloadSkills: resolvedAutoloadSkills,
|
|
@@ -930,7 +931,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
930
931
|
localProtocolOptions,
|
|
931
932
|
parentArtifactManager,
|
|
932
933
|
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
|
933
|
-
|
|
934
|
+
parentMnemopiSessionState: this.session.getMnemopiSessionState?.(),
|
|
934
935
|
parentTelemetry: this.session.getTelemetry?.(),
|
|
935
936
|
parentEvalSessionId,
|
|
936
937
|
});
|
|
@@ -978,7 +979,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
978
979
|
authStorage: this.session.authStorage,
|
|
979
980
|
modelRegistry: this.session.modelRegistry,
|
|
980
981
|
settings: this.session.settings,
|
|
981
|
-
mcpManager
|
|
982
|
+
mcpManager,
|
|
982
983
|
contextFiles,
|
|
983
984
|
skills: availableSkills,
|
|
984
985
|
autoloadSkills: resolvedAutoloadSkills,
|
|
@@ -987,7 +988,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
987
988
|
localProtocolOptions,
|
|
988
989
|
parentArtifactManager,
|
|
989
990
|
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
|
990
|
-
|
|
991
|
+
parentMnemopiSessionState: this.session.getMnemopiSessionState?.(),
|
|
991
992
|
parentTelemetry: this.session.getTelemetry?.(),
|
|
992
993
|
parentEvalSessionId,
|
|
993
994
|
});
|
|
@@ -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
|
@@ -108,7 +108,7 @@ export const ONLINE_MEMORY_MODEL_KEY = "online";
|
|
|
108
108
|
export const DEFAULT_MEMORY_LOCAL_MODEL_KEY = "qwen3-1.7b";
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
|
-
* Local models for
|
|
111
|
+
* Local models for Mnemopi memory tasks (fact extraction + consolidation).
|
|
112
112
|
* These are larger (1B-1.7B) than the title models: structured extraction and
|
|
113
113
|
* faithful summarization need more capacity than 3-6 word titles. All q4.
|
|
114
114
|
* Ranking/recipe rationale lives in docs/local-models.md.
|
|
@@ -177,7 +177,7 @@ export const TINY_MEMORY_MODEL_OPTIONS = [
|
|
|
177
177
|
value: ONLINE_MEMORY_MODEL_KEY,
|
|
178
178
|
label: "Online (smol/remote)",
|
|
179
179
|
description:
|
|
180
|
-
"Use the configured
|
|
180
|
+
"Use the configured Mnemopi LLM mode (smol or remote); no local model download or on-device inference.",
|
|
181
181
|
},
|
|
182
182
|
...TINY_MEMORY_LOCAL_MODELS.map(model => ({
|
|
183
183
|
value: model.key,
|