@oh-my-pi/pi-coding-agent 12.4.0 → 12.5.1
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 +56 -0
- package/docs/custom-tools.md +21 -6
- package/docs/extensions.md +20 -0
- package/package.json +12 -12
- package/src/cli/setup-cli.ts +62 -2
- package/src/commands/setup.ts +1 -1
- package/src/config/keybindings.ts +4 -1
- package/src/config/settings-schema.ts +67 -4
- package/src/config/settings.ts +23 -9
- package/src/debug/index.ts +26 -19
- package/src/debug/log-formatting.ts +60 -0
- package/src/debug/log-viewer.ts +903 -0
- package/src/debug/report-bundle.ts +87 -8
- package/src/discovery/helpers.ts +131 -137
- package/src/extensibility/custom-tools/types.ts +44 -6
- package/src/extensibility/extensions/types.ts +60 -0
- package/src/extensibility/hooks/types.ts +60 -0
- package/src/extensibility/skills.ts +4 -2
- package/src/main.ts +7 -1
- package/src/modes/components/custom-editor.ts +8 -0
- package/src/modes/components/settings-selector.ts +29 -14
- package/src/modes/controllers/command-controller.ts +2 -0
- package/src/modes/controllers/event-controller.ts +7 -0
- package/src/modes/controllers/input-controller.ts +23 -2
- package/src/modes/controllers/selector-controller.ts +9 -7
- package/src/modes/interactive-mode.ts +84 -1
- package/src/modes/rpc/rpc-client.ts +7 -0
- package/src/modes/rpc/rpc-mode.ts +8 -0
- package/src/modes/rpc/rpc-types.ts +2 -0
- package/src/modes/theme/theme.ts +163 -7
- package/src/modes/types.ts +1 -0
- package/src/patch/hashline.ts +2 -1
- package/src/patch/shared.ts +44 -13
- package/src/prompts/system/plan-mode-approved.md +5 -0
- package/src/prompts/system/subagent-system-prompt.md +1 -0
- package/src/prompts/system/system-prompt.md +14 -0
- package/src/prompts/tools/hashline.md +63 -72
- package/src/prompts/tools/todo-write.md +3 -1
- package/src/sdk.ts +87 -9
- package/src/session/agent-session.ts +137 -29
- package/src/stt/downloader.ts +71 -0
- package/src/stt/index.ts +3 -0
- package/src/stt/recorder.ts +351 -0
- package/src/stt/setup.ts +52 -0
- package/src/stt/stt-controller.ts +160 -0
- package/src/stt/transcribe.py +70 -0
- package/src/stt/transcriber.ts +91 -0
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +10 -2
package/src/sdk.ts
CHANGED
|
@@ -323,6 +323,7 @@ export interface BuildSystemPromptOptions {
|
|
|
323
323
|
contextFiles?: Array<{ path: string; content: string }>;
|
|
324
324
|
cwd?: string;
|
|
325
325
|
appendPrompt?: string;
|
|
326
|
+
repeatToolDescriptions?: boolean;
|
|
326
327
|
}
|
|
327
328
|
|
|
328
329
|
/**
|
|
@@ -334,6 +335,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
334
335
|
skills: options.skills,
|
|
335
336
|
contextFiles: options.contextFiles,
|
|
336
337
|
appendSystemPrompt: options.appendPrompt,
|
|
338
|
+
repeatToolDescriptions: options.repeatToolDescriptions,
|
|
337
339
|
});
|
|
338
340
|
}
|
|
339
341
|
|
|
@@ -441,6 +443,58 @@ function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory {
|
|
|
441
443
|
api.on("session_shutdown", async (_event, ctx) =>
|
|
442
444
|
runOnSession({ reason: "shutdown", previousSessionFile: undefined }, ctx),
|
|
443
445
|
);
|
|
446
|
+
api.on("auto_compaction_start", async (event, ctx) =>
|
|
447
|
+
runOnSession({ reason: "auto_compaction_start", trigger: event.reason }, ctx),
|
|
448
|
+
);
|
|
449
|
+
api.on("auto_compaction_end", async (event, ctx) =>
|
|
450
|
+
runOnSession(
|
|
451
|
+
{
|
|
452
|
+
reason: "auto_compaction_end",
|
|
453
|
+
result: event.result,
|
|
454
|
+
aborted: event.aborted,
|
|
455
|
+
willRetry: event.willRetry,
|
|
456
|
+
errorMessage: event.errorMessage,
|
|
457
|
+
},
|
|
458
|
+
ctx,
|
|
459
|
+
),
|
|
460
|
+
);
|
|
461
|
+
api.on("auto_retry_start", async (event, ctx) =>
|
|
462
|
+
runOnSession(
|
|
463
|
+
{
|
|
464
|
+
reason: "auto_retry_start",
|
|
465
|
+
attempt: event.attempt,
|
|
466
|
+
maxAttempts: event.maxAttempts,
|
|
467
|
+
delayMs: event.delayMs,
|
|
468
|
+
errorMessage: event.errorMessage,
|
|
469
|
+
},
|
|
470
|
+
ctx,
|
|
471
|
+
),
|
|
472
|
+
);
|
|
473
|
+
api.on("auto_retry_end", async (event, ctx) =>
|
|
474
|
+
runOnSession(
|
|
475
|
+
{
|
|
476
|
+
reason: "auto_retry_end",
|
|
477
|
+
success: event.success,
|
|
478
|
+
attempt: event.attempt,
|
|
479
|
+
finalError: event.finalError,
|
|
480
|
+
},
|
|
481
|
+
ctx,
|
|
482
|
+
),
|
|
483
|
+
);
|
|
484
|
+
api.on("ttsr_triggered", async (event, ctx) =>
|
|
485
|
+
runOnSession({ reason: "ttsr_triggered", rules: event.rules }, ctx),
|
|
486
|
+
);
|
|
487
|
+
api.on("todo_reminder", async (event, ctx) =>
|
|
488
|
+
runOnSession(
|
|
489
|
+
{
|
|
490
|
+
reason: "todo_reminder",
|
|
491
|
+
todos: event.todos,
|
|
492
|
+
attempt: event.attempt,
|
|
493
|
+
maxAttempts: event.maxAttempts,
|
|
494
|
+
},
|
|
495
|
+
ctx,
|
|
496
|
+
),
|
|
497
|
+
);
|
|
444
498
|
};
|
|
445
499
|
}
|
|
446
500
|
|
|
@@ -496,6 +550,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
496
550
|
time("settings");
|
|
497
551
|
initializeWithSettings(settings);
|
|
498
552
|
time("initializeWithSettings");
|
|
553
|
+
const skillsSettings = settings.getGroup("skills") as SkillsSettings;
|
|
554
|
+
const discoveredSkillsPromise =
|
|
555
|
+
options.skills === undefined ? discoverSkills(cwd, agentDir, skillsSettings) : undefined;
|
|
499
556
|
|
|
500
557
|
// Initialize provider preferences from settings
|
|
501
558
|
setPreferredSearchProvider(settings.get("providers.webSearch") ?? "auto");
|
|
@@ -504,6 +561,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
504
561
|
const sessionManager = options.sessionManager ?? SessionManager.create(cwd);
|
|
505
562
|
time("sessionManager");
|
|
506
563
|
const sessionId = sessionManager.getSessionId();
|
|
564
|
+
const modelApiKeyAvailability = new Map<string, boolean>();
|
|
565
|
+
const getModelAvailabilityKey = (candidate: Model): string =>
|
|
566
|
+
`${candidate.provider}\u0000${candidate.baseUrl ?? ""}`;
|
|
567
|
+
const hasModelApiKey = async (candidate: Model): Promise<boolean> => {
|
|
568
|
+
const availabilityKey = getModelAvailabilityKey(candidate);
|
|
569
|
+
const cached = modelApiKeyAvailability.get(availabilityKey);
|
|
570
|
+
if (cached !== undefined) {
|
|
571
|
+
return cached;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const hasKey = !!(await modelRegistry.getApiKey(candidate, sessionId));
|
|
575
|
+
modelApiKeyAvailability.set(availabilityKey, hasKey);
|
|
576
|
+
return hasKey;
|
|
577
|
+
};
|
|
507
578
|
|
|
508
579
|
// Check if session has existing data to restore
|
|
509
580
|
const existingSession = sessionManager.buildSessionContext();
|
|
@@ -521,7 +592,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
521
592
|
const parsedModel = parseModelString(defaultModelStr);
|
|
522
593
|
if (parsedModel) {
|
|
523
594
|
const restoredModel = modelRegistry.find(parsedModel.provider, parsedModel.id);
|
|
524
|
-
if (restoredModel && (await
|
|
595
|
+
if (restoredModel && (await hasModelApiKey(restoredModel))) {
|
|
525
596
|
model = restoredModel;
|
|
526
597
|
}
|
|
527
598
|
}
|
|
@@ -537,7 +608,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
537
608
|
const parsedModel = parseModelString(settingsDefaultModel);
|
|
538
609
|
if (parsedModel) {
|
|
539
610
|
const settingsModel = modelRegistry.find(parsedModel.provider, parsedModel.id);
|
|
540
|
-
if (settingsModel && (await
|
|
611
|
+
if (settingsModel && (await hasModelApiKey(settingsModel))) {
|
|
541
612
|
model = settingsModel;
|
|
542
613
|
}
|
|
543
614
|
}
|
|
@@ -547,10 +618,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
547
618
|
// Fall back to first available model with a valid API key
|
|
548
619
|
if (!model) {
|
|
549
620
|
const allModels = modelRegistry.getAll();
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
621
|
+
for (const candidate of allModels) {
|
|
622
|
+
if (await hasModelApiKey(candidate)) {
|
|
623
|
+
model = candidate;
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
554
627
|
time("findAvailableModel");
|
|
555
628
|
if (model) {
|
|
556
629
|
if (modelFallbackMessage) {
|
|
@@ -563,6 +636,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
563
636
|
}
|
|
564
637
|
}
|
|
565
638
|
|
|
639
|
+
time("findModel");
|
|
640
|
+
|
|
566
641
|
// For subagent sessions using GitHub Copilot, add X-Initiator header
|
|
567
642
|
// to ensure proper billing (agent-initiated vs user-initiated)
|
|
568
643
|
const taskDepth = options.taskDepth ?? 0;
|
|
@@ -604,12 +679,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
604
679
|
skills = options.skills;
|
|
605
680
|
skillWarnings = [];
|
|
606
681
|
} else {
|
|
607
|
-
const
|
|
608
|
-
|
|
682
|
+
const discovered = discoveredSkillsPromise ? await discoveredSkillsPromise : { skills: [], warnings: [] };
|
|
683
|
+
time("discoverSkills");
|
|
609
684
|
skills = discovered.skills;
|
|
610
685
|
skillWarnings = discovered.warnings;
|
|
611
686
|
}
|
|
612
|
-
|
|
687
|
+
|
|
613
688
|
debugStartup("sdk:discoverSkills");
|
|
614
689
|
|
|
615
690
|
// Discover rules
|
|
@@ -913,6 +988,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
913
988
|
emitEvent: event => cursorEventEmitter?.(event),
|
|
914
989
|
});
|
|
915
990
|
|
|
991
|
+
const repeatToolDescriptions = settings.get("repeatToolDescriptions");
|
|
916
992
|
const rebuildSystemPrompt = async (toolNames: string[], tools: Map<string, AgentTool>): Promise<string> => {
|
|
917
993
|
toolContextStore.setToolNames(toolNames);
|
|
918
994
|
const memoryInstructions = await buildMemoryToolDeveloperInstructions(agentDir, settings);
|
|
@@ -926,6 +1002,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
926
1002
|
rules: rulebookRules,
|
|
927
1003
|
skillsSettings: settings.getGroup("skills") as SkillsSettings,
|
|
928
1004
|
appendSystemPrompt: memoryInstructions,
|
|
1005
|
+
repeatToolDescriptions,
|
|
929
1006
|
});
|
|
930
1007
|
|
|
931
1008
|
if (options.systemPrompt === undefined) {
|
|
@@ -943,6 +1020,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
943
1020
|
skillsSettings: settings.getGroup("skills") as SkillsSettings,
|
|
944
1021
|
customPrompt: options.systemPrompt,
|
|
945
1022
|
appendSystemPrompt: memoryInstructions,
|
|
1023
|
+
repeatToolDescriptions,
|
|
946
1024
|
});
|
|
947
1025
|
}
|
|
948
1026
|
return options.systemPrompt(defaultPrompt);
|
|
@@ -66,7 +66,7 @@ import type { Skill, SkillWarning } from "../extensibility/skills";
|
|
|
66
66
|
import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
|
|
67
67
|
import { resolvePlanUrlToPath } from "../internal-urls";
|
|
68
68
|
import { executePython as executePythonCommand, type PythonResult } from "../ipy/executor";
|
|
69
|
-
import { theme } from "../modes/theme/theme";
|
|
69
|
+
import { getCurrentThemeName, theme } from "../modes/theme/theme";
|
|
70
70
|
import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../patch";
|
|
71
71
|
import type { PlanModeState } from "../plan-mode/state";
|
|
72
72
|
import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
|
|
@@ -340,6 +340,7 @@ export class AgentSession {
|
|
|
340
340
|
#streamingEditCheckedLineCounts = new Map<string, number>();
|
|
341
341
|
#streamingEditFileCache = new Map<string, string>();
|
|
342
342
|
#promptInFlight = false;
|
|
343
|
+
#promptGeneration = 0;
|
|
343
344
|
#providerSessionState = new Map<string, ProviderSessionState>();
|
|
344
345
|
|
|
345
346
|
constructor(config: AgentSessionConfig) {
|
|
@@ -400,6 +401,11 @@ export class AgentSession {
|
|
|
400
401
|
}
|
|
401
402
|
}
|
|
402
403
|
|
|
404
|
+
async #emitSessionEvent(event: AgentSessionEvent): Promise<void> {
|
|
405
|
+
await this.#emitExtensionEvent(event);
|
|
406
|
+
this.#emit(event);
|
|
407
|
+
}
|
|
408
|
+
|
|
403
409
|
// Track last assistant message for auto-compaction check
|
|
404
410
|
#lastAssistantMessage: AssistantMessage | undefined = undefined;
|
|
405
411
|
|
|
@@ -424,11 +430,7 @@ export class AgentSession {
|
|
|
424
430
|
}
|
|
425
431
|
}
|
|
426
432
|
|
|
427
|
-
|
|
428
|
-
await this.#emitExtensionEvent(event);
|
|
429
|
-
|
|
430
|
-
// Notify all listeners
|
|
431
|
-
this.#emit(event);
|
|
433
|
+
await this.#emitSessionEvent(event);
|
|
432
434
|
|
|
433
435
|
if (event.type === "turn_start") {
|
|
434
436
|
this.#resetStreamingEditState();
|
|
@@ -453,11 +455,11 @@ export class AgentSession {
|
|
|
453
455
|
this.#ttsrManager.markInjected(matches);
|
|
454
456
|
// Store for injection on retry
|
|
455
457
|
this.#pendingTtsrInjections.push(...matches);
|
|
456
|
-
//
|
|
458
|
+
// Abort the stream immediately — do not gate on extension callbacks
|
|
457
459
|
this.#ttsrAbortPending = true;
|
|
458
|
-
this.#emit({ type: "ttsr_triggered", rules: matches });
|
|
459
|
-
// Abort the stream
|
|
460
460
|
this.agent.abort();
|
|
461
|
+
// Notify extensions (fire-and-forget, does not block abort)
|
|
462
|
+
this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
|
|
461
463
|
// Schedule retry after a short delay
|
|
462
464
|
setTimeout(async () => {
|
|
463
465
|
this.#ttsrAbortPending = false;
|
|
@@ -525,8 +527,12 @@ export class AgentSession {
|
|
|
525
527
|
// Reset retry counter immediately on successful assistant response
|
|
526
528
|
// This prevents accumulation across multiple LLM calls within a turn
|
|
527
529
|
const assistantMsg = event.message as AssistantMessage;
|
|
528
|
-
if (
|
|
529
|
-
|
|
530
|
+
if (
|
|
531
|
+
assistantMsg.stopReason !== "error" &&
|
|
532
|
+
assistantMsg.stopReason !== "aborted" &&
|
|
533
|
+
this.#retryAttempt > 0
|
|
534
|
+
) {
|
|
535
|
+
await this.#emitSessionEvent({
|
|
530
536
|
type: "auto_retry_end",
|
|
531
537
|
success: true,
|
|
532
538
|
attempt: this.#retryAttempt,
|
|
@@ -537,11 +543,13 @@ export class AgentSession {
|
|
|
537
543
|
}
|
|
538
544
|
|
|
539
545
|
if (event.message.role === "toolResult") {
|
|
540
|
-
const { toolName, $normative, toolCallId, details } = event.message as {
|
|
546
|
+
const { toolName, $normative, toolCallId, details, isError, content } = event.message as {
|
|
541
547
|
toolName?: string;
|
|
542
548
|
toolCallId?: string;
|
|
543
549
|
details?: { path?: string };
|
|
544
550
|
$normative?: Record<string, unknown>;
|
|
551
|
+
isError?: boolean;
|
|
552
|
+
content?: Array<TextContent | ImageContent>;
|
|
545
553
|
};
|
|
546
554
|
if ($normative && toolCallId && this.settings.get("normativeRewrite")) {
|
|
547
555
|
await this.#rewriteToolCallArgs(toolCallId, $normative);
|
|
@@ -550,6 +558,25 @@ export class AgentSession {
|
|
|
550
558
|
if (toolName === "edit" && details?.path) {
|
|
551
559
|
this.#invalidateFileCacheForPath(details.path);
|
|
552
560
|
}
|
|
561
|
+
if (toolName === "todo_write" && isError) {
|
|
562
|
+
const errorText = content?.find(part => part.type === "text")?.text;
|
|
563
|
+
const reminderText = [
|
|
564
|
+
"<system_reminder>",
|
|
565
|
+
"todo_write failed, so todo progress is not visible to the user.",
|
|
566
|
+
errorText ? `Failure: ${errorText}` : "Failure: todo_write returned an error.",
|
|
567
|
+
"Fix the todo payload and call todo_write again before continuing.",
|
|
568
|
+
"</system_reminder>",
|
|
569
|
+
].join("\n");
|
|
570
|
+
await this.sendCustomMessage(
|
|
571
|
+
{
|
|
572
|
+
customType: "todo-write-error-reminder",
|
|
573
|
+
content: reminderText,
|
|
574
|
+
display: false,
|
|
575
|
+
details: { toolName, errorText },
|
|
576
|
+
},
|
|
577
|
+
{ deliverAs: "nextTurn" },
|
|
578
|
+
);
|
|
579
|
+
}
|
|
553
580
|
}
|
|
554
581
|
}
|
|
555
582
|
|
|
@@ -823,10 +850,9 @@ export class AgentSession {
|
|
|
823
850
|
}
|
|
824
851
|
}
|
|
825
852
|
|
|
826
|
-
/** Emit extension events based on
|
|
827
|
-
async #emitExtensionEvent(event:
|
|
853
|
+
/** Emit extension events based on session events */
|
|
854
|
+
async #emitExtensionEvent(event: AgentSessionEvent): Promise<void> {
|
|
828
855
|
if (!this.#extensionRunner) return;
|
|
829
|
-
|
|
830
856
|
if (event.type === "agent_start") {
|
|
831
857
|
this.#turnIndex = 0;
|
|
832
858
|
await this.#extensionRunner.emit({ type: "agent_start" });
|
|
@@ -848,6 +874,40 @@ export class AgentSession {
|
|
|
848
874
|
};
|
|
849
875
|
await this.#extensionRunner.emit(hookEvent);
|
|
850
876
|
this.#turnIndex++;
|
|
877
|
+
} else if (event.type === "auto_compaction_start") {
|
|
878
|
+
await this.#extensionRunner.emit({ type: "auto_compaction_start", reason: event.reason });
|
|
879
|
+
} else if (event.type === "auto_compaction_end") {
|
|
880
|
+
await this.#extensionRunner.emit({
|
|
881
|
+
type: "auto_compaction_end",
|
|
882
|
+
result: event.result,
|
|
883
|
+
aborted: event.aborted,
|
|
884
|
+
willRetry: event.willRetry,
|
|
885
|
+
errorMessage: event.errorMessage,
|
|
886
|
+
});
|
|
887
|
+
} else if (event.type === "auto_retry_start") {
|
|
888
|
+
await this.#extensionRunner.emit({
|
|
889
|
+
type: "auto_retry_start",
|
|
890
|
+
attempt: event.attempt,
|
|
891
|
+
maxAttempts: event.maxAttempts,
|
|
892
|
+
delayMs: event.delayMs,
|
|
893
|
+
errorMessage: event.errorMessage,
|
|
894
|
+
});
|
|
895
|
+
} else if (event.type === "auto_retry_end") {
|
|
896
|
+
await this.#extensionRunner.emit({
|
|
897
|
+
type: "auto_retry_end",
|
|
898
|
+
success: event.success,
|
|
899
|
+
attempt: event.attempt,
|
|
900
|
+
finalError: event.finalError,
|
|
901
|
+
});
|
|
902
|
+
} else if (event.type === "ttsr_triggered") {
|
|
903
|
+
await this.#extensionRunner.emit({ type: "ttsr_triggered", rules: event.rules });
|
|
904
|
+
} else if (event.type === "todo_reminder") {
|
|
905
|
+
await this.#extensionRunner.emit({
|
|
906
|
+
type: "todo_reminder",
|
|
907
|
+
todos: event.todos,
|
|
908
|
+
attempt: event.attempt,
|
|
909
|
+
maxAttempts: event.maxAttempts,
|
|
910
|
+
});
|
|
851
911
|
}
|
|
852
912
|
}
|
|
853
913
|
|
|
@@ -1342,6 +1402,7 @@ export class AgentSession {
|
|
|
1342
1402
|
options?: Pick<PromptOptions, "toolChoice" | "images">,
|
|
1343
1403
|
): Promise<void> {
|
|
1344
1404
|
this.#promptInFlight = true;
|
|
1405
|
+
const generation = this.#promptGeneration;
|
|
1345
1406
|
try {
|
|
1346
1407
|
// Flush any pending bash messages before the new prompt
|
|
1347
1408
|
this.#flushPendingBashMessages();
|
|
@@ -1387,6 +1448,12 @@ export class AgentSession {
|
|
|
1387
1448
|
|
|
1388
1449
|
messages.push(message);
|
|
1389
1450
|
|
|
1451
|
+
// Early bail-out: if a newer abort/prompt cycle started during setup,
|
|
1452
|
+
// return before mutating shared state (nextTurn messages, system prompt).
|
|
1453
|
+
if (this.#promptGeneration !== generation) {
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1390
1457
|
// Inject any pending "nextTurn" messages as context alongside the user message
|
|
1391
1458
|
for (const msg of this.#pendingNextTurnMessages) {
|
|
1392
1459
|
messages.push(msg);
|
|
@@ -1430,6 +1497,11 @@ export class AgentSession {
|
|
|
1430
1497
|
}
|
|
1431
1498
|
}
|
|
1432
1499
|
|
|
1500
|
+
// Bail out if a newer abort/prompt cycle has started since we began setup
|
|
1501
|
+
if (this.#promptGeneration !== generation) {
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1433
1505
|
const agentPromptOptions = options?.toolChoice ? { toolChoice: options.toolChoice } : undefined;
|
|
1434
1506
|
await this.agent.prompt(messages, agentPromptOptions);
|
|
1435
1507
|
await this.#waitForRetry();
|
|
@@ -1797,8 +1869,14 @@ export class AgentSession {
|
|
|
1797
1869
|
*/
|
|
1798
1870
|
async abort(): Promise<void> {
|
|
1799
1871
|
this.abortRetry();
|
|
1872
|
+
this.#promptGeneration++;
|
|
1800
1873
|
this.agent.abort();
|
|
1801
1874
|
await this.agent.waitForIdle();
|
|
1875
|
+
// Clear promptInFlight: waitForIdle resolves when the agent loop's finally
|
|
1876
|
+
// block runs (#resolveRunningPrompt), but #promptWithMessage's finally
|
|
1877
|
+
// (#promptInFlight = false) fires on a later microtask. Without this,
|
|
1878
|
+
// isStreaming stays true and a subsequent prompt() throws.
|
|
1879
|
+
this.#promptInFlight = false;
|
|
1802
1880
|
}
|
|
1803
1881
|
|
|
1804
1882
|
/**
|
|
@@ -2672,7 +2750,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2672
2750
|
});
|
|
2673
2751
|
|
|
2674
2752
|
// Emit event for UI to render notification
|
|
2675
|
-
this.#
|
|
2753
|
+
await this.#emitSessionEvent({
|
|
2676
2754
|
type: "todo_reminder",
|
|
2677
2755
|
todos: incomplete,
|
|
2678
2756
|
attempt: this.#todoReminderCount,
|
|
@@ -2743,7 +2821,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2743
2821
|
async #runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise<void> {
|
|
2744
2822
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
2745
2823
|
|
|
2746
|
-
this.#
|
|
2824
|
+
await this.#emitSessionEvent({ type: "auto_compaction_start", reason });
|
|
2747
2825
|
// Properly abort and null existing controller before replacing
|
|
2748
2826
|
if (this.#autoCompactionAbortController) {
|
|
2749
2827
|
this.#autoCompactionAbortController.abort();
|
|
@@ -2752,13 +2830,23 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2752
2830
|
|
|
2753
2831
|
try {
|
|
2754
2832
|
if (!this.model) {
|
|
2755
|
-
this.#
|
|
2833
|
+
await this.#emitSessionEvent({
|
|
2834
|
+
type: "auto_compaction_end",
|
|
2835
|
+
result: undefined,
|
|
2836
|
+
aborted: false,
|
|
2837
|
+
willRetry: false,
|
|
2838
|
+
});
|
|
2756
2839
|
return;
|
|
2757
2840
|
}
|
|
2758
2841
|
|
|
2759
2842
|
const availableModels = this.#modelRegistry.getAvailable();
|
|
2760
2843
|
if (availableModels.length === 0) {
|
|
2761
|
-
this.#
|
|
2844
|
+
await this.#emitSessionEvent({
|
|
2845
|
+
type: "auto_compaction_end",
|
|
2846
|
+
result: undefined,
|
|
2847
|
+
aborted: false,
|
|
2848
|
+
willRetry: false,
|
|
2849
|
+
});
|
|
2762
2850
|
return;
|
|
2763
2851
|
}
|
|
2764
2852
|
|
|
@@ -2766,7 +2854,12 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2766
2854
|
|
|
2767
2855
|
const preparation = prepareCompaction(pathEntries, compactionSettings);
|
|
2768
2856
|
if (!preparation) {
|
|
2769
|
-
this.#
|
|
2857
|
+
await this.#emitSessionEvent({
|
|
2858
|
+
type: "auto_compaction_end",
|
|
2859
|
+
result: undefined,
|
|
2860
|
+
aborted: false,
|
|
2861
|
+
willRetry: false,
|
|
2862
|
+
});
|
|
2770
2863
|
return;
|
|
2771
2864
|
}
|
|
2772
2865
|
|
|
@@ -2786,7 +2879,12 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2786
2879
|
})) as SessionBeforeCompactResult | undefined;
|
|
2787
2880
|
|
|
2788
2881
|
if (hookResult?.cancel) {
|
|
2789
|
-
this.#
|
|
2882
|
+
await this.#emitSessionEvent({
|
|
2883
|
+
type: "auto_compaction_end",
|
|
2884
|
+
result: undefined,
|
|
2885
|
+
aborted: true,
|
|
2886
|
+
willRetry: false,
|
|
2887
|
+
});
|
|
2790
2888
|
return;
|
|
2791
2889
|
}
|
|
2792
2890
|
|
|
@@ -2914,7 +3012,12 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2914
3012
|
}
|
|
2915
3013
|
|
|
2916
3014
|
if (this.#autoCompactionAbortController.signal.aborted) {
|
|
2917
|
-
this.#
|
|
3015
|
+
await this.#emitSessionEvent({
|
|
3016
|
+
type: "auto_compaction_end",
|
|
3017
|
+
result: undefined,
|
|
3018
|
+
aborted: true,
|
|
3019
|
+
willRetry: false,
|
|
3020
|
+
});
|
|
2918
3021
|
return;
|
|
2919
3022
|
}
|
|
2920
3023
|
|
|
@@ -2952,7 +3055,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2952
3055
|
details,
|
|
2953
3056
|
preserveData,
|
|
2954
3057
|
};
|
|
2955
|
-
this.#
|
|
3058
|
+
await this.#emitSessionEvent({ type: "auto_compaction_end", result, aborted: false, willRetry });
|
|
2956
3059
|
|
|
2957
3060
|
if (!willRetry && compactionSettings.autoContinue !== false) {
|
|
2958
3061
|
await this.prompt("Continue if you have next steps.", {
|
|
@@ -2980,11 +3083,16 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2980
3083
|
}
|
|
2981
3084
|
} catch (error) {
|
|
2982
3085
|
if (this.#autoCompactionAbortController?.signal.aborted) {
|
|
2983
|
-
this.#
|
|
3086
|
+
await this.#emitSessionEvent({
|
|
3087
|
+
type: "auto_compaction_end",
|
|
3088
|
+
result: undefined,
|
|
3089
|
+
aborted: true,
|
|
3090
|
+
willRetry: false,
|
|
3091
|
+
});
|
|
2984
3092
|
return;
|
|
2985
3093
|
}
|
|
2986
3094
|
const errorMessage = error instanceof Error ? error.message : "compaction failed";
|
|
2987
|
-
this.#
|
|
3095
|
+
await this.#emitSessionEvent({
|
|
2988
3096
|
type: "auto_compaction_end",
|
|
2989
3097
|
result: undefined,
|
|
2990
3098
|
aborted: false,
|
|
@@ -3106,7 +3214,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3106
3214
|
|
|
3107
3215
|
if (this.#retryAttempt > retrySettings.maxRetries) {
|
|
3108
3216
|
// Max retries exceeded, emit final failure and reset
|
|
3109
|
-
this.#
|
|
3217
|
+
await this.#emitSessionEvent({
|
|
3110
3218
|
type: "auto_retry_end",
|
|
3111
3219
|
success: false,
|
|
3112
3220
|
attempt: this.#retryAttempt - 1,
|
|
@@ -3135,7 +3243,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3135
3243
|
}
|
|
3136
3244
|
}
|
|
3137
3245
|
|
|
3138
|
-
this.#
|
|
3246
|
+
await this.#emitSessionEvent({
|
|
3139
3247
|
type: "auto_retry_start",
|
|
3140
3248
|
attempt: this.#retryAttempt,
|
|
3141
3249
|
maxAttempts: retrySettings.maxRetries,
|
|
@@ -3162,7 +3270,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3162
3270
|
const attempt = this.#retryAttempt;
|
|
3163
3271
|
this.#retryAttempt = 0;
|
|
3164
3272
|
this.#retryAbortController = undefined;
|
|
3165
|
-
this.#
|
|
3273
|
+
await this.#emitSessionEvent({
|
|
3166
3274
|
type: "auto_retry_end",
|
|
3167
3275
|
success: false,
|
|
3168
3276
|
attempt,
|
|
@@ -3932,7 +4040,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3932
4040
|
* @returns Path to exported file
|
|
3933
4041
|
*/
|
|
3934
4042
|
async exportToHtml(outputPath?: string): Promise<string> {
|
|
3935
|
-
const themeName =
|
|
4043
|
+
const themeName = getCurrentThemeName();
|
|
3936
4044
|
return exportSessionToHtml(this.sessionManager, this.state, { outputPath, themeName });
|
|
3937
4045
|
}
|
|
3938
4046
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { $ } from "bun";
|
|
3
|
+
import { resolvePython } from "./transcriber";
|
|
4
|
+
|
|
5
|
+
export interface DownloadProgress {
|
|
6
|
+
stage: string;
|
|
7
|
+
percent?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface EnsureOptions {
|
|
11
|
+
modelName?: string;
|
|
12
|
+
onProgress?: (progress: DownloadProgress) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ── Recording tool ─────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
async function ensureRecordingTool(options?: EnsureOptions): Promise<void> {
|
|
18
|
+
if (Bun.which("sox")) return;
|
|
19
|
+
if (Bun.which("ffmpeg")) return;
|
|
20
|
+
if (process.platform === "linux" && Bun.which("arecord")) return;
|
|
21
|
+
|
|
22
|
+
// Windows: PowerShell mciSendString is always available as fallback
|
|
23
|
+
if (process.platform === "win32") {
|
|
24
|
+
// Try to get ffmpeg for better quality, but don't block on failure
|
|
25
|
+
options?.onProgress?.({ stage: "Trying to install FFmpeg via winget..." });
|
|
26
|
+
const result = await $`winget install --id Gyan.FFmpeg -e --accept-source-agreements --accept-package-agreements`
|
|
27
|
+
.quiet()
|
|
28
|
+
.nothrow();
|
|
29
|
+
if (result.exitCode === 0) {
|
|
30
|
+
logger.debug("FFmpeg installed via winget");
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
throw new Error(
|
|
36
|
+
"No audio recording tool found. Install SoX: sudo apt install sox, or FFmpeg: sudo apt install ffmpeg",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Python whisper ─────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
async function ensurePythonWhisper(options?: EnsureOptions): Promise<void> {
|
|
43
|
+
const pythonCmd = resolvePython();
|
|
44
|
+
if (!pythonCmd) {
|
|
45
|
+
throw new Error("Python not found. Install Python 3.8+ from https://python.org");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check if whisper module is already importable
|
|
49
|
+
const check = Bun.spawnSync([pythonCmd, "-c", "import whisper"], {
|
|
50
|
+
stdout: "pipe",
|
|
51
|
+
stderr: "pipe",
|
|
52
|
+
});
|
|
53
|
+
if (check.exitCode === 0) return;
|
|
54
|
+
|
|
55
|
+
options?.onProgress?.({ stage: "Installing openai-whisper (this may take a few minutes)..." });
|
|
56
|
+
logger.debug("Installing openai-whisper via pip");
|
|
57
|
+
|
|
58
|
+
const install = await $`${pythonCmd} -m pip install -q openai-whisper`.quiet().nothrow();
|
|
59
|
+
if (install.exitCode !== 0) {
|
|
60
|
+
const stderr = install.stderr.toString().trim();
|
|
61
|
+
throw new Error(`Failed to install openai-whisper: ${stderr.split("\n").pop()}`);
|
|
62
|
+
}
|
|
63
|
+
logger.debug("openai-whisper installed successfully");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Public API ─────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
export async function ensureSTTDependencies(options?: EnsureOptions): Promise<void> {
|
|
69
|
+
await ensureRecordingTool(options);
|
|
70
|
+
await ensurePythonWhisper(options);
|
|
71
|
+
}
|
package/src/stt/index.ts
ADDED