@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
package/src/stt/transcriber.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { sttClient } from "./asr-client";
|
|
3
|
+
import { resolveSttModelSpec } from "./models";
|
|
4
|
+
import { decodeWavToMono16k } from "./wav";
|
|
3
5
|
|
|
4
6
|
export interface TranscribeOptions {
|
|
5
7
|
modelName?: string;
|
|
@@ -10,82 +12,49 @@ export interface TranscribeOptions {
|
|
|
10
12
|
const TRANSCRIBE_TIMEOUT_MS = 120_000;
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
|
-
*
|
|
14
|
-
*/
|
|
15
|
-
export function resolvePython(): string | null {
|
|
16
|
-
for (const cmd of ["python", "py", "python3"]) {
|
|
17
|
-
if ($which(cmd)) return cmd;
|
|
18
|
-
}
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Transcribe a WAV file using Python openai-whisper.
|
|
15
|
+
* Transcribe a WAV file using the local ONNX Whisper worker.
|
|
24
16
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
17
|
+
* Decodes the WAV to a 16 kHz mono Float32Array in-process (no Python, no
|
|
18
|
+
* ffmpeg) and routes it to the warm speech worker, which keeps the model loaded
|
|
19
|
+
* across calls. Honors `options.signal` (abort) and applies an internal timeout
|
|
20
|
+
* with the same semantics as the previous Python path.
|
|
27
21
|
*/
|
|
28
22
|
export async function transcribe(audioPath: string, options?: TranscribeOptions): Promise<string> {
|
|
29
23
|
const audioFile = Bun.file(audioPath);
|
|
30
24
|
if (audioFile.size < 100) {
|
|
31
25
|
throw new Error(`Audio file is empty or too small (${audioFile.size} bytes). Check microphone.`);
|
|
32
26
|
}
|
|
33
|
-
|
|
34
|
-
const pythonCmd = resolvePython();
|
|
35
|
-
if (!pythonCmd) {
|
|
36
|
-
throw new Error("Python not found. Install Python 3.8+ from https://python.org");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const modelName = options?.modelName ?? "base.en";
|
|
40
|
-
const language = options?.language ?? "en";
|
|
41
|
-
|
|
42
|
-
logger.debug("Transcribing with Python whisper", { pythonCmd, audioPath, modelName, language });
|
|
43
|
-
|
|
44
|
-
const proc = Bun.spawn([pythonCmd, "-c", transcribeScript, audioPath, modelName, language], {
|
|
45
|
-
stdout: "pipe",
|
|
46
|
-
stderr: "pipe",
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
if (options?.signal?.aborted) {
|
|
50
|
-
proc.kill();
|
|
51
|
-
options.signal.throwIfAborted();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const onAbort = () => proc.kill();
|
|
55
|
-
options?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
56
|
-
|
|
57
|
-
let timedOut = false;
|
|
58
|
-
|
|
59
|
-
const killTimer = setTimeout(() => {
|
|
60
|
-
timedOut = true;
|
|
61
|
-
logger.error("Python whisper transcription timed out, killing process", { timeoutMs: TRANSCRIBE_TIMEOUT_MS });
|
|
62
|
-
proc.kill();
|
|
63
|
-
}, TRANSCRIBE_TIMEOUT_MS);
|
|
64
|
-
|
|
65
|
-
const exitCode = await proc.exited;
|
|
66
|
-
clearTimeout(killTimer);
|
|
67
|
-
options?.signal?.removeEventListener("abort", onAbort);
|
|
68
|
-
|
|
69
27
|
options?.signal?.throwIfAborted();
|
|
70
28
|
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
29
|
+
const spec = resolveSttModelSpec(options?.modelName);
|
|
30
|
+
const language = options?.language || undefined;
|
|
31
|
+
const audio = decodeWavToMono16k(await audioFile.arrayBuffer());
|
|
32
|
+
if (audio.length === 0) return "";
|
|
33
|
+
|
|
34
|
+
logger.debug("Transcribing with local ONNX whisper", {
|
|
35
|
+
audioPath,
|
|
36
|
+
modelKey: spec.key,
|
|
37
|
+
repo: spec.repo,
|
|
38
|
+
language,
|
|
39
|
+
samples: audio.length,
|
|
40
|
+
});
|
|
77
41
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
42
|
+
// Bound runaway inference. Abort the request on timeout; the warm worker
|
|
43
|
+
// keeps the model loaded (the request promise just rejects).
|
|
44
|
+
const timeout = new AbortController();
|
|
45
|
+
const timer = setTimeout(() => timeout.abort(), TRANSCRIBE_TIMEOUT_MS);
|
|
46
|
+
const signal = options?.signal ? AbortSignal.any([options.signal, timeout.signal]) : timeout.signal;
|
|
47
|
+
try {
|
|
48
|
+
const text = (await sttClient.transcribe(spec.key, audio, { language, signal })).trim();
|
|
49
|
+
logger.debug("Transcription complete", { length: text.length });
|
|
50
|
+
return text;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (timeout.signal.aborted && !options?.signal?.aborted) {
|
|
53
|
+
logger.error("Local whisper transcription timed out", { timeoutMs: TRANSCRIBE_TIMEOUT_MS });
|
|
54
|
+
throw new Error(`Transcription timed out after ${Math.round(TRANSCRIBE_TIMEOUT_MS / 1000)}s`);
|
|
82
55
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
56
|
+
throw error;
|
|
57
|
+
} finally {
|
|
58
|
+
clearTimeout(timer);
|
|
86
59
|
}
|
|
87
|
-
|
|
88
|
-
const text = stdout.trim();
|
|
89
|
-
logger.debug("Transcription complete", { length: text.length });
|
|
90
|
-
return text;
|
|
91
60
|
}
|
package/src/stt/wav.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal WAV (RIFF/PCM) decoder producing the Float32Array @ 16 kHz mono that
|
|
3
|
+
* transformers.js `automatic-speech-recognition` expects. Ports the decode/
|
|
4
|
+
* mono-mix/resample logic from the retired Python `transcribe.py` (which read
|
|
5
|
+
* via the stdlib `wave` module) so STT no longer shells out to Python.
|
|
6
|
+
*
|
|
7
|
+
* Supported sample formats: PCM uint8 (8-bit), int16 (16-bit), int32 (32-bit),
|
|
8
|
+
* and IEEE float32 (format tag 3). Any number of channels is mixed down to mono.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** transformers.js Whisper feature extractor operates at 16 kHz. */
|
|
12
|
+
export const TARGET_SAMPLE_RATE = 16_000;
|
|
13
|
+
|
|
14
|
+
const WAV_FORMAT_PCM = 1;
|
|
15
|
+
const WAV_FORMAT_IEEE_FLOAT = 3;
|
|
16
|
+
const WAV_FORMAT_EXTENSIBLE = 0xfffe;
|
|
17
|
+
|
|
18
|
+
interface WavData {
|
|
19
|
+
format: number;
|
|
20
|
+
channels: number;
|
|
21
|
+
sampleRate: number;
|
|
22
|
+
bitsPerSample: number;
|
|
23
|
+
/** Raw PCM/float bytes from the `data` chunk. */
|
|
24
|
+
samples: DataView;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readFourCc(view: DataView, offset: number): string {
|
|
28
|
+
return String.fromCharCode(
|
|
29
|
+
view.getUint8(offset),
|
|
30
|
+
view.getUint8(offset + 1),
|
|
31
|
+
view.getUint8(offset + 2),
|
|
32
|
+
view.getUint8(offset + 3),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Parse the RIFF container, returning the `fmt ` parameters and `data` bytes. */
|
|
37
|
+
function parseWav(buffer: ArrayBuffer): WavData {
|
|
38
|
+
const view = new DataView(buffer);
|
|
39
|
+
if (buffer.byteLength < 12 || readFourCc(view, 0) !== "RIFF" || readFourCc(view, 8) !== "WAVE") {
|
|
40
|
+
throw new Error("Not a RIFF/WAVE file");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let format: number | undefined;
|
|
44
|
+
let channels = 0;
|
|
45
|
+
let sampleRate = 0;
|
|
46
|
+
let bitsPerSample = 0;
|
|
47
|
+
let samples: DataView | undefined;
|
|
48
|
+
|
|
49
|
+
// Chunks begin after the 12-byte RIFF/WAVE header; each is an 8-byte header
|
|
50
|
+
// (4-char id + uint32 LE size) followed by `size` bytes padded to even.
|
|
51
|
+
let offset = 12;
|
|
52
|
+
while (offset + 8 <= buffer.byteLength) {
|
|
53
|
+
const id = readFourCc(view, offset);
|
|
54
|
+
const size = view.getUint32(offset + 4, true);
|
|
55
|
+
const body = offset + 8;
|
|
56
|
+
if (id === "fmt ") {
|
|
57
|
+
format = view.getUint16(body, true);
|
|
58
|
+
channels = view.getUint16(body + 2, true);
|
|
59
|
+
sampleRate = view.getUint32(body + 4, true);
|
|
60
|
+
bitsPerSample = view.getUint16(body + 14, true);
|
|
61
|
+
// WAVE_FORMAT_EXTENSIBLE (ffmpeg & friends): the real codec is the
|
|
62
|
+
// first 2 bytes of the SubFormat GUID in the fmt extension.
|
|
63
|
+
if (format === WAV_FORMAT_EXTENSIBLE && size >= 40) format = view.getUint16(body + 24, true);
|
|
64
|
+
} else if (id === "data") {
|
|
65
|
+
const length = Math.min(size, buffer.byteLength - body);
|
|
66
|
+
samples = new DataView(buffer, body, length);
|
|
67
|
+
}
|
|
68
|
+
offset = body + size + (size % 2);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (format === undefined || samples === undefined || channels < 1 || sampleRate < 1) {
|
|
72
|
+
throw new Error("WAV file missing fmt/data chunks");
|
|
73
|
+
}
|
|
74
|
+
return { format, channels, sampleRate, bitsPerSample, samples };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Decode raw PCM/float bytes into interleaved normalized [-1, 1] float samples. */
|
|
78
|
+
function decodeSamples(wav: WavData): Float32Array {
|
|
79
|
+
const { format, bitsPerSample, samples } = wav;
|
|
80
|
+
const view = samples;
|
|
81
|
+
if (format === WAV_FORMAT_IEEE_FLOAT && bitsPerSample === 32) {
|
|
82
|
+
const count = Math.floor(view.byteLength / 4);
|
|
83
|
+
const out = new Float32Array(count);
|
|
84
|
+
for (let i = 0; i < count; i += 1) out[i] = view.getFloat32(i * 4, true);
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
if (format !== WAV_FORMAT_PCM) {
|
|
88
|
+
throw new Error(`Unsupported WAV format tag: ${format}`);
|
|
89
|
+
}
|
|
90
|
+
if (bitsPerSample === 16) {
|
|
91
|
+
const count = Math.floor(view.byteLength / 2);
|
|
92
|
+
const out = new Float32Array(count);
|
|
93
|
+
for (let i = 0; i < count; i += 1) out[i] = view.getInt16(i * 2, true) / 32_768;
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
if (bitsPerSample === 8) {
|
|
97
|
+
// 8-bit PCM is unsigned, centered at 128.
|
|
98
|
+
const count = view.byteLength;
|
|
99
|
+
const out = new Float32Array(count);
|
|
100
|
+
for (let i = 0; i < count; i += 1) out[i] = (view.getUint8(i) - 128) / 128;
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
if (bitsPerSample === 32) {
|
|
104
|
+
const count = Math.floor(view.byteLength / 4);
|
|
105
|
+
const out = new Float32Array(count);
|
|
106
|
+
for (let i = 0; i < count; i += 1) out[i] = view.getInt32(i * 4, true) / 2_147_483_648;
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
throw new Error(`Unsupported PCM sample width: ${bitsPerSample} bits`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Average interleaved channels down to a single mono track. */
|
|
113
|
+
function mixToMono(interleaved: Float32Array, channels: number): Float32Array {
|
|
114
|
+
if (channels <= 1) return interleaved;
|
|
115
|
+
const frames = Math.floor(interleaved.length / channels);
|
|
116
|
+
const out = new Float32Array(frames);
|
|
117
|
+
for (let frame = 0; frame < frames; frame += 1) {
|
|
118
|
+
let sum = 0;
|
|
119
|
+
for (let channel = 0; channel < channels; channel += 1) sum += interleaved[frame * channels + channel]!;
|
|
120
|
+
out[frame] = sum / channels;
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resample via linear interpolation, mirroring the Python `np.interp` over
|
|
127
|
+
* `linspace(0, n-1, targetLen)` against `arange(n)`.
|
|
128
|
+
*/
|
|
129
|
+
export function resampleLinear(input: Float32Array, fromRate: number, toRate: number): Float32Array {
|
|
130
|
+
if (fromRate === toRate || input.length === 0) return input;
|
|
131
|
+
const n = input.length;
|
|
132
|
+
const targetLen = Math.max(1, Math.floor((n * toRate) / fromRate));
|
|
133
|
+
const out = new Float32Array(targetLen);
|
|
134
|
+
if (targetLen === 1) {
|
|
135
|
+
out[0] = input[0]!;
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
const step = (n - 1) / (targetLen - 1);
|
|
139
|
+
for (let i = 0; i < targetLen; i += 1) {
|
|
140
|
+
const pos = i * step;
|
|
141
|
+
const lo = Math.floor(pos);
|
|
142
|
+
const hi = Math.min(lo + 1, n - 1);
|
|
143
|
+
const frac = pos - lo;
|
|
144
|
+
out[i] = input[lo]! * (1 - frac) + input[hi]! * frac;
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Decode a WAV byte buffer into a 16 kHz mono Float32Array suitable for the
|
|
151
|
+
* transformers.js Whisper pipeline.
|
|
152
|
+
*/
|
|
153
|
+
export function decodeWavToMono16k(buffer: ArrayBuffer): Float32Array {
|
|
154
|
+
const wav = parseWav(buffer);
|
|
155
|
+
const interleaved = decodeSamples(wav);
|
|
156
|
+
const mono = mixToMono(interleaved, wav.channels);
|
|
157
|
+
return resampleLinear(mono, wav.sampleRate, TARGET_SAMPLE_RATE);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Decode interleaved little-endian signed 16-bit PCM bytes into normalized
|
|
162
|
+
* [-1, 1] mono float samples. The live recorder streams raw s16le frames from
|
|
163
|
+
* sox/ffmpeg/arecord stdout (no RIFF container), so this is the hot-path
|
|
164
|
+
* counterpart to {@link decodeWavToMono16k}. `bytes` MUST be 2-byte aligned;
|
|
165
|
+
* callers buffer any trailing odd byte across chunk boundaries.
|
|
166
|
+
*/
|
|
167
|
+
export function decodePcmS16LE(bytes: Uint8Array): Float32Array {
|
|
168
|
+
const count = bytes.length >>> 1;
|
|
169
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, count * 2);
|
|
170
|
+
const out = new Float32Array(count);
|
|
171
|
+
for (let i = 0; i < count; i += 1) out[i] = view.getInt16(i * 2, true) / 32_768;
|
|
172
|
+
return out;
|
|
173
|
+
}
|
package/src/system-prompt.ts
CHANGED
|
@@ -385,6 +385,10 @@ export interface BuildSystemPromptOptions {
|
|
|
385
385
|
mcpDiscoveryServerSummaries?: string[];
|
|
386
386
|
/** Encourage the agent to delegate via tasks unless changes are trivial. */
|
|
387
387
|
eagerTasks?: boolean;
|
|
388
|
+
/** When true, the Eager Tasks section uses the hard MUST/ONLY wording (`task.eager: always`) rather than the softer `preferred` nudge. */
|
|
389
|
+
eagerTasksAlways?: boolean;
|
|
390
|
+
/** Whether `task.batch` is enabled; gates batch-call guidance in the Eager Tasks section. */
|
|
391
|
+
taskBatch?: boolean;
|
|
388
392
|
/** Rules with alwaysApply=true — their full content is injected into the prompt. */
|
|
389
393
|
alwaysApplyRules?: AlwaysApplyRule[];
|
|
390
394
|
/** Whether secret obfuscation is active. When true, explains the redaction format in the prompt. */
|
|
@@ -427,6 +431,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
427
431
|
mcpDiscoveryMode = false,
|
|
428
432
|
mcpDiscoveryServerSummaries = [],
|
|
429
433
|
eagerTasks = false,
|
|
434
|
+
eagerTasksAlways = false,
|
|
435
|
+
taskBatch = true,
|
|
430
436
|
secretsEnabled = false,
|
|
431
437
|
workspaceTree: providedWorkspaceTree,
|
|
432
438
|
memoryRootEnabled = false,
|
|
@@ -610,6 +616,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
610
616
|
hasMCPDiscoveryServers: mcpDiscoveryServerSummaries.length > 0,
|
|
611
617
|
mcpDiscoveryServerSummaries,
|
|
612
618
|
eagerTasks,
|
|
619
|
+
eagerTasksAlways,
|
|
620
|
+
taskBatch,
|
|
613
621
|
secretsEnabled,
|
|
614
622
|
hasMemoryRoot: memoryRootEnabled,
|
|
615
623
|
hasObsidian: hasObsidian(),
|
package/src/task/agents.ts
CHANGED
|
@@ -55,7 +55,6 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
|
|
|
55
55
|
description: "General-purpose subagent with full capabilities for delegated multi-step tasks",
|
|
56
56
|
spawns: "*",
|
|
57
57
|
model: "pi/task",
|
|
58
|
-
thinkingLevel: Effort.Medium,
|
|
59
58
|
},
|
|
60
59
|
template: taskMd,
|
|
61
60
|
},
|
|
@@ -65,7 +64,7 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [
|
|
|
65
64
|
name: "quick_task",
|
|
66
65
|
description: "Low-reasoning agent for strictly mechanical updates or data collection only",
|
|
67
66
|
model: "pi/smol",
|
|
68
|
-
thinkingLevel: Effort.
|
|
67
|
+
thinkingLevel: Effort.Medium,
|
|
69
68
|
},
|
|
70
69
|
template: taskMd,
|
|
71
70
|
},
|
package/src/task/executor.ts
CHANGED
|
@@ -8,7 +8,7 @@ import path from "node:path";
|
|
|
8
8
|
import type { AgentEvent, AgentIdentity, AgentTelemetryConfig, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
9
9
|
import { recordHandoff, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
|
|
10
10
|
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
11
|
-
import { logger, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
11
|
+
import { logger, popLoopPhase, prompt, pushLoopPhase, untilAborted } from "@oh-my-pi/pi-utils";
|
|
12
12
|
import type { Rule } from "../capability/rule";
|
|
13
13
|
import { ModelRegistry } from "../config/model-registry";
|
|
14
14
|
import { resolveModelOverrideWithAuthFallback } from "../config/model-resolver";
|
|
@@ -56,7 +56,9 @@ import {
|
|
|
56
56
|
type AgentProgress,
|
|
57
57
|
MAX_OUTPUT_BYTES,
|
|
58
58
|
MAX_OUTPUT_LINES,
|
|
59
|
+
oneLineLabel,
|
|
59
60
|
type ReviewFinding,
|
|
61
|
+
resolveSubagentDisplayName,
|
|
60
62
|
type SingleResult,
|
|
61
63
|
TASK_SUBAGENT_EVENT_CHANNEL,
|
|
62
64
|
TASK_SUBAGENT_LIFECYCLE_CHANNEL,
|
|
@@ -123,7 +125,10 @@ function renderIrcPeerRoster(selfId: string): string {
|
|
|
123
125
|
.list()
|
|
124
126
|
.filter(ref => ref.id !== selfId && ref.status !== "aborted");
|
|
125
127
|
if (peers.length === 0) return "- (no other agents)";
|
|
126
|
-
const lines = peers.map(
|
|
128
|
+
const lines = peers.map(
|
|
129
|
+
peer =>
|
|
130
|
+
`- \`${peer.id}\` — ${peer.displayName} (${peer.kind}, ${peer.status})${peer.activity ? `: ${peer.activity}` : ""}`,
|
|
131
|
+
);
|
|
127
132
|
if (peers.some(peer => peer.status === "idle" || peer.status === "parked")) {
|
|
128
133
|
lines.push("Idle/parked peers are not gone: messaging them wakes (or revives) them.");
|
|
129
134
|
}
|
|
@@ -192,6 +197,8 @@ export interface ExecutorOptions {
|
|
|
192
197
|
*/
|
|
193
198
|
planReference?: { path: string; content: string };
|
|
194
199
|
description?: string;
|
|
200
|
+
/** Specialist role/expertise for this spawn; drives the system-prompt preamble, display name, and telemetry identity. */
|
|
201
|
+
role?: string;
|
|
195
202
|
index: number;
|
|
196
203
|
id: string;
|
|
197
204
|
parentToolCallId?: string;
|
|
@@ -837,6 +844,9 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
837
844
|
const emitProgressNow = () => {
|
|
838
845
|
progress.durationMs = Date.now() - startTime;
|
|
839
846
|
onProgress?.({ ...progress });
|
|
847
|
+
const activityGist =
|
|
848
|
+
progress.lastIntent ?? (progress.currentTool ? `running ${progress.currentTool}` : undefined);
|
|
849
|
+
if (activityGist) AgentRegistry.global().setActivity(id, activityGist);
|
|
840
850
|
if (args.eventBus) {
|
|
841
851
|
args.eventBus.emit(TASK_SUBAGENT_PROGRESS_CHANNEL, {
|
|
842
852
|
index,
|
|
@@ -1198,6 +1208,9 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
1198
1208
|
return;
|
|
1199
1209
|
}
|
|
1200
1210
|
if (isAgentEvent(event)) {
|
|
1211
|
+
// Breadcrumb the synchronous subagent event handling so the loop
|
|
1212
|
+
// watchdog can attribute any block to this in-process subagent.
|
|
1213
|
+
pushLoopPhase(`subagent:${id}`);
|
|
1201
1214
|
try {
|
|
1202
1215
|
processEvent(event);
|
|
1203
1216
|
} catch (err) {
|
|
@@ -1205,6 +1218,8 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
1205
1218
|
error: err instanceof Error ? err.message : String(err),
|
|
1206
1219
|
});
|
|
1207
1220
|
requestAbort("terminate");
|
|
1221
|
+
} finally {
|
|
1222
|
+
popLoopPhase();
|
|
1208
1223
|
}
|
|
1209
1224
|
}
|
|
1210
1225
|
});
|
|
@@ -1444,16 +1459,24 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
|
|
|
1444
1459
|
const yieldItems = progress.extractedToolData?.yield as YieldItem[] | undefined;
|
|
1445
1460
|
const reportFindingDetails = progress.extractedToolData?.report_finding as ReportFindingDetails[] | undefined;
|
|
1446
1461
|
const reportFindings: ReviewFinding[] | undefined = reportFindingDetails?.map(toReviewFinding);
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1462
|
+
// Breadcrumb the synchronous yield-payload shaping (O(rawOutput)) so a block
|
|
1463
|
+
// here is attributed to this subagent rather than logged as "unknown".
|
|
1464
|
+
pushLoopPhase(`subagent:${id}`);
|
|
1465
|
+
let finalized: FinalizeSubprocessOutputResult;
|
|
1466
|
+
try {
|
|
1467
|
+
finalized = finalizeSubprocessOutput({
|
|
1468
|
+
rawOutput,
|
|
1469
|
+
exitCode,
|
|
1470
|
+
stderr,
|
|
1471
|
+
doneAborted: Boolean(done.aborted),
|
|
1472
|
+
signalAborted: Boolean(signal?.aborted),
|
|
1473
|
+
yieldItems,
|
|
1474
|
+
reportFindings,
|
|
1475
|
+
outputSchema: args.outputSchema,
|
|
1476
|
+
});
|
|
1477
|
+
} finally {
|
|
1478
|
+
popLoopPhase();
|
|
1479
|
+
}
|
|
1457
1480
|
rawOutput = finalized.rawOutput;
|
|
1458
1481
|
exitCode = finalized.exitCode;
|
|
1459
1482
|
stderr = finalized.stderr;
|
|
@@ -1618,6 +1641,12 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1618
1641
|
agent.readSummarize === false ? { "read.summarize.enabled": false } : undefined,
|
|
1619
1642
|
);
|
|
1620
1643
|
const maxRecursionDepth = settings.get("task.maxRecursionDepth") ?? 2;
|
|
1644
|
+
// Tailored specialist identity for this spawn. `subagentRole` is the full
|
|
1645
|
+
// (trimmed) role text fed to the system-prompt preamble; `subagentDisplayName`
|
|
1646
|
+
// is the label-normalized form the registry/roster show, falling back to the
|
|
1647
|
+
// agent type name when no role was given.
|
|
1648
|
+
const subagentRole = options.role?.trim() || undefined;
|
|
1649
|
+
const subagentDisplayName = resolveSubagentDisplayName(options.role, agent.name);
|
|
1621
1650
|
const maxRuntimeMs = Math.max(
|
|
1622
1651
|
0,
|
|
1623
1652
|
Math.trunc(Number(options.maxRuntimeMs ?? settings.get("task.maxRuntimeMs") ?? 0) || 0),
|
|
@@ -1816,7 +1845,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1816
1845
|
// carry the subagent's own agent identity, and use the subagent's
|
|
1817
1846
|
// own session id for `gen_ai.conversation.id`.
|
|
1818
1847
|
const subagentAgentIdentity: AgentIdentity | undefined = options.parentTelemetry
|
|
1819
|
-
? {
|
|
1848
|
+
? {
|
|
1849
|
+
id,
|
|
1850
|
+
name: subagentDisplayName,
|
|
1851
|
+
description: subagentRole ? oneLineLabel(subagentRole) : agent.description,
|
|
1852
|
+
}
|
|
1820
1853
|
: undefined;
|
|
1821
1854
|
const subagentTelemetry: AgentTelemetryConfig | undefined =
|
|
1822
1855
|
options.parentTelemetry && subagentAgentIdentity
|
|
@@ -1866,6 +1899,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1866
1899
|
systemPrompt: defaultPrompt => {
|
|
1867
1900
|
const subagentPrompt = prompt.render(subagentSystemPromptTemplate, {
|
|
1868
1901
|
agent: agent.systemPrompt,
|
|
1902
|
+
role: subagentRole ? oneLineLabel(subagentRole) : "",
|
|
1869
1903
|
context: options.context?.trim() ?? "",
|
|
1870
1904
|
planReference: options.planReference?.content ?? "",
|
|
1871
1905
|
planReferencePath: options.planReference?.path ?? "",
|
|
@@ -1886,7 +1920,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1886
1920
|
parentMnemopiSessionState: options.parentMnemopiSessionState,
|
|
1887
1921
|
parentTaskPrefix: id,
|
|
1888
1922
|
agentId: id,
|
|
1889
|
-
agentDisplayName:
|
|
1923
|
+
agentDisplayName: subagentDisplayName,
|
|
1890
1924
|
enableLsp: lspEnabled,
|
|
1891
1925
|
skipPythonPreflight,
|
|
1892
1926
|
enableMCP,
|
|
@@ -1971,7 +2005,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1971
2005
|
}
|
|
1972
2006
|
|
|
1973
2007
|
const extensionRunner = session.extensionRunner;
|
|
1974
|
-
const pendingExtensionMessages: Promise<
|
|
2008
|
+
const pendingExtensionMessages: Promise<unknown>[] = [];
|
|
1975
2009
|
if (extensionRunner) {
|
|
1976
2010
|
extensionRunner.initialize(
|
|
1977
2011
|
{
|
package/src/task/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { formatBytes, formatDuration } from "../tools/render-utils";
|
|
|
33
33
|
import {
|
|
34
34
|
type AgentDefinition,
|
|
35
35
|
type AgentProgress,
|
|
36
|
+
canSpawnAtDepth,
|
|
36
37
|
getTaskSchema,
|
|
37
38
|
type SingleResult,
|
|
38
39
|
type TaskItem,
|
|
@@ -310,7 +311,7 @@ function resolveSpawnItems(params: TaskParams): TaskItem[] {
|
|
|
310
311
|
if (Array.isArray(params.tasks) && params.tasks.length > 0) {
|
|
311
312
|
return params.tasks;
|
|
312
313
|
}
|
|
313
|
-
return [{ id: params.id, description: params.description, assignment: params.assignment }];
|
|
314
|
+
return [{ id: params.id, description: params.description, role: params.role, assignment: params.assignment }];
|
|
314
315
|
}
|
|
315
316
|
|
|
316
317
|
/**
|
|
@@ -324,6 +325,7 @@ function spawnParamsFor(params: TaskParams, item: TaskItem): TaskParams {
|
|
|
324
325
|
const spawn: TaskParams = { agent: params.agent };
|
|
325
326
|
if (item.id !== undefined) spawn.id = item.id;
|
|
326
327
|
if (item.description !== undefined) spawn.description = item.description;
|
|
328
|
+
if (item.role !== undefined) spawn.role = item.role;
|
|
327
329
|
if (item.assignment !== undefined) spawn.assignment = item.assignment;
|
|
328
330
|
if (params.context !== undefined) spawn.context = params.context;
|
|
329
331
|
if (item.isolated !== undefined) {
|
|
@@ -334,6 +336,36 @@ function spawnParamsFor(params: TaskParams, item: TaskItem): TaskParams {
|
|
|
334
336
|
return spawn;
|
|
335
337
|
}
|
|
336
338
|
|
|
339
|
+
/** Generic worker agents whose output sharpens with a tailored `role` rather than the bare type. */
|
|
340
|
+
const GENERIC_SPAWN_AGENTS: ReadonlySet<string> = new Set(["task", "quick_task"]);
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Advisory — never a rejection — nudging the spawner toward tailored
|
|
344
|
+
* specialists when it spawns generic role-less workers and still holds spawn
|
|
345
|
+
* capacity (DepthCapacity: it currently has the `task` tool). Fires when a
|
|
346
|
+
* generic `task`/`quick_task` spawn carries no `role`, or when one call clones
|
|
347
|
+
* the same agent ≥2× all without roles. Returns undefined when no nudge applies.
|
|
348
|
+
*/
|
|
349
|
+
export function buildSpecializationAdvisory(
|
|
350
|
+
agentName: string | undefined,
|
|
351
|
+
items: TaskItem[],
|
|
352
|
+
depthCapacity: boolean,
|
|
353
|
+
): string | undefined {
|
|
354
|
+
if (!depthCapacity) return undefined;
|
|
355
|
+
const rolelessCount = items.filter(item => !item.role?.trim()).length;
|
|
356
|
+
if (rolelessCount === 0) return undefined;
|
|
357
|
+
const generic = agentName !== undefined && GENERIC_SPAWN_AGENTS.has(agentName);
|
|
358
|
+
const cloned = items.length >= 2 && rolelessCount === items.length;
|
|
359
|
+
if (!generic && !cloned) return undefined;
|
|
360
|
+
const label = agentName ?? "task";
|
|
361
|
+
return (
|
|
362
|
+
`Tip: spawned ${rolelessCount} \`${label}\` worker${rolelessCount === 1 ? "" : "s"} without a \`role\`. ` +
|
|
363
|
+
`Tailored specialists outperform generic workers — give each spawn a \`role\` naming its expertise ` +
|
|
364
|
+
`(e.g. "Auth-flow security reviewer"). Depth budget remains, so decompose into named specialists ` +
|
|
365
|
+
`rather than cloning one generic worker.`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
337
369
|
/** Sentinel for async jobs whose subagent finished with a failing result; progress is already updated. */
|
|
338
370
|
class TaskJobError extends Error {}
|
|
339
371
|
|
|
@@ -388,6 +420,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
388
420
|
if (typeof params.agent === "string") {
|
|
389
421
|
lines.push(`Agent: ${truncateForPrompt(params.agent)}`);
|
|
390
422
|
}
|
|
423
|
+
if (typeof params.role === "string" && params.role.trim()) {
|
|
424
|
+
lines.push(`Role: ${truncateForPrompt(params.role)}`);
|
|
425
|
+
}
|
|
391
426
|
if (typeof params.id === "string" && params.id.trim()) {
|
|
392
427
|
lines.push(`Task: ${truncateForPrompt(params.id)}`);
|
|
393
428
|
}
|
|
@@ -403,6 +438,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
403
438
|
if (typeof firstTask.id === "string" && firstTask.id.trim()) {
|
|
404
439
|
lines.push(`Task: ${truncateForPrompt(firstTask.id)}`);
|
|
405
440
|
}
|
|
441
|
+
if (typeof firstTask.role === "string" && firstTask.role.trim()) {
|
|
442
|
+
lines.push(`Role: ${truncateForPrompt(firstTask.role)}`);
|
|
443
|
+
}
|
|
406
444
|
if (typeof firstTask.assignment === "string") {
|
|
407
445
|
lines.push(`Assignment:\n${truncateForPrompt(firstTask.assignment)}`);
|
|
408
446
|
}
|
|
@@ -497,6 +535,21 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
497
535
|
const selectedAgent = this.#discoveredAgents.find(agent => agent.name === params.agent);
|
|
498
536
|
const asyncEnabled = this.session.settings.get("async.enabled");
|
|
499
537
|
const manager = asyncEnabled ? this.session.asyncJobManager : undefined;
|
|
538
|
+
const depthCapacity = canSpawnAtDepth(
|
|
539
|
+
this.session.settings.get("task.maxRecursionDepth") ?? 2,
|
|
540
|
+
this.session.taskDepth ?? 0,
|
|
541
|
+
);
|
|
542
|
+
const advisory = buildSpecializationAdvisory(params.agent, spawnItems, depthCapacity);
|
|
543
|
+
const withAdvisory = (result: AgentToolResult<TaskToolDetails>): AgentToolResult<TaskToolDetails> => {
|
|
544
|
+
if (!advisory) return result;
|
|
545
|
+
const textPart = result.content.find(part => part.type === "text");
|
|
546
|
+
if (textPart && typeof textPart.text === "string") {
|
|
547
|
+
textPart.text = `${textPart.text}\n\n${advisory}`;
|
|
548
|
+
} else {
|
|
549
|
+
result.content.push({ type: "text", text: advisory });
|
|
550
|
+
}
|
|
551
|
+
return result;
|
|
552
|
+
};
|
|
500
553
|
if (!asyncEnabled || !manager || selectedAgent?.blocking === true) {
|
|
501
554
|
// Sync fallback: async execution disabled, orphaned host that never
|
|
502
555
|
// wired a job manager, or an agent definition that declares
|
|
@@ -505,7 +558,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
505
558
|
if (asyncEnabled && !manager) {
|
|
506
559
|
logger.warn("task: no AsyncJobManager registered; falling back to sync execution");
|
|
507
560
|
}
|
|
508
|
-
return this.#executeSyncFanout(toolCallId, params, spawnItems, signal, onUpdate);
|
|
561
|
+
return withAdvisory(await this.#executeSyncFanout(toolCallId, params, spawnItems, signal, onUpdate));
|
|
509
562
|
}
|
|
510
563
|
|
|
511
564
|
// Resolve agent ids up front so the immediate result can name them.
|
|
@@ -613,7 +666,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
613
666
|
content: [{ type: "text", text: `Spawned agent \`${agentId}\`...` }],
|
|
614
667
|
details: buildAsyncDetails("running", jobId),
|
|
615
668
|
});
|
|
616
|
-
return {
|
|
669
|
+
return withAdvisory({
|
|
617
670
|
content: [
|
|
618
671
|
{
|
|
619
672
|
type: "text",
|
|
@@ -621,7 +674,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
621
674
|
},
|
|
622
675
|
],
|
|
623
676
|
details: buildAsyncDetails("running", jobId),
|
|
624
|
-
};
|
|
677
|
+
});
|
|
625
678
|
}
|
|
626
679
|
|
|
627
680
|
const coordinationHint = ircEnabled
|
|
@@ -641,7 +694,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
641
694
|
content: [{ type: "text", text: `Spawned ${started.length} agents...` }],
|
|
642
695
|
details: buildAsyncDetails("running", primaryJobId),
|
|
643
696
|
});
|
|
644
|
-
return {
|
|
697
|
+
return withAdvisory({
|
|
645
698
|
content: [
|
|
646
699
|
{
|
|
647
700
|
type: "text",
|
|
@@ -649,7 +702,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
649
702
|
},
|
|
650
703
|
],
|
|
651
704
|
details: buildAsyncDetails("running", primaryJobId),
|
|
652
|
-
};
|
|
705
|
+
});
|
|
653
706
|
}
|
|
654
707
|
|
|
655
708
|
/**
|
|
@@ -1146,6 +1199,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1146
1199
|
context: sharedContext,
|
|
1147
1200
|
planReference,
|
|
1148
1201
|
description: params.description,
|
|
1202
|
+
role: params.role,
|
|
1149
1203
|
index: spawnIndex,
|
|
1150
1204
|
parentToolCallId: toolCallId,
|
|
1151
1205
|
detached,
|