@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/task/render.ts
CHANGED
|
@@ -47,6 +47,12 @@ interface TaskRenderContext {
|
|
|
47
47
|
}
|
|
48
48
|
type TaskRenderOptions = RenderResultOptions & { renderContext?: TaskRenderContext };
|
|
49
49
|
|
|
50
|
+
const MAX_NESTED_TASK_RENDER_DEPTH = 8;
|
|
51
|
+
|
|
52
|
+
function renderNestedCycleLine(theme: Theme): string {
|
|
53
|
+
return theme.fg("dim", "… nested task progress already shown");
|
|
54
|
+
}
|
|
55
|
+
|
|
50
56
|
/**
|
|
51
57
|
* Get status icon for agent state.
|
|
52
58
|
* For running status, uses animated spinner if spinnerFrame is provided.
|
|
@@ -687,6 +693,8 @@ function renderAgentProgress(
|
|
|
687
693
|
theme: Theme,
|
|
688
694
|
spinnerFrame?: number,
|
|
689
695
|
frozen = false,
|
|
696
|
+
seenNestedTasks?: WeakSet<object>,
|
|
697
|
+
nestedDepth = 0,
|
|
690
698
|
): string[] {
|
|
691
699
|
const lines: string[] = [];
|
|
692
700
|
|
|
@@ -859,7 +867,15 @@ function renderAgentProgress(
|
|
|
859
867
|
const inflight = progress.inflightTaskDetails;
|
|
860
868
|
if (completedTaskCalls.length > 0 || inflight) {
|
|
861
869
|
const snapshots = inflight ? [...completedTaskCalls, inflight] : completedTaskCalls;
|
|
862
|
-
const nestedLines = renderNestedTaskTree(
|
|
870
|
+
const nestedLines = renderNestedTaskTree(
|
|
871
|
+
snapshots,
|
|
872
|
+
expanded,
|
|
873
|
+
theme,
|
|
874
|
+
spinnerFrame,
|
|
875
|
+
frozen,
|
|
876
|
+
seenNestedTasks,
|
|
877
|
+
nestedDepth,
|
|
878
|
+
);
|
|
863
879
|
for (const line of nestedLines) {
|
|
864
880
|
lines.push(`${continuePrefix}${line}`);
|
|
865
881
|
}
|
|
@@ -984,6 +1000,8 @@ function renderAgentResult(
|
|
|
984
1000
|
continuePrefix: string,
|
|
985
1001
|
expanded: boolean,
|
|
986
1002
|
theme: Theme,
|
|
1003
|
+
seenNestedTasks?: WeakSet<object>,
|
|
1004
|
+
nestedDepth = 0,
|
|
987
1005
|
): string[] {
|
|
988
1006
|
const lines: string[] = [];
|
|
989
1007
|
|
|
@@ -1088,11 +1106,24 @@ function renderAgentResult(
|
|
|
1088
1106
|
// Skip review tools - handled above
|
|
1089
1107
|
if (toolName === "yield" || toolName === "report_finding") continue;
|
|
1090
1108
|
|
|
1109
|
+
const isTaskTool = toolName === "task";
|
|
1110
|
+
if (isTaskTool && (dataArray as unknown[]).length > 0) {
|
|
1111
|
+
for (const line of renderNestedTaskResults(
|
|
1112
|
+
dataArray as TaskToolDetails[],
|
|
1113
|
+
expanded,
|
|
1114
|
+
theme,
|
|
1115
|
+
seenNestedTasks,
|
|
1116
|
+
nestedDepth,
|
|
1117
|
+
)) {
|
|
1118
|
+
deferredToolLines.push(`${continuePrefix}${line}`);
|
|
1119
|
+
}
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1091
1123
|
const handler = subprocessToolRegistry.getHandler(toolName);
|
|
1092
1124
|
if (handler?.renderFinal && (dataArray as unknown[]).length > 0) {
|
|
1093
|
-
const isTaskTool = toolName === "task";
|
|
1094
1125
|
const component = handler.renderFinal(dataArray as unknown[], theme, expanded);
|
|
1095
|
-
const target =
|
|
1126
|
+
const target = lines;
|
|
1096
1127
|
if (!isTaskTool) {
|
|
1097
1128
|
hasCustomRendering = true;
|
|
1098
1129
|
target.push(`${continuePrefix}${theme.fg("dim", `Tool: ${toolName}`)}`);
|
|
@@ -1417,15 +1448,34 @@ function nestedMarkers(isLast: boolean, theme: Theme): { prefix: string; continu
|
|
|
1417
1448
|
};
|
|
1418
1449
|
}
|
|
1419
1450
|
|
|
1420
|
-
function renderNestedTaskResults(
|
|
1451
|
+
function renderNestedTaskResults(
|
|
1452
|
+
detailsList: TaskToolDetails[],
|
|
1453
|
+
expanded: boolean,
|
|
1454
|
+
theme: Theme,
|
|
1455
|
+
seen: WeakSet<object> = new WeakSet<object>(),
|
|
1456
|
+
depth = 0,
|
|
1457
|
+
): string[] {
|
|
1421
1458
|
const lines: string[] = [];
|
|
1422
1459
|
for (const details of detailsList) {
|
|
1423
|
-
if (
|
|
1460
|
+
if (seen.has(details)) {
|
|
1461
|
+
lines.push(renderNestedCycleLine(theme));
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
if (depth >= MAX_NESTED_TASK_RENDER_DEPTH) {
|
|
1465
|
+
lines.push(theme.fg("dim", "… nested task depth limit reached"));
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
seen.add(details);
|
|
1469
|
+
if (!details.results || details.results.length === 0) {
|
|
1470
|
+
seen.delete(details);
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1424
1473
|
const ordered = orderResultsForDisplay(details.results);
|
|
1425
1474
|
ordered.forEach((result, index) => {
|
|
1426
1475
|
const { prefix, continuePrefix } = nestedMarkers(index === ordered.length - 1, theme);
|
|
1427
|
-
lines.push(...renderAgentResult(result, prefix, continuePrefix, expanded, theme));
|
|
1476
|
+
lines.push(...renderAgentResult(result, prefix, continuePrefix, expanded, theme, seen, depth + 1));
|
|
1428
1477
|
});
|
|
1478
|
+
seen.delete(details);
|
|
1429
1479
|
}
|
|
1430
1480
|
return lines;
|
|
1431
1481
|
}
|
|
@@ -1441,16 +1491,28 @@ function renderNestedTaskTree(
|
|
|
1441
1491
|
theme: Theme,
|
|
1442
1492
|
spinnerFrame?: number,
|
|
1443
1493
|
frozen = false,
|
|
1494
|
+
seen: WeakSet<object> = new WeakSet<object>(),
|
|
1495
|
+
depth = 0,
|
|
1444
1496
|
): string[] {
|
|
1445
1497
|
const lines: string[] = [];
|
|
1446
1498
|
for (const details of detailsList) {
|
|
1499
|
+
if (seen.has(details)) {
|
|
1500
|
+
lines.push(renderNestedCycleLine(theme));
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
if (depth >= MAX_NESTED_TASK_RENDER_DEPTH) {
|
|
1504
|
+
lines.push(theme.fg("dim", "… nested task depth limit reached"));
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
seen.add(details);
|
|
1447
1508
|
const hasResults = Boolean(details.results && details.results.length > 0);
|
|
1448
1509
|
if (hasResults) {
|
|
1449
1510
|
const ordered = orderResultsForDisplay(details.results);
|
|
1450
1511
|
ordered.forEach((result, index) => {
|
|
1451
1512
|
const { prefix, continuePrefix } = nestedMarkers(index === ordered.length - 1, theme);
|
|
1452
|
-
lines.push(...renderAgentResult(result, prefix, continuePrefix, expanded, theme));
|
|
1513
|
+
lines.push(...renderAgentResult(result, prefix, continuePrefix, expanded, theme, seen, depth + 1));
|
|
1453
1514
|
});
|
|
1515
|
+
seen.delete(details);
|
|
1454
1516
|
continue;
|
|
1455
1517
|
}
|
|
1456
1518
|
const inflight = details.progress;
|
|
@@ -1458,9 +1520,22 @@ function renderNestedTaskTree(
|
|
|
1458
1520
|
const ordered = orderProgressForDisplay(inflight);
|
|
1459
1521
|
ordered.forEach((prog, index) => {
|
|
1460
1522
|
const { prefix, continuePrefix } = nestedMarkers(index === ordered.length - 1, theme);
|
|
1461
|
-
lines.push(
|
|
1523
|
+
lines.push(
|
|
1524
|
+
...renderAgentProgress(
|
|
1525
|
+
prog,
|
|
1526
|
+
prefix,
|
|
1527
|
+
continuePrefix,
|
|
1528
|
+
expanded,
|
|
1529
|
+
theme,
|
|
1530
|
+
spinnerFrame,
|
|
1531
|
+
frozen,
|
|
1532
|
+
seen,
|
|
1533
|
+
depth + 1,
|
|
1534
|
+
),
|
|
1535
|
+
);
|
|
1462
1536
|
});
|
|
1463
1537
|
}
|
|
1538
|
+
seen.delete(details);
|
|
1464
1539
|
}
|
|
1465
1540
|
return lines;
|
|
1466
1541
|
}
|
package/src/task/types.ts
CHANGED
|
@@ -74,6 +74,11 @@ export interface SubagentLifecyclePayload {
|
|
|
74
74
|
detached?: boolean;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/** Display cap for a normalized one-line label (roster line, registry `displayName`, prompt field). */
|
|
78
|
+
export const ROLE_LABEL_MAX = 80;
|
|
79
|
+
/** Schema bound on the raw `role` input, before it is label-normalized at every use site. */
|
|
80
|
+
export const ROLE_INPUT_MAX = 256;
|
|
81
|
+
|
|
77
82
|
/**
|
|
78
83
|
* One unit of work. The single-spawn schema is `{ agent, ...taskItemSchema }`;
|
|
79
84
|
* the batch schema (`task.batch`) is `{ agent, context, tasks: taskItemSchema[] }`.
|
|
@@ -83,6 +88,13 @@ export interface SubagentLifecyclePayload {
|
|
|
83
88
|
const taskItemShape = {
|
|
84
89
|
id: z.string().max(48).optional().describe("stable agent id; default generated"),
|
|
85
90
|
description: z.string().optional().describe("ui label, not seen by subagent"),
|
|
91
|
+
role: z
|
|
92
|
+
.string()
|
|
93
|
+
.max(ROLE_INPUT_MAX)
|
|
94
|
+
.optional()
|
|
95
|
+
.describe(
|
|
96
|
+
"specialist role/expertise this subagent embodies (e.g. 'Rust async-runtime specialist'); shapes its identity and display name",
|
|
97
|
+
),
|
|
86
98
|
assignment: z.string().describe("the work; self-contained instructions"),
|
|
87
99
|
};
|
|
88
100
|
const isolatedShape = {
|
|
@@ -104,6 +116,8 @@ export interface TaskItem {
|
|
|
104
116
|
id?: string;
|
|
105
117
|
/** UI label, not seen by the subagent. */
|
|
106
118
|
description?: string;
|
|
119
|
+
/** Specialist role/expertise this subagent embodies; shapes its system-prompt identity and display name. */
|
|
120
|
+
role?: string;
|
|
107
121
|
/** The work; required by the schema. */
|
|
108
122
|
assignment?: string;
|
|
109
123
|
/** Run this spawn in an isolated worktree (batch form; flat form carries it top-level). */
|
|
@@ -149,6 +163,8 @@ export interface TaskParams {
|
|
|
149
163
|
id?: string;
|
|
150
164
|
/** UI label (flat form), not seen by the subagent. */
|
|
151
165
|
description?: string;
|
|
166
|
+
/** Specialist role/expertise this subagent embodies; shapes its system-prompt identity and display name. */
|
|
167
|
+
role?: string;
|
|
152
168
|
/** The work (flat form). */
|
|
153
169
|
assignment?: string;
|
|
154
170
|
/** Batch form (`task.batch`): one subagent per item. */
|
|
@@ -159,6 +175,43 @@ export interface TaskParams {
|
|
|
159
175
|
isolated?: boolean;
|
|
160
176
|
}
|
|
161
177
|
|
|
178
|
+
/**
|
|
179
|
+
* One-line, length-capped label safe for a single roster line, a registry
|
|
180
|
+
* `displayName`, or a system-prompt field. Collapses every run of whitespace
|
|
181
|
+
* AND control/format characters — including U+0085 NEL, ESC/ANSI, and the
|
|
182
|
+
* zero-width separators that `\s` misses — to a single space, then caps length.
|
|
183
|
+
* So untrusted text (a spawn `role`, a peer activity gist) can neither break the
|
|
184
|
+
* line, inject prompt structure, nor smuggle terminal escapes. Caps at `max`
|
|
185
|
+
* characters (clamped to >= 1; default `ROLE_LABEL_MAX`), appending an ellipsis when truncated.
|
|
186
|
+
*/
|
|
187
|
+
export function oneLineLabel(text: string, max = ROLE_LABEL_MAX): string {
|
|
188
|
+
const oneLine = text.replace(/[\p{Cc}\p{Cf}\s]+/gu, " ").trim();
|
|
189
|
+
const cap = Math.max(1, max);
|
|
190
|
+
// Count/cut by code point, not UTF-16 code unit, so truncation can never
|
|
191
|
+
// split an astral character into a lone surrogate.
|
|
192
|
+
const chars = [...oneLine];
|
|
193
|
+
return chars.length > cap ? `${chars.slice(0, cap - 1).join("")}…` : oneLine;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Display name for a spawned subagent: its tailored `role` (label-normalized)
|
|
198
|
+
* when one is given, else the agent type's name. Empty/whitespace roles fall
|
|
199
|
+
* back to the agent name.
|
|
200
|
+
*/
|
|
201
|
+
export function resolveSubagentDisplayName(role: string | undefined, agentName: string): string {
|
|
202
|
+
const trimmed = role?.trim();
|
|
203
|
+
return trimmed ? oneLineLabel(trimmed) : agentName;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Whether an agent at `taskDepth` may still spawn children — i.e. it currently
|
|
208
|
+
* holds the `task` tool. Mirrors the task-tool availability gate;
|
|
209
|
+
* `maxRecursionDepth < 0` disables the cap entirely.
|
|
210
|
+
*/
|
|
211
|
+
export function canSpawnAtDepth(maxRecursionDepth: number, taskDepth: number): boolean {
|
|
212
|
+
return maxRecursionDepth < 0 || taskDepth < maxRecursionDepth;
|
|
213
|
+
}
|
|
214
|
+
|
|
162
215
|
/** A code review finding reported by the reviewer agent */
|
|
163
216
|
export interface ReviewFinding {
|
|
164
217
|
title: string;
|
package/src/tools/ask.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
|
23
23
|
import type { ExtensionUISelectItem } from "../extensibility/extensions";
|
|
24
24
|
import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
|
|
25
25
|
import askDescription from "../prompts/tools/ask.md" with { type: "text" };
|
|
26
|
+
import { vocalizer } from "../tts/vocalizer";
|
|
26
27
|
import { framedBlock, renderStatusLine } from "../tui";
|
|
27
28
|
import type { ToolSession } from ".";
|
|
28
29
|
import { formatErrorMessage, formatMeta, formatTitle } from "./render-utils";
|
|
@@ -487,6 +488,13 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
487
488
|
};
|
|
488
489
|
}
|
|
489
490
|
|
|
491
|
+
// Speak the question(s) aloud before surfacing them. Ask vocalizes in every
|
|
492
|
+
// mode — it's the assistant addressing the user — gated only by speech.enabled
|
|
493
|
+
// (the vocalizer re-checks the setting and no-ops when disabled).
|
|
494
|
+
if (this.session.settings.get("speech.enabled")) {
|
|
495
|
+
vocalizer.speak(params.questions.map(q => q.question).join("\n"));
|
|
496
|
+
}
|
|
497
|
+
|
|
490
498
|
const askQuestion = async (
|
|
491
499
|
q: AskParams["questions"][number],
|
|
492
500
|
options?: { previous?: QuestionResult; navigation?: NavigationControls },
|
package/src/tools/bash.ts
CHANGED
|
@@ -1385,9 +1385,10 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1385
1385
|
},
|
|
1386
1386
|
mergeCallAndResult: true,
|
|
1387
1387
|
inline: true,
|
|
1388
|
-
//
|
|
1389
|
-
// shifts while args stream
|
|
1390
|
-
|
|
1388
|
+
// Collapsed pending preview caps the command to a viewport-sized tail
|
|
1389
|
+
// window that shifts while args stream. Expanded output is top-anchored
|
|
1390
|
+
// enough for the transcript to commit its settled prefix.
|
|
1391
|
+
provisionalPendingPreview: "collapsed",
|
|
1391
1392
|
};
|
|
1392
1393
|
}
|
|
1393
1394
|
|
package/src/tools/eval-render.ts
CHANGED
|
@@ -754,8 +754,9 @@ export const evalToolRenderer = {
|
|
|
754
754
|
|
|
755
755
|
mergeCallAndResult: true,
|
|
756
756
|
inline: true,
|
|
757
|
-
//
|
|
757
|
+
// Collapsed pending preview shows tail-window code cells; the result render
|
|
758
758
|
// interleaves each cell's output under its code, re-laying-out every row
|
|
759
|
-
// below the first cell.
|
|
760
|
-
|
|
759
|
+
// below the first cell. Expanded output is top-anchored enough for the
|
|
760
|
+
// transcript to commit its settled prefix.
|
|
761
|
+
provisionalPendingPreview: "collapsed",
|
|
761
762
|
};
|
package/src/tools/index.ts
CHANGED
|
@@ -22,9 +22,11 @@ import type { AgentRegistry } from "../registry/agent-registry";
|
|
|
22
22
|
import type { ArtifactManager } from "../session/artifacts";
|
|
23
23
|
import type { ClientBridge } from "../session/client-bridge";
|
|
24
24
|
import type { CustomMessage } from "../session/messages";
|
|
25
|
+
import type { UsageStatistics } from "../session/session-entries";
|
|
25
26
|
import type { ToolChoiceQueue } from "../session/tool-choice-queue";
|
|
26
27
|
import { TaskTool } from "../task";
|
|
27
28
|
import type { AgentOutputManager } from "../task/output-manager";
|
|
29
|
+
import { canSpawnAtDepth } from "../task/types";
|
|
28
30
|
import { countToolsForAutoDiscovery, resolveEffectiveToolDiscoveryMode } from "../tool-discovery/mode";
|
|
29
31
|
import type { DiscoverableTool, DiscoverableToolSearchIndex } from "../tool-discovery/tool-index";
|
|
30
32
|
import type { EventBus } from "../utils/event-bus";
|
|
@@ -44,6 +46,8 @@ import { GithubTool } from "./gh";
|
|
|
44
46
|
import { InspectImageTool } from "./inspect-image";
|
|
45
47
|
import { IrcTool, isIrcEnabled } from "./irc";
|
|
46
48
|
import { JobTool } from "./job";
|
|
49
|
+
import { LearnTool } from "./learn";
|
|
50
|
+
import { ManageSkillTool } from "./manage-skill";
|
|
47
51
|
import { MemoryEditTool } from "./memory-edit";
|
|
48
52
|
import { MemoryRecallTool } from "./memory-recall";
|
|
49
53
|
import { MemoryReflectTool } from "./memory-reflect";
|
|
@@ -82,6 +86,8 @@ export * from "./image-gen";
|
|
|
82
86
|
export * from "./inspect-image";
|
|
83
87
|
export * from "./irc";
|
|
84
88
|
export * from "./job";
|
|
89
|
+
export * from "./learn";
|
|
90
|
+
export * from "./manage-skill";
|
|
85
91
|
export * from "./memory-edit";
|
|
86
92
|
export * from "./memory-recall";
|
|
87
93
|
export * from "./memory-reflect";
|
|
@@ -144,6 +150,13 @@ export interface ToolSession {
|
|
|
144
150
|
cwd: string;
|
|
145
151
|
/** Whether UI is available */
|
|
146
152
|
hasUI: boolean;
|
|
153
|
+
/**
|
|
154
|
+
* Suppress the spawn specialization/coordination advisory appended to `task`
|
|
155
|
+
* results. Set by internal/programmatic callers (e.g. the commit agent's
|
|
156
|
+
* file-analysis fan-out) whose results are consumed by code — not by a model
|
|
157
|
+
* orchestrating further spawns — so the nudge would only be noise.
|
|
158
|
+
*/
|
|
159
|
+
suppressSpawnAdvisory?: boolean;
|
|
147
160
|
/** Optional fetch implementation injected into the URL read pipeline (tests, proxies). Defaults to global fetch. */
|
|
148
161
|
fetch?: FetchImpl;
|
|
149
162
|
/** Skip Python kernel availability check and warmup */
|
|
@@ -255,7 +268,7 @@ export interface ToolSession {
|
|
|
255
268
|
/** Goal runtime for the active agent session. */
|
|
256
269
|
getGoalRuntime?: () => GoalRuntime | undefined;
|
|
257
270
|
/** Get cumulative session usage statistics (input/output tokens, cost). */
|
|
258
|
-
getUsageStatistics?: () =>
|
|
271
|
+
getUsageStatistics?: () => UsageStatistics;
|
|
259
272
|
/** Current per-turn token budget {total, spent, hard} for the eval `budget` helper. */
|
|
260
273
|
getTurnBudget?: () => { total: number | null; spent: number; hard: boolean };
|
|
261
274
|
/** Record output tokens consumed by an eval-spawned subagent toward the current turn budget. */
|
|
@@ -430,6 +443,8 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
430
443
|
retain: MemoryRetainTool.createIf,
|
|
431
444
|
recall: MemoryRecallTool.createIf,
|
|
432
445
|
reflect: MemoryReflectTool.createIf,
|
|
446
|
+
learn: LearnTool.createIf,
|
|
447
|
+
manage_skill: ManageSkillTool.createIf,
|
|
433
448
|
};
|
|
434
449
|
|
|
435
450
|
export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
|
|
@@ -510,6 +525,21 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
510
525
|
if (!requestedTools.includes(name)) requestedTools.push(name);
|
|
511
526
|
}
|
|
512
527
|
}
|
|
528
|
+
// Auto-learn tools are gated by `autolearn.enabled` but, like the memory
|
|
529
|
+
// tools above, must also be force-included into an explicit requestedTools
|
|
530
|
+
// list so a restricted top-level session whose controller/guidance is
|
|
531
|
+
// active still exposes the tools the nudge points at. Gated to top-level
|
|
532
|
+
// (taskDepth 0): the controller only runs there, so a subagent's explicit
|
|
533
|
+
// tool whitelist must never be silently widened with write-capable tools.
|
|
534
|
+
if (session.settings.get("autolearn.enabled") && (session.taskDepth ?? 0) === 0) {
|
|
535
|
+
if (!requestedTools.includes("manage_skill")) requestedTools.push("manage_skill");
|
|
536
|
+
if (
|
|
537
|
+
["hindsight", "mnemopi", "local"].includes(session.settings.get("memory.backend") ?? "") &&
|
|
538
|
+
!requestedTools.includes("learn")
|
|
539
|
+
) {
|
|
540
|
+
requestedTools.push("learn");
|
|
541
|
+
}
|
|
542
|
+
}
|
|
513
543
|
}
|
|
514
544
|
// Resolve effective tool discovery mode.
|
|
515
545
|
// tools.discoveryMode controls the new modes; mcp.discoveryMode remains a back-compat alias for "mcp-only".
|
|
@@ -543,10 +573,16 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
543
573
|
if (name === "retain" || name === "recall" || name === "reflect") {
|
|
544
574
|
return ["hindsight", "mnemopi"].includes(session.settings.get("memory.backend") ?? "");
|
|
545
575
|
}
|
|
576
|
+
if (name === "manage_skill") return session.settings.get("autolearn.enabled") && (session.taskDepth ?? 0) === 0;
|
|
577
|
+
if (name === "learn") {
|
|
578
|
+
return (
|
|
579
|
+
session.settings.get("autolearn.enabled") &&
|
|
580
|
+
(session.taskDepth ?? 0) === 0 &&
|
|
581
|
+
["hindsight", "mnemopi", "local"].includes(session.settings.get("memory.backend") ?? "")
|
|
582
|
+
);
|
|
583
|
+
}
|
|
546
584
|
if (name === "task") {
|
|
547
|
-
|
|
548
|
-
const currentDepth = session.taskDepth ?? 0;
|
|
549
|
-
return maxDepth < 0 || currentDepth < maxDepth;
|
|
585
|
+
return canSpawnAtDepth(session.settings.get("task.maxRecursionDepth") ?? 2, session.taskDepth ?? 0);
|
|
550
586
|
}
|
|
551
587
|
return true;
|
|
552
588
|
};
|
package/src/tools/irc.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { IrcBus, type IrcDeliveryReceipt, type IrcMessage } from "../irc/bus";
|
|
|
19
19
|
import type { Theme } from "../modes/theme/theme";
|
|
20
20
|
import ircDescription from "../prompts/tools/irc.md" with { type: "text" };
|
|
21
21
|
import type { AgentRegistry } from "../registry/agent-registry";
|
|
22
|
+
import { canSpawnAtDepth } from "../task/types";
|
|
22
23
|
import { Ellipsis, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
23
24
|
import type { ToolSession } from ".";
|
|
24
25
|
import {
|
|
@@ -41,8 +42,10 @@ const DEFAULT_IRC_TIMEOUT_MS = 120_000;
|
|
|
41
42
|
*/
|
|
42
43
|
export function isIrcEnabled(settings: Settings, taskDepth: number): boolean {
|
|
43
44
|
if (taskDepth > 0) return true;
|
|
45
|
+
// Top-level session: peers exist only if it can still spawn subagents — the
|
|
46
|
+
// same capacity gate the task tool uses, reused here to avoid drift.
|
|
44
47
|
const maxDepth = settings.get("task.maxRecursionDepth") ?? 2;
|
|
45
|
-
return maxDepth
|
|
48
|
+
return canSpawnAtDepth(maxDepth, taskDepth);
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
const ircSchema = z.object({
|
|
@@ -66,6 +69,7 @@ interface IrcPeerInfo {
|
|
|
66
69
|
parentId?: string;
|
|
67
70
|
unread: number;
|
|
68
71
|
lastActivity: number;
|
|
72
|
+
activity?: string;
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
export interface IrcDetails {
|
|
@@ -146,6 +150,7 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
|
146
150
|
parentId: ref.parentId,
|
|
147
151
|
unread: bus.unreadCount(ref.id),
|
|
148
152
|
lastActivity: ref.lastActivity,
|
|
153
|
+
activity: ref.activity,
|
|
149
154
|
}));
|
|
150
155
|
const lines: string[] = [];
|
|
151
156
|
if (peers.length === 0) {
|
|
@@ -154,6 +159,7 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
|
154
159
|
lines.push(`${peers.length} peer(s):`);
|
|
155
160
|
for (const peer of peers) {
|
|
156
161
|
const extras = [
|
|
162
|
+
peer.activity || undefined,
|
|
157
163
|
peer.unread > 0 ? `unread ${peer.unread}` : undefined,
|
|
158
164
|
peer.parentId ? `parent ${peer.parentId}` : undefined,
|
|
159
165
|
`active ${formatDuration(Date.now() - peer.lastActivity)} ago`,
|
|
@@ -673,7 +679,9 @@ function renderListResult(details: Partial<IrcDetails>, expanded: boolean, theme
|
|
|
673
679
|
const kindText = peer.parentId ? `${peer.kind}${theme.sep.dot}of ${peer.parentId}` : peer.kind;
|
|
674
680
|
const unread = peer.unread > 0 ? ` ${formatBadge(`${peer.unread} unread`, "warning", theme)}` : "";
|
|
675
681
|
const age = messageAge(peer.lastActivity);
|
|
676
|
-
|
|
682
|
+
const activity = peer.activity ? ` ${theme.fg("dim", replaceTabs(peer.activity))}` : "";
|
|
683
|
+
const name = theme.fg("dim", replaceTabs(peer.displayName));
|
|
684
|
+
return `${peerStatusBadge(peer.status, theme)} ${theme.bold(replaceTabs(peer.id))} ${name} ${theme.fg("dim", kindText)}${activity}${unread}${age ? ` ${theme.fg("dim", age)}` : ""}`;
|
|
677
685
|
},
|
|
678
686
|
},
|
|
679
687
|
theme,
|
package/src/tools/job.ts
CHANGED
|
@@ -184,9 +184,16 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
184
184
|
return this.#buildResult(manager, [...cancelledJobs, ...jobsToWatch], cancelOutcomes);
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
// Wait until at least one running job finishes, the wait
|
|
187
|
+
// Wait until at least one running job finishes, the wait window elapses,
|
|
188
|
+
// or the call is aborted. With `async.pollWaitDuration` set to `smart`,
|
|
189
|
+
// the window adapts: it starts at the ladder floor and climbs as the agent
|
|
190
|
+
// polls in a tight loop, then resets to the floor once the agent steps
|
|
191
|
+
// away from polling (see AsyncJobManager.nextPollWaitMs). Any fixed value
|
|
192
|
+
// waits that exact duration every time.
|
|
188
193
|
const racePromises: Promise<unknown>[] = runningJobs.map(j => j.promise);
|
|
189
|
-
const
|
|
194
|
+
const pollSetting = this.session.settings.get("async.pollWaitDuration");
|
|
195
|
+
const smartPoll = pollSetting === "smart";
|
|
196
|
+
const waitMs = smartPoll ? manager.nextPollWaitMs(ownerId) : parseWaitDurationMs(pollSetting);
|
|
190
197
|
const { promise: timeoutPromise, resolve: timeoutResolve } = Promise.withResolvers<void>();
|
|
191
198
|
const timeoutHandle = setTimeout(() => timeoutResolve(), waitMs);
|
|
192
199
|
racePromises.push(timeoutPromise);
|
|
@@ -232,6 +239,11 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
232
239
|
manager.unwatchJobs(watchedJobIds);
|
|
233
240
|
clearTimeout(timeoutHandle);
|
|
234
241
|
if (progressTimer) clearInterval(progressTimer);
|
|
242
|
+
if (smartPoll) {
|
|
243
|
+
// Reset the idle-gap clock: escalate if the agent polls again soon,
|
|
244
|
+
// drop back to the floor once it goes quiet for a while.
|
|
245
|
+
manager.recordPollWaitEnd(ownerId);
|
|
246
|
+
}
|
|
235
247
|
}
|
|
236
248
|
|
|
237
249
|
return this.#buildResult(manager, allTrackedJobs, cancelOutcomes);
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
|
+
import { sanitizeSkillName, writeManagedSkill } from "../autolearn/managed-skills";
|
|
4
|
+
import { isNameClaimedByAuthoredSkill } from "../extensibility/skills";
|
|
5
|
+
import { localBackend } from "../memory-backend/local-backend";
|
|
6
|
+
import learnDescription from "../prompts/tools/learn.md" with { type: "text" };
|
|
7
|
+
import type { ToolSession } from ".";
|
|
8
|
+
|
|
9
|
+
const learnSchema = z.object({
|
|
10
|
+
memory: z.string().describe("the durable, self-contained lesson to remember (what, when, why)"),
|
|
11
|
+
context: z.string().describe("optional source context for the lesson").optional(),
|
|
12
|
+
skill: z
|
|
13
|
+
.object({
|
|
14
|
+
action: z.enum(["create", "update"]),
|
|
15
|
+
name: z.string().describe("kebab-case skill name"),
|
|
16
|
+
description: z.string().describe("one-line description of when to use the skill"),
|
|
17
|
+
body: z.string().describe("the SKILL.md body in markdown (no frontmatter)"),
|
|
18
|
+
})
|
|
19
|
+
.describe("also create or enhance a managed skill in the same call")
|
|
20
|
+
.optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export type LearnParams = z.infer<typeof learnSchema>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Orchestrating "learn" tool: persists a lesson to long-term memory and,
|
|
27
|
+
* given a `skill` payload, mints/enhances a managed skill via the shared
|
|
28
|
+
* `writeManagedSkill` primitive. Gated behind `autolearn.enabled` plus a live
|
|
29
|
+
* memory backend — `hindsight`/`mnemopi` (remote/SQLite) or `local` (the
|
|
30
|
+
* file-based rollout backend, where lessons append to `learned.md`).
|
|
31
|
+
*/
|
|
32
|
+
export class LearnTool implements AgentTool<typeof learnSchema> {
|
|
33
|
+
readonly name = "learn";
|
|
34
|
+
readonly approval = (args: unknown) =>
|
|
35
|
+
(args as Partial<LearnParams>).skill || this.session.settings.get("memory.backend") === "local"
|
|
36
|
+
? "write"
|
|
37
|
+
: "read";
|
|
38
|
+
readonly label = "Learn";
|
|
39
|
+
readonly description = learnDescription;
|
|
40
|
+
readonly parameters = learnSchema;
|
|
41
|
+
readonly strict = true;
|
|
42
|
+
readonly loadMode = "essential" as const;
|
|
43
|
+
readonly summary = "Capture a reusable lesson to memory (and optionally a managed skill)";
|
|
44
|
+
|
|
45
|
+
constructor(private readonly session: ToolSession) {}
|
|
46
|
+
|
|
47
|
+
static createIf(session: ToolSession): LearnTool | null {
|
|
48
|
+
if (!session.settings.get("autolearn.enabled")) return null;
|
|
49
|
+
const backend = session.settings.get("memory.backend");
|
|
50
|
+
if (backend !== "hindsight" && backend !== "mnemopi" && backend !== "local") return null;
|
|
51
|
+
return new LearnTool(session);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async execute(_id: string, params: LearnParams): Promise<AgentToolResult> {
|
|
55
|
+
// 1) Persist or queue the lesson to long-term memory (mirrors MemoryRetainTool).
|
|
56
|
+
const backend = this.session.settings.get("memory.backend");
|
|
57
|
+
let memoryMessage = "Lesson stored";
|
|
58
|
+
if (backend === "mnemopi") {
|
|
59
|
+
const state = this.session.getMnemopiSessionState?.();
|
|
60
|
+
if (!state) {
|
|
61
|
+
throw new Error("Mnemopi backend is not initialised for this session.");
|
|
62
|
+
}
|
|
63
|
+
const id = state.rememberScoped(params.memory, {
|
|
64
|
+
source: "coding-agent-learn",
|
|
65
|
+
importance: 0.8,
|
|
66
|
+
metadata: {
|
|
67
|
+
session_id: state.sessionId,
|
|
68
|
+
cwd: state.session.sessionManager.getCwd(),
|
|
69
|
+
context: params.context ?? null,
|
|
70
|
+
tool: "learn",
|
|
71
|
+
},
|
|
72
|
+
scope: "bank",
|
|
73
|
+
extract: true,
|
|
74
|
+
extractEntities: true,
|
|
75
|
+
veracity: "tool",
|
|
76
|
+
memoryType: "fact",
|
|
77
|
+
});
|
|
78
|
+
// rememberScoped returns undefined when the retain failed (closed DB /
|
|
79
|
+
// disk error); mirror mnemopiBackend.save and fail loudly rather than
|
|
80
|
+
// reporting (and minting a skill for) a lesson that was silently dropped.
|
|
81
|
+
if (!id) {
|
|
82
|
+
throw new Error("Mnemopi did not store the lesson (no memory id returned).");
|
|
83
|
+
}
|
|
84
|
+
} else if (backend === "local") {
|
|
85
|
+
const result = await localBackend.save?.(
|
|
86
|
+
{ agentDir: this.session.settings.getAgentDir(), cwd: this.session.settings.getCwd() },
|
|
87
|
+
{ content: params.memory, context: params.context, source: "coding-agent-learn", importance: 0.8 },
|
|
88
|
+
);
|
|
89
|
+
if (!result || result.stored === 0) {
|
|
90
|
+
throw new Error("Lesson was empty after sanitization; nothing stored.");
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
const state = this.session.getHindsightSessionState?.();
|
|
94
|
+
if (!state) {
|
|
95
|
+
throw new Error("Hindsight backend is not initialised for this session.");
|
|
96
|
+
}
|
|
97
|
+
state.enqueueRetain(params.memory, params.context);
|
|
98
|
+
memoryMessage = "Lesson queued for retention";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 2) Optionally mint/enhance a managed skill. A failure here is surfaced
|
|
102
|
+
// as a partial outcome — the lesson is already stored or queued.
|
|
103
|
+
if (params.skill) {
|
|
104
|
+
// A managed skill resolves below any authored skill of the same name, so
|
|
105
|
+
// minting one under a claimed name writes a file that never surfaces. The
|
|
106
|
+
// lesson is already stored/queued; refuse the skill rather than report a
|
|
107
|
+
// false "Created" (mirrors ManageSkillTool).
|
|
108
|
+
let safeSkillName: string | undefined;
|
|
109
|
+
try {
|
|
110
|
+
safeSkillName = sanitizeSkillName(params.skill.name);
|
|
111
|
+
} catch {
|
|
112
|
+
safeSkillName = undefined;
|
|
113
|
+
}
|
|
114
|
+
if (params.skill.action === "create" && safeSkillName && isNameClaimedByAuthoredSkill(safeSkillName)) {
|
|
115
|
+
return {
|
|
116
|
+
content: [
|
|
117
|
+
{
|
|
118
|
+
type: "text",
|
|
119
|
+
text: `${memoryMessage}. Did not create managed skill "${params.skill.name}": an authored skill of that name already exists, and managed skills cannot override authored ones. Choose a different name.`,
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
isError: true,
|
|
123
|
+
details: { skill: null, shadowed: true },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
await writeManagedSkill(params.skill);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
130
|
+
throw new Error(`${memoryMessage}, but the managed skill could not be written: ${reason}`);
|
|
131
|
+
}
|
|
132
|
+
const verb = params.skill.action === "create" ? "Created" : "Updated";
|
|
133
|
+
return {
|
|
134
|
+
content: [{ type: "text", text: `${memoryMessage}. ${verb} managed skill "${params.skill.name}".` }],
|
|
135
|
+
details: { skill: params.skill.name },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
content: [{ type: "text", text: `${memoryMessage}.` }],
|
|
141
|
+
details: { skill: null },
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|