@oh-my-pi/pi-coding-agent 15.12.4 → 15.13.0
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 +304 -6
- package/dist/cli.js +1015 -881
- package/dist/types/async/job-manager.d.ts +15 -0
- package/dist/types/autolearn/controller.d.ts +25 -0
- package/dist/types/autolearn/managed-skills.d.ts +45 -0
- package/dist/types/autoresearch/state.d.ts +1 -1
- package/dist/types/autoresearch/types.d.ts +1 -1
- package/dist/types/cli/args.d.ts +19 -1
- package/dist/types/cli/session-picker.d.ts +1 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/cli/setup-model-picker.d.ts +14 -0
- package/dist/types/collab/protocol.d.ts +1 -1
- package/dist/types/commands/say.d.ts +24 -0
- package/dist/types/config/keybindings.d.ts +3 -3
- package/dist/types/config/model-registry.d.ts +10 -0
- package/dist/types/config/models-config-schema.d.ts +12 -0
- package/dist/types/config/models-config.d.ts +8 -2
- package/dist/types/config/settings-schema.d.ts +261 -58
- package/dist/types/export/html/index.d.ts +2 -1
- package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +47 -1
- package/dist/types/extensibility/hooks/index.d.ts +2 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
- package/dist/types/extensibility/plugins/loader.d.ts +11 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/extensibility/skills.d.ts +10 -0
- package/dist/types/goals/guided-setup.d.ts +18 -0
- package/dist/types/goals/state.d.ts +1 -1
- package/dist/types/hindsight/transcript.d.ts +1 -1
- package/dist/types/index.d.ts +5 -0
- package/dist/types/internal-urls/local-protocol.d.ts +4 -2
- package/dist/types/main.d.ts +4 -3
- package/dist/types/mcp/startup-events.d.ts +11 -0
- package/dist/types/memories/index.d.ts +7 -0
- package/dist/types/memory-backend/local-backend.d.ts +4 -3
- package/dist/types/mnemopi/config.d.ts +4 -4
- package/dist/types/modes/components/agent-hub.d.ts +6 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -2
- package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
- package/dist/types/modes/components/custom-editor.d.ts +39 -1
- package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/tool-execution.d.ts +26 -16
- package/dist/types/modes/components/transcript-container.d.ts +23 -2
- package/dist/types/modes/components/tree-selector.d.ts +1 -1
- package/dist/types/modes/components/usage-row.d.ts +3 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -2
- package/dist/types/modes/controllers/input-controller.d.ts +14 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/gradient-highlight.d.ts +9 -4
- package/dist/types/modes/image-references.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +27 -3
- package/dist/types/modes/magic-keywords.d.ts +13 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
- package/dist/types/modes/runtime-init.d.ts +4 -0
- package/dist/types/modes/theme/theme.d.ts +13 -2
- package/dist/types/modes/types.d.ts +8 -2
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
- package/dist/types/registry/agent-registry.d.ts +17 -0
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +14 -2
- package/dist/types/session/indexed-session-storage.d.ts +3 -4
- package/dist/types/session/session-context.d.ts +39 -0
- package/dist/types/session/session-entries.d.ts +159 -0
- package/dist/types/session/session-listing.d.ts +69 -0
- package/dist/types/session/session-loader.d.ts +16 -0
- package/dist/types/session/session-manager.d.ts +82 -474
- package/dist/types/session/session-migrations.d.ts +12 -0
- package/dist/types/session/session-paths.d.ts +25 -0
- package/dist/types/session/session-persistence.d.ts +8 -0
- package/dist/types/session/session-storage.d.ts +11 -12
- package/dist/types/session/snapcompact-inline.d.ts +12 -1
- package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
- package/dist/types/session/tool-choice-queue.d.ts +6 -6
- package/dist/types/stt/asr-client.d.ts +90 -0
- package/dist/types/stt/asr-protocol.d.ts +97 -0
- package/dist/types/stt/asr-worker.d.ts +2 -0
- package/dist/types/stt/downloader.d.ts +38 -0
- package/dist/types/stt/endpointer.d.ts +59 -0
- package/dist/types/stt/index.d.ts +5 -1
- package/dist/types/stt/models.d.ts +120 -0
- package/dist/types/stt/recorder.d.ts +17 -0
- package/dist/types/stt/stt-controller.d.ts +6 -0
- package/dist/types/stt/transcriber.d.ts +5 -7
- package/dist/types/stt/wav.d.ts +29 -0
- package/dist/types/system-prompt.d.ts +4 -0
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/task/index.d.ts +9 -1
- package/dist/types/task/types.d.ts +36 -0
- package/dist/types/tools/bash.d.ts +2 -2
- package/dist/types/tools/eval-render.d.ts +1 -1
- package/dist/types/tools/index.d.ts +11 -1
- package/dist/types/tools/irc.d.ts +1 -0
- package/dist/types/tools/learn.d.ts +51 -0
- package/dist/types/tools/manage-skill.d.ts +40 -0
- package/dist/types/tools/plan-mode-guard.d.ts +10 -0
- package/dist/types/tools/renderers.d.ts +7 -11
- package/dist/types/tools/ssh.d.ts +1 -1
- package/dist/types/tools/todo.d.ts +1 -1
- package/dist/types/tools/tts.d.ts +25 -0
- package/dist/types/tools/write.d.ts +1 -1
- package/dist/types/tts/downloader.d.ts +20 -0
- package/dist/types/tts/index.d.ts +8 -0
- package/dist/types/tts/models.d.ts +82 -0
- package/dist/types/tts/player.d.ts +32 -0
- package/dist/types/tts/runtime.d.ts +6 -0
- package/dist/types/tts/streaming-player.d.ts +41 -0
- package/dist/types/tts/tts-client.d.ts +93 -0
- package/dist/types/tts/tts-protocol.d.ts +95 -0
- package/dist/types/tts/tts-worker.d.ts +2 -0
- package/dist/types/tts/vocalizer.d.ts +41 -0
- package/dist/types/tts/wav.d.ts +8 -0
- package/dist/types/utils/tool-choice.d.ts +8 -0
- package/dist/types/utils/tools-manager.d.ts +2 -1
- package/dist/types/utils/tools-manager.test.d.ts +1 -0
- package/dist/types/web/scrapers/github.d.ts +1 -1
- package/package.json +15 -14
- package/src/async/job-manager.ts +49 -0
- package/src/autolearn/controller.ts +139 -0
- package/src/autolearn/managed-skills.ts +257 -0
- package/src/autoresearch/state.ts +1 -1
- package/src/autoresearch/types.ts +1 -1
- package/src/cli/args.ts +56 -2
- package/src/cli/session-picker.ts +2 -1
- package/src/cli/setup-cli.ts +148 -47
- package/src/cli/setup-model-picker.ts +43 -0
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +45 -13
- package/src/collab/host.ts +1 -1
- package/src/collab/protocol.ts +1 -1
- package/src/commands/say.ts +102 -0
- package/src/commands/setup.ts +1 -1
- package/src/commit/agentic/tools/analyze-file.ts +3 -0
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-discovery.ts +11 -5
- package/src/config/model-registry.ts +64 -9
- package/src/config/models-config-schema.ts +4 -1
- package/src/config/models-config.ts +2 -1
- package/src/config/settings-schema.ts +248 -32
- package/src/config/settings.ts +10 -0
- package/src/discovery/builtin.ts +23 -1
- package/src/discovery/claude-plugins.ts +44 -5
- package/src/discovery/helpers.ts +41 -1
- package/src/eval/__tests__/budget-bridge.test.ts +1 -1
- package/src/eval/js/shared/prelude.txt +69 -17
- package/src/export/html/index.ts +3 -6
- package/src/extensibility/extensions/model-api.ts +41 -0
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +52 -1
- package/src/extensibility/extensions/wrapper.ts +41 -5
- package/src/extensibility/hooks/index.ts +2 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
- package/src/extensibility/plugins/loader.ts +30 -19
- package/src/extensibility/plugins/manager.ts +221 -90
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skills.ts +96 -15
- package/src/goals/guided-setup.ts +133 -0
- package/src/goals/state.ts +1 -1
- package/src/hindsight/transcript.ts +1 -1
- package/src/index.ts +5 -0
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/internal-urls/history-protocol.ts +1 -1
- package/src/internal-urls/local-protocol.ts +29 -7
- package/src/main.ts +27 -7
- package/src/mcp/startup-events.ts +21 -0
- package/src/mcp/transports/stdio.ts +2 -1
- package/src/memories/index.ts +146 -11
- package/src/memory-backend/local-backend.ts +11 -5
- package/src/mnemopi/backend.ts +1 -0
- package/src/mnemopi/config.ts +26 -10
- package/src/modes/acp/acp-agent.ts +3 -5
- package/src/modes/components/agent-hub.ts +49 -4
- package/src/modes/components/assistant-message.ts +4 -37
- package/src/modes/components/compaction-summary-message.ts +125 -26
- package/src/modes/components/custom-editor.test.ts +96 -0
- package/src/modes/components/custom-editor.ts +164 -8
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/tool-execution.ts +82 -43
- package/src/modes/components/transcript-container.ts +70 -1
- package/src/modes/components/tree-selector.ts +1 -1
- package/src/modes/components/usage-row.ts +18 -0
- package/src/modes/components/user-message.ts +4 -2
- package/src/modes/controllers/command-controller.ts +14 -4
- package/src/modes/controllers/event-controller.ts +78 -11
- package/src/modes/controllers/extension-ui-controller.ts +6 -0
- package/src/modes/controllers/input-controller.ts +258 -27
- package/src/modes/controllers/selector-controller.ts +12 -2
- package/src/modes/gradient-highlight.ts +21 -9
- package/src/modes/image-references.ts +20 -0
- package/src/modes/interactive-mode.ts +286 -40
- package/src/modes/magic-keywords.ts +27 -5
- package/src/modes/rpc/rpc-mode.ts +146 -14
- package/src/modes/rpc/rpc-subagents.ts +2 -2
- package/src/modes/rpc/rpc-types.ts +8 -2
- package/src/modes/runtime-init.ts +28 -3
- package/src/modes/theme/theme.ts +98 -50
- package/src/modes/types.ts +6 -2
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +34 -6
- package/src/priority.json +5 -1
- package/src/prompts/agents/task.md +1 -0
- package/src/prompts/goals/guided-goal-interview.md +8 -0
- package/src/prompts/goals/guided-goal-system.md +12 -0
- package/src/prompts/memories/read-path.md +6 -0
- package/src/prompts/system/autolearn-guidance-learn.md +1 -0
- package/src/prompts/system/autolearn-guidance.md +7 -0
- package/src/prompts/system/autolearn-nudge.md +3 -0
- package/src/prompts/system/eager-task.md +7 -0
- package/src/prompts/system/eager-todo.md +11 -6
- package/src/prompts/system/subagent-system-prompt.md +4 -0
- package/src/prompts/system/system-prompt.md +10 -5
- package/src/prompts/system/title-marker-instruction.md +1 -0
- package/src/prompts/system/title-system-marker.md +16 -0
- package/src/prompts/tools/job.md +1 -0
- package/src/prompts/tools/learn.md +7 -0
- package/src/prompts/tools/manage-skill.md +9 -0
- package/src/prompts/tools/task.md +3 -0
- package/src/registry/agent-registry.ts +30 -0
- package/src/sdk.ts +88 -24
- package/src/secrets/obfuscator.ts +1 -1
- package/src/session/agent-session.ts +209 -87
- package/src/session/history-storage.ts +2 -2
- package/src/session/indexed-session-storage.ts +7 -17
- package/src/session/session-context.ts +352 -0
- package/src/session/session-entries.ts +194 -0
- package/src/session/session-listing.ts +588 -0
- package/src/session/session-loader.ts +106 -0
- package/src/session/session-manager.ts +933 -3145
- package/src/session/session-migrations.ts +78 -0
- package/src/session/session-paths.ts +193 -0
- package/src/session/session-persistence.ts +131 -0
- package/src/session/session-storage.ts +91 -50
- package/src/session/snapcompact-inline.ts +21 -1
- package/src/session/snapcompact-savings-journal.ts +113 -0
- package/src/session/tool-choice-queue.ts +23 -11
- package/src/slash-commands/builtin-registry.ts +25 -3
- package/src/stt/asr-client.ts +520 -0
- package/src/stt/asr-protocol.ts +65 -0
- package/src/stt/asr-worker.ts +790 -0
- package/src/stt/downloader.ts +107 -47
- package/src/stt/endpointer.ts +259 -0
- package/src/stt/index.ts +5 -1
- package/src/stt/models.ts +150 -0
- package/src/stt/recorder.ts +247 -60
- package/src/stt/stt-controller.ts +201 -22
- package/src/stt/transcriber.ts +37 -68
- package/src/stt/wav.ts +173 -0
- package/src/system-prompt.ts +8 -0
- package/src/task/agents.ts +1 -2
- package/src/task/executor.ts +49 -15
- package/src/task/index.ts +60 -6
- package/src/task/render.ts +83 -8
- package/src/task/types.ts +53 -0
- package/src/tools/ask.ts +8 -0
- package/src/tools/bash.ts +4 -3
- package/src/tools/eval-render.ts +4 -3
- package/src/tools/index.ts +40 -4
- package/src/tools/irc.ts +10 -2
- package/src/tools/job.ts +14 -2
- package/src/tools/learn.ts +144 -0
- package/src/tools/manage-skill.ts +104 -0
- package/src/tools/plan-mode-guard.ts +53 -19
- package/src/tools/renderers.ts +7 -11
- package/src/tools/ssh.ts +4 -3
- package/src/tools/todo.ts +1 -1
- package/src/tools/tts.ts +203 -92
- package/src/tools/write.ts +18 -2
- package/src/tts/downloader.ts +64 -0
- package/src/tts/index.ts +8 -0
- package/src/tts/models.ts +137 -0
- package/src/tts/player.ts +137 -0
- package/src/tts/runtime.ts +21 -0
- package/src/tts/streaming-player.ts +266 -0
- package/src/tts/tts-client.ts +647 -0
- package/src/tts/tts-protocol.ts +60 -0
- package/src/tts/tts-worker.ts +497 -0
- package/src/tts/vocalizer.ts +162 -0
- package/src/tts/wav.ts +58 -0
- package/src/utils/title-generator.ts +48 -5
- package/src/utils/tool-choice.ts +16 -0
- package/src/utils/tools-manager.test.ts +25 -0
- package/src/utils/tools-manager.ts +19 -1
- package/src/web/scrapers/github.ts +96 -0
- package/src/web/search/index.ts +13 -0
- package/src/web/search/providers/searxng.ts +13 -1
- package/dist/types/stt/setup.d.ts +0 -18
- package/src/stt/setup.ts +0 -52
- package/src/stt/transcribe.py +0 -70
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface PlayerCommand {
|
|
2
|
+
cmd: string;
|
|
3
|
+
args: string[];
|
|
4
|
+
}
|
|
5
|
+
/** Injection seam for {@link playerCommandsFor} — defaults to real PATH/tools lookups. */
|
|
6
|
+
export interface PlayerLookup {
|
|
7
|
+
which?: (bin: string) => string | null;
|
|
8
|
+
ffmpeg?: () => string | null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Build the ordered list of playback commands to try for `filePath` on the
|
|
12
|
+
* given platform. Pure + injectable so the selection logic is testable without
|
|
13
|
+
* spawning anything.
|
|
14
|
+
*
|
|
15
|
+
* - darwin: `afplay` (always present on macOS).
|
|
16
|
+
* - win32: PowerShell `Media.SoundPlayer.PlaySync()` (no extra deps).
|
|
17
|
+
* - linux/other POSIX: `paplay` (PulseAudio) → `aplay` (ALSA) → the bundled
|
|
18
|
+
* static `ffmpeg` (`-f pulse` then `-f alsa`). Empty result means nothing is
|
|
19
|
+
* available and the caller should surface an install hint.
|
|
20
|
+
*/
|
|
21
|
+
export declare function playerCommandsFor(platform: NodeJS.Platform, filePath: string, lookup?: PlayerLookup): PlayerCommand[];
|
|
22
|
+
export interface PlayAudioOptions {
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Play `filePath` through the speakers, trying each candidate command in order
|
|
27
|
+
* and returning on the first clean exit. Throws an actionable Error if no
|
|
28
|
+
* player exists or every candidate fails (with the collected stderr).
|
|
29
|
+
*/
|
|
30
|
+
export declare function playAudioFile(filePath: string, options?: PlayAudioOptions): Promise<void>;
|
|
31
|
+
/** Best-effort temp-file cleanup used by callers after playback. */
|
|
32
|
+
export declare function removeTempFile(filePath: string): Promise<void>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const KOKORO_PACKAGE = "kokoro-js";
|
|
2
|
+
export declare const KOKORO_VERSION = "1.2.1";
|
|
3
|
+
export declare const ONNXRUNTIME_NODE_PACKAGE = "onnxruntime-node";
|
|
4
|
+
export declare const ONNXRUNTIME_NODE_VERSION = "1.26.0";
|
|
5
|
+
export declare function getTtsRuntimeDir(): string;
|
|
6
|
+
export declare function isTtsRuntimeCached(): Promise<boolean>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type PlayerCommand } from "./player";
|
|
2
|
+
/** Output gain applied while ducked (the user is speaking over the assistant). */
|
|
3
|
+
export declare const DUCK_GAIN = 0.25;
|
|
4
|
+
/** Injection seam for {@link streamingPlayerCommandsFor} — defaults to real PATH/tools lookups. */
|
|
5
|
+
export interface StreamingPlayerLookup {
|
|
6
|
+
which?: (bin: string) => string | null;
|
|
7
|
+
ffmpeg?: () => string | null;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Ordered candidate commands for a persistent raw-PCM player on `platform`: each
|
|
11
|
+
* reads 32-bit-float little-endian mono PCM at `sampleRate` from stdin (`pipe:0`)
|
|
12
|
+
* and plays it to the default output device. An empty list means no streaming
|
|
13
|
+
* backend is available and the caller should fall back to per-file playback.
|
|
14
|
+
*
|
|
15
|
+
* - darwin: none; `afplay` is file-only, so macOS uses the interruptible
|
|
16
|
+
* per-file fallback.
|
|
17
|
+
* - linux/other POSIX: `ffmpeg` (`-f pulse` then `-f alsa`) → `paplay`/`aplay`
|
|
18
|
+
* raw fallbacks.
|
|
19
|
+
* - win32: none (PowerShell `SoundPlayer` is file-only).
|
|
20
|
+
*/
|
|
21
|
+
export declare function streamingPlayerCommandsFor(platform: NodeJS.Platform, sampleRate: number, lookup?: StreamingPlayerLookup): PlayerCommand[];
|
|
22
|
+
/**
|
|
23
|
+
* Single-session gapless player. Lifecycle: {@link start} once, {@link write}
|
|
24
|
+
* chunks in order, then {@link end} to drain or {@link stop} to abort. Not
|
|
25
|
+
* reusable after stop/end — create a new instance per utterance.
|
|
26
|
+
*/
|
|
27
|
+
export declare class StreamingAudioPlayer {
|
|
28
|
+
#private;
|
|
29
|
+
/** Pick a backend and begin draining. Idempotent; the first call's rate wins. */
|
|
30
|
+
start(sampleRate: number): void;
|
|
31
|
+
/** Queue a mono float32 PCM chunk for playback in arrival order. */
|
|
32
|
+
write(pcm: Float32Array): void;
|
|
33
|
+
/** Scale subsequent output (1 = normal, <1 = ducked). Applies within {@link LEAD_SECONDS}. */
|
|
34
|
+
setGain(gain: number): void;
|
|
35
|
+
/** Close the input; resolves once all queued audio has finished playing. */
|
|
36
|
+
end(): Promise<void>;
|
|
37
|
+
/** Stop immediately: kill the player, drop everything still queued. */
|
|
38
|
+
stop(): void;
|
|
39
|
+
}
|
|
40
|
+
/** Factory the vocalizer calls; a function so tests can stub it without spawning a player. */
|
|
41
|
+
export declare function createStreamingPlayer(): StreamingAudioPlayer;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Subprocess } from "bun";
|
|
2
|
+
import type { TtsProgressEvent, TtsWorkerInbound, TtsWorkerOutbound } from "./tts-protocol";
|
|
3
|
+
/** Decoded PCM returned by a local synthesis request. */
|
|
4
|
+
export interface TtsAudio {
|
|
5
|
+
pcm: Float32Array;
|
|
6
|
+
sampleRate: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Abstraction over the TTS subprocess. The runtime implementation is a Bun child
|
|
10
|
+
* process so `onnxruntime-node`'s NAPI finalizer never runs inside the main agent
|
|
11
|
+
* address space — that destructor segfaults Bun during shutdown (issue #1606).
|
|
12
|
+
*/
|
|
13
|
+
interface WorkerHandle {
|
|
14
|
+
send(message: TtsWorkerInbound): void;
|
|
15
|
+
onMessage(handler: (message: TtsWorkerOutbound) => void): () => void;
|
|
16
|
+
onError(handler: (error: Error) => void): () => void;
|
|
17
|
+
/** Re-reference the subprocess so a pending request keeps the parent event loop alive. */
|
|
18
|
+
ref(): void;
|
|
19
|
+
/** Drop the reference once the worker is idle so it never blocks process exit. */
|
|
20
|
+
unref(): void;
|
|
21
|
+
terminate(): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export interface TtsSynthesizeOptions {
|
|
24
|
+
voice?: string;
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
}
|
|
27
|
+
export interface TtsDownloadOptions {
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
onProgress?: (event: TtsProgressEvent) => void;
|
|
30
|
+
}
|
|
31
|
+
export interface TtsStreamOptions {
|
|
32
|
+
voice?: string;
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
}
|
|
35
|
+
/** One synthesized sentence of a streaming session, in emission order. */
|
|
36
|
+
export interface TtsAudioChunk {
|
|
37
|
+
index: number;
|
|
38
|
+
text: string;
|
|
39
|
+
pcm: Float32Array;
|
|
40
|
+
sampleRate: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A live streaming-synthesis session. Feed text incrementally with {@link push}
|
|
44
|
+
* and close the input with {@link end}; `chunks` yields each synthesized
|
|
45
|
+
* sentence's audio as soon as it is ready, then completes once the worker
|
|
46
|
+
* finishes draining the closed input.
|
|
47
|
+
*/
|
|
48
|
+
export interface TtsStreamHandle {
|
|
49
|
+
push(text: string): void;
|
|
50
|
+
end(): void;
|
|
51
|
+
chunks: AsyncIterableIterator<TtsAudioChunk>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Hidden subcommand on the main CLI that boots the TTS worker in the spawned
|
|
55
|
+
* subprocess. Kept in sync with the dispatch in `cli.ts` (Main-owned).
|
|
56
|
+
*/
|
|
57
|
+
export declare const TTS_WORKER_ARG = "__omp_tts_worker";
|
|
58
|
+
interface SpawnedSubprocess {
|
|
59
|
+
proc: Subprocess<"ignore", "ignore", "ignore">;
|
|
60
|
+
inbound: Set<(message: TtsWorkerOutbound) => void>;
|
|
61
|
+
errors: Set<(error: Error) => void>;
|
|
62
|
+
/** Flipped to `true` right before the deliberate SIGKILL so `onExit` can tell it apart from a crash. */
|
|
63
|
+
intentionalExit: {
|
|
64
|
+
value: boolean;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Spawn the TTS worker as a subprocess. Exported for tests and the smoke probe;
|
|
69
|
+
* production callers go through {@link spawnTtsWorker}.
|
|
70
|
+
*/
|
|
71
|
+
export declare function createTtsSubprocess(): SpawnedSubprocess;
|
|
72
|
+
export declare class TtsClient {
|
|
73
|
+
#private;
|
|
74
|
+
constructor(spawnWorker?: () => WorkerHandle);
|
|
75
|
+
onProgress(listener: (event: TtsProgressEvent) => void): () => void;
|
|
76
|
+
synthesize(modelKey: string, text: string, options?: TtsSynthesizeOptions): Promise<TtsAudio | null>;
|
|
77
|
+
/**
|
|
78
|
+
* Open a streaming-synthesis session. Text is fed incrementally through the
|
|
79
|
+
* returned handle's `push`/`end`; audio is emitted one synthesized sentence at
|
|
80
|
+
* a time via `chunks`, so playback can begin before the full text is known.
|
|
81
|
+
* Returns an inert handle (immediately-ended `chunks`) for unknown models or
|
|
82
|
+
* an already-aborted signal, and fails the iterator if the worker cannot spawn.
|
|
83
|
+
*/
|
|
84
|
+
synthesizeStream(modelKey: string, options?: TtsStreamOptions): TtsStreamHandle;
|
|
85
|
+
downloadModel(modelKey: string, options?: TtsDownloadOptions): Promise<boolean>;
|
|
86
|
+
terminate(): Promise<void>;
|
|
87
|
+
}
|
|
88
|
+
export declare const ttsClient: TtsClient;
|
|
89
|
+
export declare function shutdownTtsClient(): Promise<void>;
|
|
90
|
+
export declare function smokeTestTtsWorker({ timeoutMs, }?: {
|
|
91
|
+
timeoutMs?: number;
|
|
92
|
+
}): Promise<void>;
|
|
93
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { TtsLocalModelKey } from "./models";
|
|
2
|
+
export type TtsProgressStatus = "initiate" | "download" | "progress" | "progress_total" | "done" | "ready" | "error";
|
|
3
|
+
export interface TtsProgressFileState {
|
|
4
|
+
loaded: number;
|
|
5
|
+
total: number;
|
|
6
|
+
}
|
|
7
|
+
export interface TtsProgressEvent {
|
|
8
|
+
modelKey: TtsLocalModelKey;
|
|
9
|
+
status: TtsProgressStatus;
|
|
10
|
+
name?: string;
|
|
11
|
+
file?: string;
|
|
12
|
+
progress?: number;
|
|
13
|
+
loaded?: number;
|
|
14
|
+
total?: number;
|
|
15
|
+
files?: Record<string, TtsProgressFileState>;
|
|
16
|
+
task?: string;
|
|
17
|
+
model?: string;
|
|
18
|
+
}
|
|
19
|
+
export type TtsWorkerInbound = {
|
|
20
|
+
type: "ping";
|
|
21
|
+
id: string;
|
|
22
|
+
} | {
|
|
23
|
+
type: "synthesize";
|
|
24
|
+
id: string;
|
|
25
|
+
modelKey: TtsLocalModelKey;
|
|
26
|
+
text: string;
|
|
27
|
+
voice?: string;
|
|
28
|
+
} | {
|
|
29
|
+
type: "download";
|
|
30
|
+
id: string;
|
|
31
|
+
modelKey: TtsLocalModelKey;
|
|
32
|
+
} | {
|
|
33
|
+
type: "stream-start";
|
|
34
|
+
id: string;
|
|
35
|
+
modelKey: TtsLocalModelKey;
|
|
36
|
+
voice?: string;
|
|
37
|
+
} | {
|
|
38
|
+
type: "stream-push";
|
|
39
|
+
id: string;
|
|
40
|
+
text: string;
|
|
41
|
+
} | {
|
|
42
|
+
type: "stream-end";
|
|
43
|
+
id: string;
|
|
44
|
+
} | {
|
|
45
|
+
type: "stream-cancel";
|
|
46
|
+
id: string;
|
|
47
|
+
};
|
|
48
|
+
export type TtsWorkerOutbound = {
|
|
49
|
+
type: "pong";
|
|
50
|
+
id: string;
|
|
51
|
+
} | {
|
|
52
|
+
type: "audio";
|
|
53
|
+
id: string;
|
|
54
|
+
pcm: Float32Array;
|
|
55
|
+
sampleRate: number;
|
|
56
|
+
} | {
|
|
57
|
+
type: "downloaded";
|
|
58
|
+
id: string;
|
|
59
|
+
} | {
|
|
60
|
+
type: "error";
|
|
61
|
+
id: string;
|
|
62
|
+
error: string;
|
|
63
|
+
} | {
|
|
64
|
+
type: "progress";
|
|
65
|
+
id: string;
|
|
66
|
+
event: TtsProgressEvent;
|
|
67
|
+
} | {
|
|
68
|
+
type: "log";
|
|
69
|
+
level: "debug" | "warn" | "error";
|
|
70
|
+
msg: string;
|
|
71
|
+
meta?: Record<string, unknown>;
|
|
72
|
+
} | {
|
|
73
|
+
type: "audio-chunk";
|
|
74
|
+
id: string;
|
|
75
|
+
index: number;
|
|
76
|
+
text: string;
|
|
77
|
+
pcm: Float32Array;
|
|
78
|
+
sampleRate: number;
|
|
79
|
+
} | {
|
|
80
|
+
type: "stream-done";
|
|
81
|
+
id: string;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Wire transport between the parent (`TtsClient`) and the local TTS subprocess.
|
|
85
|
+
* The parent owns the subprocess lifecycle (graceful work, hard SIGKILL on
|
|
86
|
+
* shutdown); the protocol carries no explicit close handshake — once the parent
|
|
87
|
+
* decides to terminate, it signals the OS to reap the child so
|
|
88
|
+
* `onnxruntime-node`'s NAPI finalizer never runs in the main agent address
|
|
89
|
+
* space (it segfaults Bun on shutdown — issue #1606). See `tts-client.ts` for
|
|
90
|
+
* the spawn/kill glue.
|
|
91
|
+
*/
|
|
92
|
+
export interface TtsTransport {
|
|
93
|
+
send(message: TtsWorkerOutbound): void;
|
|
94
|
+
onMessage(handler: (message: TtsWorkerInbound) => void): () => void;
|
|
95
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface VocalizerPlayer {
|
|
2
|
+
start(sampleRate: number): void;
|
|
3
|
+
write(pcm: Float32Array): void;
|
|
4
|
+
setGain(gain: number): void;
|
|
5
|
+
end(): Promise<void>;
|
|
6
|
+
stop(): void;
|
|
7
|
+
}
|
|
8
|
+
export declare class Vocalizer {
|
|
9
|
+
#private;
|
|
10
|
+
constructor(createPlayer?: () => VocalizerPlayer);
|
|
11
|
+
/**
|
|
12
|
+
* Stream a delta of assistant text into the engine. No-op when vocalization
|
|
13
|
+
* is disabled. The engine buffers the running text and emits audio for each
|
|
14
|
+
* complete sentence; the trailing partial is flushed by {@link flush}.
|
|
15
|
+
*/
|
|
16
|
+
pushDelta(text: string): void;
|
|
17
|
+
/**
|
|
18
|
+
* Close the current input stream (call at message/turn end). The engine
|
|
19
|
+
* flushes its trailing partial as a final chunk; the player keeps draining
|
|
20
|
+
* queued audio until it completes.
|
|
21
|
+
*/
|
|
22
|
+
flush(): void;
|
|
23
|
+
/**
|
|
24
|
+
* Speak a complete piece of text in one shot (ask questions, yield-mode final
|
|
25
|
+
* message): stream it in and immediately close the input. No-op when disabled.
|
|
26
|
+
*/
|
|
27
|
+
speak(text: string): void;
|
|
28
|
+
/**
|
|
29
|
+
* Interrupt and drop the current session, killing in-flight playback and
|
|
30
|
+
* synthesis (new turn / user message / Esc interrupt). Audio stops at once.
|
|
31
|
+
*/
|
|
32
|
+
clear(): void;
|
|
33
|
+
/** Lower the volume while the user is speaking (push-to-talk), so speech doesn't drown them out. */
|
|
34
|
+
duck(): void;
|
|
35
|
+
/** Restore full volume once the user stops speaking. */
|
|
36
|
+
unduck(): void;
|
|
37
|
+
/** Resolve once the playback chain has drained (tests / shutdown). */
|
|
38
|
+
idle(): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
/** Process-level vocalizer shared by the event controller and the ask tool. */
|
|
41
|
+
export declare const vocalizer: Vocalizer;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assemble a mono PCM16 WAV byte buffer from Float32 PCM samples (the shape
|
|
3
|
+
* transformers.js `RawAudio` emits: normalized [-1, 1] amplitudes plus a sample
|
|
4
|
+
* rate). No external encoder is involved — we write a canonical 44-byte RIFF/
|
|
5
|
+
* WAVE header followed by little-endian signed 16-bit samples. Samples are
|
|
6
|
+
* clamped before quantization so out-of-range float values do not wrap.
|
|
7
|
+
*/
|
|
8
|
+
export declare function encodeWav(samples: Float32Array, sampleRate: number): Uint8Array;
|
|
@@ -5,3 +5,11 @@ import type { Api, Model, ToolChoice } from "@oh-my-pi/pi-ai";
|
|
|
5
5
|
* narrowing their request tool list before transport.
|
|
6
6
|
*/
|
|
7
7
|
export declare function buildNamedToolChoice(toolName: string, model?: Model<Api>): ToolChoice | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Whether the given tool choice can be satisfied by the active tool set for the
|
|
10
|
+
* upcoming turn. Non-named choices (`"none"`, `"required"`, etc.) do not name a
|
|
11
|
+
* specific tool and are therefore always active.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isToolChoiceActive(toolChoice: ToolChoice | undefined, tools: readonly {
|
|
14
|
+
name: string;
|
|
15
|
+
}[]): boolean;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export
|
|
1
|
+
export declare function ffmpegAssetName(_version: string, plat: string, architecture: string): string | null;
|
|
2
|
+
export type ToolName = "sd" | "sg" | "yt-dlp" | "trafilatura" | "ffmpeg";
|
|
2
3
|
export declare function getToolPath(tool: ToolName): string | null;
|
|
3
4
|
type EnsureToolOptions = {
|
|
4
5
|
signal?: AbortSignal;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SpecialHandler } from "./types";
|
|
2
2
|
interface GitHubUrl {
|
|
3
|
-
type: "blob" | "tree" | "repo" | "issue" | "issues" | "pull" | "pulls" | "discussion" | "discussions" | "actions-run" | "actions-job" | "other";
|
|
3
|
+
type: "blob" | "tree" | "repo" | "commit" | "issue" | "issues" | "pull" | "pulls" | "discussion" | "discussions" | "actions-run" | "actions-job" | "other";
|
|
4
4
|
owner: string;
|
|
5
5
|
repo: string;
|
|
6
6
|
ref?: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.13.0",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"check": "biome check . && bun run check:types",
|
|
36
36
|
"check:types": "tsgo -p tsconfig.json --noEmit",
|
|
37
37
|
"lint": "biome lint .",
|
|
38
|
-
"test": "bun test --parallel",
|
|
38
|
+
"test": "bun test --parallel=2",
|
|
39
39
|
"fix": "biome check --write --unsafe . && bun run format-prompts && bun run generate-docs-index",
|
|
40
40
|
"fmt": "biome format --write . && bun run format-prompts",
|
|
41
41
|
"format-prompts": "bun scripts/format-prompts.ts",
|
|
@@ -47,17 +47,17 @@
|
|
|
47
47
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
48
48
|
"@babel/parser": "^7.29.7",
|
|
49
49
|
"@mozilla/readability": "^0.6.0",
|
|
50
|
-
"@oh-my-pi/hashline": "15.
|
|
51
|
-
"@oh-my-pi/omp-stats": "15.
|
|
52
|
-
"@oh-my-pi/pi-agent-core": "15.
|
|
53
|
-
"@oh-my-pi/pi-ai": "15.
|
|
54
|
-
"@oh-my-pi/pi-catalog": "15.
|
|
55
|
-
"@oh-my-pi/pi-mnemopi": "15.
|
|
56
|
-
"@oh-my-pi/pi-natives": "15.
|
|
57
|
-
"@oh-my-pi/pi-tui": "15.
|
|
58
|
-
"@oh-my-pi/pi-utils": "15.
|
|
59
|
-
"@oh-my-pi/pi-wire": "15.
|
|
60
|
-
"@oh-my-pi/snapcompact": "15.
|
|
50
|
+
"@oh-my-pi/hashline": "15.13.0",
|
|
51
|
+
"@oh-my-pi/omp-stats": "15.13.0",
|
|
52
|
+
"@oh-my-pi/pi-agent-core": "15.13.0",
|
|
53
|
+
"@oh-my-pi/pi-ai": "15.13.0",
|
|
54
|
+
"@oh-my-pi/pi-catalog": "15.13.0",
|
|
55
|
+
"@oh-my-pi/pi-mnemopi": "15.13.0",
|
|
56
|
+
"@oh-my-pi/pi-natives": "15.13.0",
|
|
57
|
+
"@oh-my-pi/pi-tui": "15.13.0",
|
|
58
|
+
"@oh-my-pi/pi-utils": "15.13.0",
|
|
59
|
+
"@oh-my-pi/pi-wire": "15.13.0",
|
|
60
|
+
"@oh-my-pi/snapcompact": "15.13.0",
|
|
61
61
|
"@opentelemetry/api": "^1.9.1",
|
|
62
62
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
63
63
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
|
@@ -80,7 +80,8 @@
|
|
|
80
80
|
"zod": "^4"
|
|
81
81
|
},
|
|
82
82
|
"optionalDependencies": {
|
|
83
|
-
"@huggingface/transformers": "^4.2.0"
|
|
83
|
+
"@huggingface/transformers": "^4.2.0",
|
|
84
|
+
"sherpa-onnx-node": "1.13.2"
|
|
84
85
|
},
|
|
85
86
|
"devDependencies": {
|
|
86
87
|
"@types/bun": "^1.3.14"
|
package/src/async/job-manager.ts
CHANGED
|
@@ -6,6 +6,27 @@ const DELIVERY_RETRY_JITTER_MS = 200;
|
|
|
6
6
|
const DEFAULT_RETENTION_MS = 5 * 60 * 1000;
|
|
7
7
|
const DEFAULT_MAX_RUNNING_JOBS = 15;
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Adaptive ("smart") `job` poll-wait ladder (ms). A tight poll loop climbs
|
|
11
|
+
* these rungs so each immediate re-poll backs off and stops spending turns on
|
|
12
|
+
* "still running" frames; the floor (first rung) is the shortest wait and the
|
|
13
|
+
* top rung is the longest a smart poll will ever block. Only used when
|
|
14
|
+
* `async.pollWaitDuration` is set to `smart`; fixed durations wait verbatim.
|
|
15
|
+
*/
|
|
16
|
+
const POLL_WAIT_LADDER_MS = [5_000, 10_000, 30_000, 60_000, 300_000] as const;
|
|
17
|
+
/**
|
|
18
|
+
* Going at least this long between poll calls means the agent stepped out of
|
|
19
|
+
* the poll loop to do real work — the next poll drops back to the ladder floor.
|
|
20
|
+
*/
|
|
21
|
+
const POLL_ESCALATION_RESET_MS = 60_000;
|
|
22
|
+
|
|
23
|
+
interface PollEscalationState {
|
|
24
|
+
/** Index into POLL_WAIT_LADDER_MS used for the most recent poll wait. */
|
|
25
|
+
level: number;
|
|
26
|
+
/** Timestamp (ms) when the most recent poll wait returned. */
|
|
27
|
+
lastPollEndAt: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
9
30
|
export interface AsyncJob {
|
|
10
31
|
id: string;
|
|
11
32
|
type: "bash" | "task";
|
|
@@ -96,6 +117,7 @@ export class AsyncJobManager {
|
|
|
96
117
|
readonly #suppressedDeliveries = new Set<string>();
|
|
97
118
|
readonly #watchedJobs = new Set<string>();
|
|
98
119
|
readonly #evictionTimers = new Map<string, NodeJS.Timeout>();
|
|
120
|
+
readonly #pollEscalation = new Map<string | undefined, PollEscalationState>();
|
|
99
121
|
readonly #onJobComplete: AsyncJobManagerOptions["onJobComplete"];
|
|
100
122
|
readonly #maxRunningJobs: number;
|
|
101
123
|
readonly #retentionMs: number;
|
|
@@ -295,6 +317,32 @@ export class AsyncJobManager {
|
|
|
295
317
|
return removed;
|
|
296
318
|
}
|
|
297
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Compute the next adaptive ("smart") wait (ms) for a blocking `job` poll by
|
|
322
|
+
* the given owner. Consecutive polls — those starting within
|
|
323
|
+
* POLL_ESCALATION_RESET_MS of the previous poll returning — climb
|
|
324
|
+
* POLL_WAIT_LADDER_MS so a tight wait loop backs off; a longer gap means the
|
|
325
|
+
* agent left to do real work, so the wait resets to the floor. Pair each call
|
|
326
|
+
* with `recordPollWaitEnd()` once the wait returns.
|
|
327
|
+
*/
|
|
328
|
+
nextPollWaitMs(ownerId: string | undefined, now: number = Date.now()): number {
|
|
329
|
+
const prev = this.#pollEscalation.get(ownerId);
|
|
330
|
+
const reset = !prev || now - prev.lastPollEndAt >= POLL_ESCALATION_RESET_MS;
|
|
331
|
+
const level = reset ? 0 : Math.min(prev.level + 1, POLL_WAIT_LADDER_MS.length - 1);
|
|
332
|
+
this.#pollEscalation.set(ownerId, { level, lastPollEndAt: prev?.lastPollEndAt ?? now });
|
|
333
|
+
return POLL_WAIT_LADDER_MS[level];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Mark a blocking poll wait as finished so the idle-reset window is measured
|
|
338
|
+
* from now. Polling again before POLL_ESCALATION_RESET_MS elapses keeps
|
|
339
|
+
* climbing the ladder; waiting longer resets it to the floor.
|
|
340
|
+
*/
|
|
341
|
+
recordPollWaitEnd(ownerId: string | undefined, now: number = Date.now()): void {
|
|
342
|
+
const prev = this.#pollEscalation.get(ownerId);
|
|
343
|
+
this.#pollEscalation.set(ownerId, { level: prev?.level ?? 0, lastPollEndAt: now });
|
|
344
|
+
}
|
|
345
|
+
|
|
298
346
|
acknowledgeDeliveries(jobIds: string[]): number {
|
|
299
347
|
const uniqueJobIds = Array.from(new Set(jobIds.map(id => id.trim()).filter(id => id.length > 0)));
|
|
300
348
|
if (uniqueJobIds.length === 0) return 0;
|
|
@@ -405,6 +453,7 @@ export class AsyncJobManager {
|
|
|
405
453
|
this.#inFlightDeliveries.length = 0;
|
|
406
454
|
this.#suppressedDeliveries.clear();
|
|
407
455
|
this.#watchedJobs.clear();
|
|
456
|
+
this.#pollEscalation.clear();
|
|
408
457
|
return drained;
|
|
409
458
|
}
|
|
410
459
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-learn session controller (experimental).
|
|
3
|
+
*
|
|
4
|
+
* Subscribes to the session event stream and, after a substantive turn,
|
|
5
|
+
* nudges the agent to capture reusable lessons. Default posture is passive
|
|
6
|
+
* (a hidden reminder rides the next real turn); with `autolearn.autoContinue`
|
|
7
|
+
* it auto-runs exactly one synthetic capture turn at stop.
|
|
8
|
+
*
|
|
9
|
+
* Installed once per top-level session (taskDepth 0). The subscription lives
|
|
10
|
+
* for the session's lifetime — `newSession` resets the session in place
|
|
11
|
+
* without re-running startup — so the controller needs no disposal.
|
|
12
|
+
*/
|
|
13
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
14
|
+
import type { Settings } from "../config/settings";
|
|
15
|
+
import autolearnGuidance from "../prompts/system/autolearn-guidance.md" with { type: "text" };
|
|
16
|
+
import autolearnGuidanceLearn from "../prompts/system/autolearn-guidance-learn.md" with { type: "text" };
|
|
17
|
+
import autolearnNudge from "../prompts/system/autolearn-nudge.md" with { type: "text" };
|
|
18
|
+
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
19
|
+
|
|
20
|
+
const AUTOLEARN_NUDGE = autolearnNudge.trim();
|
|
21
|
+
const DEFAULT_MIN_TOOL_CALLS = 5;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build the standing auto-learn guidance for the system prompt from the tools
|
|
25
|
+
* actually present in the active set, or null when `manage_skill` is absent.
|
|
26
|
+
*
|
|
27
|
+
* Driven by tool presence rather than live settings: the `learn`/`manage_skill`
|
|
28
|
+
* registry is built ONCE at session start (and only for top-level sessions), so
|
|
29
|
+
* keying the guidance on `autolearn.enabled` would let a mid-session enable — or
|
|
30
|
+
* a subagent that filtered the tools out — inject guidance pointing at tools the
|
|
31
|
+
* session never built. The `learn` addendum is included only when the `learn`
|
|
32
|
+
* tool is present (it requires a memory backend).
|
|
33
|
+
*/
|
|
34
|
+
export function buildAutoLearnInstructions(available: { manageSkill: boolean; learn: boolean }): string | null {
|
|
35
|
+
if (!available.manageSkill) return null;
|
|
36
|
+
const parts = [autolearnGuidance.trim()];
|
|
37
|
+
if (available.learn) parts.push(autolearnGuidanceLearn.trim());
|
|
38
|
+
return parts.join("\n\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AutoLearnControllerOptions {
|
|
42
|
+
session: AgentSession;
|
|
43
|
+
settings: Settings;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class AutoLearnController {
|
|
47
|
+
readonly #session: AgentSession;
|
|
48
|
+
readonly #settings: Settings;
|
|
49
|
+
#toolCalls = 0;
|
|
50
|
+
/**
|
|
51
|
+
* Whether the in-flight turn BEGAN while goal mode was active. Captured at
|
|
52
|
+
* agent_start because a `goal` tool can complete or drop the goal mid-turn,
|
|
53
|
+
* clearing the live flag before agent_end — so the end-of-turn state alone
|
|
54
|
+
* would let a goal-continuation turn slip through and get nudged.
|
|
55
|
+
*/
|
|
56
|
+
#turnStartedInGoalMode = false;
|
|
57
|
+
/** Swallow the agent_end produced by an auto-run capture turn so it cannot re-trigger. */
|
|
58
|
+
#suppressNext = false;
|
|
59
|
+
|
|
60
|
+
constructor(options: AutoLearnControllerOptions) {
|
|
61
|
+
this.#session = options.session;
|
|
62
|
+
this.#settings = options.settings;
|
|
63
|
+
// The listener closure captures `this`, so the session's listener array
|
|
64
|
+
// keeps the controller alive — no stored unsubscribe needed.
|
|
65
|
+
this.#session.subscribe(event => this.#onEvent(event));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#onEvent(event: AgentSessionEvent): void {
|
|
69
|
+
if (event.type === "agent_start") {
|
|
70
|
+
// Capture goal-mode state at the turn boundary, before any tool runs.
|
|
71
|
+
this.#turnStartedInGoalMode = this.#session.getGoalModeState()?.enabled === true;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (event.type === "tool_execution_end") {
|
|
75
|
+
this.#toolCalls++;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (event.type === "agent_end") {
|
|
79
|
+
this.#onAgentEnd();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#onAgentEnd(): void {
|
|
84
|
+
// Snapshot and reset every turn: the counter describes only the
|
|
85
|
+
// just-finished turn, so below-threshold, disabled, and plan-mode stops
|
|
86
|
+
// must not let tool calls accumulate into a later turn.
|
|
87
|
+
const toolCalls = this.#toolCalls;
|
|
88
|
+
this.#toolCalls = 0;
|
|
89
|
+
// Snapshot the turn-start goal flag alongside the counter so a turn that
|
|
90
|
+
// observed no agent_start can never inherit a stale value.
|
|
91
|
+
const startedInGoalMode = this.#turnStartedInGoalMode;
|
|
92
|
+
this.#turnStartedInGoalMode = false;
|
|
93
|
+
|
|
94
|
+
if (this.#suppressNext) {
|
|
95
|
+
this.#suppressNext = false;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// Honor a live opt-out: the subscription outlives the setting, so re-check
|
|
99
|
+
// the current flag rather than trusting install-time state.
|
|
100
|
+
if (!this.#settings.get("autolearn.enabled")) return;
|
|
101
|
+
const minToolCalls = this.#settings.get("autolearn.minToolCalls") ?? DEFAULT_MIN_TOOL_CALLS;
|
|
102
|
+
if (toolCalls < minToolCalls) return;
|
|
103
|
+
// Never interrupt plan-mode review.
|
|
104
|
+
if (this.#session.getPlanModeState()?.enabled) return;
|
|
105
|
+
// Never divert a goal loop. Skip when the turn STARTED in goal mode — a
|
|
106
|
+
// `goal` tool may have completed/dropped the goal before this stop — or is
|
|
107
|
+
// still in it: a passive nudge would ride the goal continuation, and
|
|
108
|
+
// auto-continue would compete with it.
|
|
109
|
+
if (startedInGoalMode || this.#session.getGoalModeState()?.enabled) return;
|
|
110
|
+
|
|
111
|
+
// Auto-run a capture turn only when explicitly enabled; otherwise the
|
|
112
|
+
// hidden reminder rides the next real turn passively.
|
|
113
|
+
const autoContinue = this.#settings.get("autolearn.autoContinue") === true;
|
|
114
|
+
// Arm suppression synchronously: the synthetic capture turn's agent_end
|
|
115
|
+
// fires inside sendCustomMessage (before it resolves), so the flag must be
|
|
116
|
+
// set before then. Disarm when no turn actually started — a deferred/queued
|
|
117
|
+
// dispatch or a failed send produces no agent_end, and a latched flag would
|
|
118
|
+
// otherwise swallow the next real stop.
|
|
119
|
+
if (autoContinue) this.#suppressNext = true;
|
|
120
|
+
|
|
121
|
+
this.#session
|
|
122
|
+
.sendCustomMessage(
|
|
123
|
+
{
|
|
124
|
+
customType: "autolearn-nudge",
|
|
125
|
+
content: AUTOLEARN_NUDGE,
|
|
126
|
+
display: false,
|
|
127
|
+
attribution: "user",
|
|
128
|
+
},
|
|
129
|
+
{ deliverAs: "nextTurn", triggerTurn: autoContinue },
|
|
130
|
+
)
|
|
131
|
+
.then(started => {
|
|
132
|
+
if (!started) this.#suppressNext = false;
|
|
133
|
+
})
|
|
134
|
+
.catch(err => {
|
|
135
|
+
this.#suppressNext = false;
|
|
136
|
+
logger.warn("auto-learn nudge delivery failed", { err });
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|