@oh-my-pi/pi-coding-agent 16.3.11 → 16.3.13
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 +65 -0
- package/dist/cli.js +3176 -3087
- package/dist/types/advisor/runtime.d.ts +11 -0
- package/dist/types/config/keybindings.d.ts +9 -4
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +6 -0
- package/dist/types/config/settings.d.ts +3 -1
- package/dist/types/discovery/helpers.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +11 -2
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +1 -0
- package/dist/types/internal-urls/registry-helpers.d.ts +7 -5
- package/dist/types/mnemopi/state.d.ts +7 -3
- package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -1
- package/dist/types/modes/components/read-tool-group.d.ts +1 -0
- package/dist/types/modes/github-ref-autocomplete.d.ts +35 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +11 -5
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/modes/utils/context-usage.d.ts +0 -12
- package/dist/types/modes/workflow.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +8 -4
- package/dist/types/system-prompt.d.ts +1 -1
- package/dist/types/tools/bash-interactive.d.ts +1 -1
- package/dist/types/tools/bash-skill-urls.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +2 -1
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/grep.d.ts +2 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/path-utils.d.ts +24 -0
- package/dist/types/tools/read.d.ts +3 -0
- package/dist/types/tools/renderers.d.ts +12 -5
- package/dist/types/tools/ssh.d.ts +4 -1
- package/dist/types/tools/write.d.ts +1 -0
- package/dist/types/utils/local-date.d.ts +2 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +145 -0
- package/src/advisor/runtime.ts +19 -0
- package/src/config/api-key-resolver.ts +7 -2
- package/src/config/keybindings.ts +62 -10
- package/src/config/model-registry.ts +94 -20
- package/src/config/settings-schema.ts +11 -1
- package/src/config/settings.ts +59 -21
- package/src/discovery/builtin.ts +2 -1
- package/src/discovery/claude-plugins.ts +167 -46
- package/src/discovery/helpers.ts +16 -1
- package/src/edit/renderer.ts +20 -6
- package/src/eval/js/worker-core.ts +163 -6
- package/src/exec/bash-executor.ts +14 -9
- package/src/extensibility/extensions/runner.ts +1 -0
- package/src/extensibility/extensions/types.ts +13 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +6 -2
- package/src/extensibility/plugins/marketplace/fetcher.ts +15 -14
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +68 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/registry-helpers.ts +9 -6
- package/src/mnemopi/state.ts +19 -5
- package/src/modes/acp/acp-agent.ts +69 -8
- package/src/modes/acp/acp-event-mapper.ts +1 -1
- package/src/modes/components/model-selector.ts +30 -6
- package/src/modes/components/read-tool-group.ts +5 -1
- package/src/modes/components/settings-defs.ts +1 -1
- package/src/modes/components/status-line/component.ts +14 -2
- package/src/modes/components/tool-execution.ts +28 -24
- package/src/modes/controllers/command-controller.ts +13 -23
- package/src/modes/controllers/event-controller.ts +12 -12
- package/src/modes/controllers/extension-ui-controller.test.ts +16 -0
- package/src/modes/controllers/extension-ui-controller.ts +7 -35
- package/src/modes/controllers/input-controller.ts +23 -57
- package/src/modes/controllers/mcp-command-controller.ts +10 -9
- package/src/modes/controllers/selector-controller.ts +16 -5
- package/src/modes/github-ref-autocomplete.ts +75 -0
- package/src/modes/interactive-mode.ts +97 -12
- package/src/modes/prompt-action-autocomplete.ts +35 -0
- package/src/modes/rpc/rpc-client.ts +42 -13
- package/src/modes/rpc/rpc-mode.ts +21 -19
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/context-usage.ts +58 -5
- package/src/modes/utils/hotkeys-markdown.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/modes/workflow.ts +14 -8
- package/src/prompts/agents/plan.md +0 -1
- package/src/prompts/agents/reviewer.md +0 -1
- package/src/prompts/system/plan-mode-active.md +5 -2
- package/src/prompts/system/system-prompt.md +1 -2
- package/src/prompts/system/workflow-notice.md +69 -50
- package/src/prompts/tools/bash.md +18 -7
- package/src/prompts/tools/grep.md +2 -1
- package/src/prompts/tools/memory-edit.md +2 -0
- package/src/prompts/tools/read.md +4 -3
- package/src/sdk.ts +11 -0
- package/src/session/agent-session.ts +136 -19
- package/src/system-prompt.ts +3 -2
- package/src/tools/bash-interactive.ts +1 -1
- package/src/tools/bash-skill-urls.ts +39 -7
- package/src/tools/bash.ts +69 -39
- package/src/tools/browser/launch.ts +31 -4
- package/src/tools/grep.ts +105 -21
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/index.ts +11 -0
- package/src/tools/memory-edit.ts +3 -1
- package/src/tools/path-utils.ts +46 -1
- package/src/tools/read.ts +135 -57
- package/src/tools/renderers.ts +13 -5
- package/src/tools/ssh.ts +10 -3
- package/src/tools/tts.ts +1 -1
- package/src/tools/write.ts +26 -0
- package/src/utils/local-date.ts +7 -0
- package/src/utils/open.ts +36 -10
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { formatKeyHints, type KeybindingsManager } from "../config/keybindings";
|
|
10
10
|
import { isSettingsInitialized, settings } from "../config/settings";
|
|
11
11
|
import { applyEmojiCompletion, getEmojiSuggestions, isEmojiPrefix, tryEmojiInlineReplace } from "./emoji-autocomplete";
|
|
12
|
+
import { getGithubRefContext, getGithubRefSuggestions } from "./github-ref-autocomplete";
|
|
12
13
|
import {
|
|
13
14
|
applyInternalUrlCompletion,
|
|
14
15
|
getInternalUrlSuggestions,
|
|
@@ -94,6 +95,36 @@ function getPromptActionPrefix(textBeforeCursor: string): string | null {
|
|
|
94
95
|
return textBeforeCursor.slice(hashIndex);
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
function applyGithubRefCompletion(
|
|
99
|
+
lines: string[],
|
|
100
|
+
cursorLine: number,
|
|
101
|
+
cursorCol: number,
|
|
102
|
+
item: AutocompleteItem,
|
|
103
|
+
prefix: string,
|
|
104
|
+
): { lines: string[]; cursorLine: number; cursorCol: number } | null {
|
|
105
|
+
if (!getGithubRefContext(prefix)) return null;
|
|
106
|
+
const scheme: "pr" | "issue" | null = item.value.startsWith("pr://")
|
|
107
|
+
? "pr"
|
|
108
|
+
: item.value.startsWith("issue://")
|
|
109
|
+
? "issue"
|
|
110
|
+
: null;
|
|
111
|
+
if (!scheme) return { lines, cursorLine, cursorCol };
|
|
112
|
+
|
|
113
|
+
const currentLine = lines[cursorLine] || "";
|
|
114
|
+
const liveContext = getGithubRefContext(currentLine.slice(0, cursorCol));
|
|
115
|
+
if (!liveContext || (liveContext.qualifier && liveContext.qualifier !== scheme)) {
|
|
116
|
+
return { lines, cursorLine, cursorCol };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return applyInternalUrlCompletion(
|
|
120
|
+
lines,
|
|
121
|
+
cursorLine,
|
|
122
|
+
cursorCol,
|
|
123
|
+
{ ...item, value: `${scheme}://${liveContext.number}` },
|
|
124
|
+
liveContext.prefix,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
97
128
|
export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
98
129
|
#commands: SlashCommand[];
|
|
99
130
|
#baseProvider: CombinedAutocompleteProvider;
|
|
@@ -129,6 +160,8 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
129
160
|
}
|
|
130
161
|
}
|
|
131
162
|
|
|
163
|
+
const githubRefSuggestions = getGithubRefSuggestions(textBeforeCursor);
|
|
164
|
+
if (githubRefSuggestions) return githubRefSuggestions;
|
|
132
165
|
const promptActionPrefix = getPromptActionPrefix(textBeforeCursor);
|
|
133
166
|
if (promptActionPrefix) {
|
|
134
167
|
const query = promptActionPrefix.slice(1).toLowerCase();
|
|
@@ -176,6 +209,8 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
176
209
|
cursorCol: number;
|
|
177
210
|
onApplied?: () => void;
|
|
178
211
|
} {
|
|
212
|
+
const githubRefCompletion = applyGithubRefCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
213
|
+
if (githubRefCompletion) return githubRefCompletion;
|
|
179
214
|
if (prefix.startsWith("#") && isPromptActionItem(item)) {
|
|
180
215
|
if (item.actionId === "undo") {
|
|
181
216
|
return {
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
RpcAvailableSlashCommand,
|
|
18
18
|
RpcCommand,
|
|
19
19
|
RpcExtensionUIRequest,
|
|
20
|
+
RpcExtensionUIResponse,
|
|
20
21
|
RpcHandoffResult,
|
|
21
22
|
RpcHostToolCallRequest,
|
|
22
23
|
RpcHostToolCancelRequest,
|
|
@@ -722,25 +723,50 @@ export class RpcClient {
|
|
|
722
723
|
/**
|
|
723
724
|
* Trigger OAuth login for the given provider.
|
|
724
725
|
* The server will emit an `open_url` extension_ui_request for the auth URL.
|
|
726
|
+
* Providers that require pasted-code completion may then emit an `input`
|
|
727
|
+
* extension_ui_request; pass `onManualCodeInput` to satisfy it.
|
|
725
728
|
* Resolves when login completes or rejects on failure.
|
|
726
729
|
*
|
|
727
730
|
* @param onOpenUrl Called when the server emits the auth URL. The host must
|
|
728
|
-
* open `url` in a browser
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
731
|
+
* open `url` in a browser. When the flow's callback server hosts a
|
|
732
|
+
* `/launch` redirect, `launchUrl` is a short loopback URL that 302s to
|
|
733
|
+
* `url` — hosts SHOULD surface it as the truncation-safe copy target so
|
|
734
|
+
* terminal viewport clipping cannot corrupt trailing OAuth query
|
|
735
|
+
* parameters (e.g. `code_challenge_method=S256`).
|
|
733
736
|
*/
|
|
734
737
|
async login(
|
|
735
738
|
providerId: string,
|
|
736
|
-
options?: {
|
|
739
|
+
options?: {
|
|
740
|
+
onOpenUrl?: (url: string, instructions?: string, launchUrl?: string) => void;
|
|
741
|
+
onManualCodeInput?: (prompt: { title: string; placeholder?: string }) => string | Promise<string>;
|
|
742
|
+
},
|
|
737
743
|
): Promise<{ providerId: string }> {
|
|
738
|
-
const { onOpenUrl } = options ?? {};
|
|
739
|
-
const listener =
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
+
const { onManualCodeInput, onOpenUrl } = options ?? {};
|
|
745
|
+
const listener =
|
|
746
|
+
onOpenUrl || onManualCodeInput
|
|
747
|
+
? (req: RpcExtensionUIRequest) => {
|
|
748
|
+
if (req.method === "open_url") {
|
|
749
|
+
onOpenUrl?.(req.url, req.instructions, req.launchUrl);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (req.method !== "input" || !onManualCodeInput) return;
|
|
753
|
+
void Promise.resolve(onManualCodeInput({ title: req.title, placeholder: req.placeholder }))
|
|
754
|
+
.then(value => {
|
|
755
|
+
this.#writeFrame({
|
|
756
|
+
type: "extension_ui_response",
|
|
757
|
+
id: req.id,
|
|
758
|
+
value,
|
|
759
|
+
});
|
|
760
|
+
})
|
|
761
|
+
.catch(() => {
|
|
762
|
+
this.#writeFrame({
|
|
763
|
+
type: "extension_ui_response",
|
|
764
|
+
id: req.id,
|
|
765
|
+
cancelled: true,
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
: undefined;
|
|
744
770
|
if (listener) this.#extensionUiListeners.add(listener);
|
|
745
771
|
try {
|
|
746
772
|
const response = await this.#send({ type: "login", providerId }, 600_000);
|
|
@@ -1006,7 +1032,10 @@ export class RpcClient {
|
|
|
1006
1032
|
}
|
|
1007
1033
|
}
|
|
1008
1034
|
|
|
1009
|
-
#writeFrame(
|
|
1035
|
+
#writeFrame(
|
|
1036
|
+
frame: RpcCommand | RpcExtensionUIResponse | RpcHostToolResult | RpcHostToolUpdate,
|
|
1037
|
+
onError?: (error: Error) => void,
|
|
1038
|
+
): void {
|
|
1010
1039
|
if (!this.#process?.stdin) {
|
|
1011
1040
|
throw new Error("Client not started");
|
|
1012
1041
|
}
|
|
@@ -88,6 +88,7 @@ export type RpcSkillCommandResult = { agentInvoked: true };
|
|
|
88
88
|
export async function tryRunRpcSkillCommand(
|
|
89
89
|
session: RpcSkillCommandSession,
|
|
90
90
|
text: string,
|
|
91
|
+
streamingBehavior: "steer" | "followUp" = "steer",
|
|
91
92
|
): Promise<RpcSkillCommandResult | false> {
|
|
92
93
|
if (!session.skillsSettings?.enableSkillCommands) return false;
|
|
93
94
|
const parsed = parseSkillInvocation(text);
|
|
@@ -95,13 +96,16 @@ export async function tryRunRpcSkillCommand(
|
|
|
95
96
|
const skill = session.skills.find(candidate => candidate.name === parsed.name);
|
|
96
97
|
if (!skill) return false;
|
|
97
98
|
const built = await buildSkillPromptMessage(skill, parsed.args, "user");
|
|
98
|
-
await session.promptCustomMessage(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
await session.promptCustomMessage(
|
|
100
|
+
{
|
|
101
|
+
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
102
|
+
content: built.message,
|
|
103
|
+
display: true,
|
|
104
|
+
details: built.details,
|
|
105
|
+
attribution: "user",
|
|
106
|
+
},
|
|
107
|
+
{ streamingBehavior },
|
|
108
|
+
);
|
|
105
109
|
return { agentInvoked: true };
|
|
106
110
|
}
|
|
107
111
|
|
|
@@ -755,6 +759,10 @@ export async function runRpcMode(
|
|
|
755
759
|
return requestRpcEditor(this.pendingRequests, this.output, title, prefill, dialogOptions, editorOptions);
|
|
756
760
|
}
|
|
757
761
|
|
|
762
|
+
addAutocompleteProvider(): void {
|
|
763
|
+
// Autocomplete provider composition is not supported in RPC mode
|
|
764
|
+
}
|
|
765
|
+
|
|
758
766
|
get theme(): Theme {
|
|
759
767
|
return theme;
|
|
760
768
|
}
|
|
@@ -842,7 +850,7 @@ export async function runRpcMode(
|
|
|
842
850
|
// =================================================================
|
|
843
851
|
|
|
844
852
|
case "prompt": {
|
|
845
|
-
const skillResult = await tryRunRpcSkillCommand(session, command.message);
|
|
853
|
+
const skillResult = await tryRunRpcSkillCommand(session, command.message, command.streamingBehavior);
|
|
846
854
|
if (skillResult) {
|
|
847
855
|
return success(id, "prompt", skillResult);
|
|
848
856
|
}
|
|
@@ -1198,11 +1206,9 @@ export async function runRpcMode(
|
|
|
1198
1206
|
return error(id, "login", `Unknown OAuth provider: ${command.providerId}`);
|
|
1199
1207
|
}
|
|
1200
1208
|
const uiCtx = new RpcExtensionUIContext(pendingExtensionRequests, output);
|
|
1201
|
-
// Track whether onAuth has fired. Providers that
|
|
1202
|
-
//
|
|
1203
|
-
//
|
|
1204
|
-
// GitHub Enterprise URL, device-code entry) call onPrompt before onAuth.
|
|
1205
|
-
// We use this ordering to self-classify at runtime — no static allowlist.
|
|
1209
|
+
// Track whether onAuth has fired. Providers that require interactive
|
|
1210
|
+
// input before a browser URL cannot be satisfied headlessly; after
|
|
1211
|
+
// onAuth, prompt input is the pasted OAuth code/redirect URL path.
|
|
1206
1212
|
let authEmitted = false;
|
|
1207
1213
|
try {
|
|
1208
1214
|
await session.modelRegistry.authStorage.login(command.providerId, {
|
|
@@ -1220,7 +1226,7 @@ export async function runRpcMode(
|
|
|
1220
1226
|
onProgress: message => {
|
|
1221
1227
|
uiCtx.notify(message, "info");
|
|
1222
1228
|
},
|
|
1223
|
-
onPrompt:
|
|
1229
|
+
onPrompt: async prompt => {
|
|
1224
1230
|
if (!authEmitted) {
|
|
1225
1231
|
// onPrompt called before any auth URL — provider requires
|
|
1226
1232
|
// interactive input that cannot be satisfied headlessly.
|
|
@@ -1231,11 +1237,7 @@ export async function runRpcMode(
|
|
|
1231
1237
|
),
|
|
1232
1238
|
);
|
|
1233
1239
|
}
|
|
1234
|
-
|
|
1235
|
-
// manual-redirect fallback race. Returning a never-settling promise
|
|
1236
|
-
// lets the race block until the callback server wins; a rejection
|
|
1237
|
-
// would be caught as null and spin the while(true) loop.
|
|
1238
|
-
return new Promise<string>(() => {});
|
|
1240
|
+
return (await uiCtx.input(prompt.message, prompt.placeholder, { timeout: 600_000 })) ?? "";
|
|
1239
1241
|
},
|
|
1240
1242
|
});
|
|
1241
1243
|
await session.modelRegistry.refresh();
|
package/src/modes/types.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { CollabHost } from "../collab/host";
|
|
|
7
7
|
import type { KeybindingsManager } from "../config/keybindings";
|
|
8
8
|
import type { Settings } from "../config/settings";
|
|
9
9
|
import type {
|
|
10
|
+
AutocompleteProviderFactory,
|
|
10
11
|
ExtensionUIContext,
|
|
11
12
|
ExtensionUIDialogOptions,
|
|
12
13
|
ExtensionUISelectItem,
|
|
@@ -218,6 +219,8 @@ export interface InteractiveModeContext {
|
|
|
218
219
|
// Extension UI integration
|
|
219
220
|
setToolUIContext(uiContext: ExtensionUIContext, hasUI: boolean): void;
|
|
220
221
|
initializeHookRunner(uiContext: ExtensionUIContext, hasUI: boolean): void;
|
|
222
|
+
/** Stack extension autocomplete behavior on top of the built-in editor provider. */
|
|
223
|
+
addAutocompleteProvider(factory: AutocompleteProviderFactory): void;
|
|
221
224
|
setEditorComponent(
|
|
222
225
|
factory: ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => CustomEditor) | undefined,
|
|
223
226
|
): void;
|
|
@@ -43,6 +43,7 @@ export interface ContextBreakdown {
|
|
|
43
43
|
|
|
44
44
|
const EMPTY_STRING_PARTS: readonly string[] = [];
|
|
45
45
|
const EMPTY_TOOLS: ReadonlyArray<Pick<Tool, "name" | "description" | "parameters">> = [];
|
|
46
|
+
const EMPTY_SKILLS: readonly Skill[] = [];
|
|
46
47
|
|
|
47
48
|
export function estimateSkillsTokens(skills: readonly Skill[]): number {
|
|
48
49
|
const fragments: string[] = [];
|
|
@@ -86,10 +87,58 @@ export function estimateToolSchemaTokens(
|
|
|
86
87
|
* cadence — non-message recomputed only when the inputs identity changes,
|
|
87
88
|
* messages walked incrementally as new entries append.
|
|
88
89
|
*/
|
|
90
|
+
// Non-message inputs (system prompt, tools, skills) change rarely — at most
|
|
91
|
+
// once per turn via setSystemPrompt/setTools — but the per-turn compaction and
|
|
92
|
+
// threshold paths call these helpers several times: getContextBreakdown calls
|
|
93
|
+
// both, and #estimateStoredContextTokens adds a third. Memoize on the identity
|
|
94
|
+
// of the three input arrays so the expensive parts (system-prompt tokenization
|
|
95
|
+
// and the per-tool JSON.stringify(toolWireSchema) inside estimateToolSchemaTokens)
|
|
96
|
+
// run at most once per input change rather than per call. The identity keys are
|
|
97
|
+
// the same stable references the StatusLineComponent cache already trusts
|
|
98
|
+
// (setSystemPrompt/setTools replace the array reference rather than mutating it).
|
|
99
|
+
interface NonMessageTokenCache {
|
|
100
|
+
systemPromptRef: readonly string[];
|
|
101
|
+
toolsRef: ReadonlyArray<Pick<Tool, "name" | "description" | "parameters">>;
|
|
102
|
+
skillsRef: readonly Skill[];
|
|
103
|
+
tokens: number | undefined;
|
|
104
|
+
breakdown:
|
|
105
|
+
| {
|
|
106
|
+
skillsTokens: number;
|
|
107
|
+
toolsTokens: number;
|
|
108
|
+
systemContextTokens: number;
|
|
109
|
+
systemPromptTokens: number;
|
|
110
|
+
}
|
|
111
|
+
| undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const nonMessageTokenCache = new WeakMap<AgentSession, NonMessageTokenCache>();
|
|
115
|
+
|
|
116
|
+
function nonMessageTokenCacheEntry(session: AgentSession): NonMessageTokenCache {
|
|
117
|
+
const systemPromptRef = session.systemPrompt ?? EMPTY_STRING_PARTS;
|
|
118
|
+
const toolsRef = session.agent?.state?.tools ?? EMPTY_TOOLS;
|
|
119
|
+
const skillsRef = session.skills ?? EMPTY_SKILLS;
|
|
120
|
+
let entry = nonMessageTokenCache.get(session);
|
|
121
|
+
if (
|
|
122
|
+
entry &&
|
|
123
|
+
entry.systemPromptRef === systemPromptRef &&
|
|
124
|
+
entry.toolsRef === toolsRef &&
|
|
125
|
+
entry.skillsRef === skillsRef
|
|
126
|
+
) {
|
|
127
|
+
return entry;
|
|
128
|
+
}
|
|
129
|
+
entry = { systemPromptRef, toolsRef, skillsRef, tokens: undefined, breakdown: undefined };
|
|
130
|
+
nonMessageTokenCache.set(session, entry);
|
|
131
|
+
return entry;
|
|
132
|
+
}
|
|
133
|
+
|
|
89
134
|
export function computeNonMessageTokens(session: AgentSession): number {
|
|
135
|
+
const entry = nonMessageTokenCacheEntry(session);
|
|
136
|
+
if (entry.tokens !== undefined) return entry.tokens;
|
|
90
137
|
const systemPromptParts = session.systemPrompt ?? EMPTY_STRING_PARTS;
|
|
91
138
|
const tools = session.agent?.state?.tools ?? EMPTY_TOOLS;
|
|
92
|
-
|
|
139
|
+
const tokens = countTokens(systemPromptParts) + estimateToolSchemaTokens(tools);
|
|
140
|
+
entry.tokens = tokens;
|
|
141
|
+
return tokens;
|
|
93
142
|
}
|
|
94
143
|
|
|
95
144
|
/**
|
|
@@ -104,12 +153,16 @@ export function computeNonMessageBreakdown(session: AgentSession): {
|
|
|
104
153
|
systemContextTokens: number;
|
|
105
154
|
systemPromptTokens: number;
|
|
106
155
|
} {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
const
|
|
156
|
+
const entry = nonMessageTokenCacheEntry(session);
|
|
157
|
+
if (entry.breakdown) return entry.breakdown;
|
|
158
|
+
const skillsTokens = estimateSkillsTokens(session.skills ?? EMPTY_SKILLS);
|
|
159
|
+
const toolsTokens = estimateToolSchemaTokens(session.agent?.state?.tools ?? EMPTY_TOOLS);
|
|
160
|
+
const systemPromptParts = session.systemPrompt ?? EMPTY_STRING_PARTS;
|
|
110
161
|
const systemContextTokens = countTokens(systemPromptParts.slice(1));
|
|
111
162
|
const systemPromptTokens = Math.max(0, countTokens(systemPromptParts[0] ?? "") - skillsTokens);
|
|
112
|
-
|
|
163
|
+
const breakdown = { skillsTokens, toolsTokens, systemContextTokens, systemPromptTokens };
|
|
164
|
+
entry.breakdown = breakdown;
|
|
165
|
+
return breakdown;
|
|
113
166
|
}
|
|
114
167
|
|
|
115
168
|
/**
|
|
@@ -52,7 +52,8 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
|
|
|
52
52
|
`| \`${appKey(bindings, "app.clipboard.pasteImage")}\` | Paste image or text from clipboard |`,
|
|
53
53
|
"| Hold `Space` | Speech-to-text (push-to-talk): hold to record, release to transcribe |",
|
|
54
54
|
`| \`${appKey(bindings, "app.agents.hub")}\` / \`${appKey(bindings, "app.session.observe")}\` / double-tap \`←\` (empty editor) | Open the agent hub |`,
|
|
55
|
-
"|
|
|
55
|
+
"| `#<number>` | GitHub issue/PR reference (e.g. `#3164` → `pr://`/`issue://`) |",
|
|
56
|
+
"| `#` / `#<text>` | Prompt actions (copy / undo / move cursor) |",
|
|
56
57
|
"| `/` | Slash commands |",
|
|
57
58
|
"| `!` | Run bash command |",
|
|
58
59
|
"| `!!` | Run bash command (excluded from context) |",
|
|
@@ -581,7 +581,7 @@ export class UiHelpers {
|
|
|
581
581
|
} else {
|
|
582
582
|
this.ctx.resetTranscript();
|
|
583
583
|
}
|
|
584
|
-
this.ctx.pendingMessagesContainer.
|
|
584
|
+
this.ctx.pendingMessagesContainer.disposeChildren();
|
|
585
585
|
this.ctx.pendingBashComponents = [];
|
|
586
586
|
this.ctx.pendingPythonComponents = [];
|
|
587
587
|
|
|
@@ -647,7 +647,7 @@ export class UiHelpers {
|
|
|
647
647
|
}
|
|
648
648
|
|
|
649
649
|
updatePendingMessagesDisplay(): void {
|
|
650
|
-
this.ctx.pendingMessagesContainer.
|
|
650
|
+
this.ctx.pendingMessagesContainer.disposeChildren();
|
|
651
651
|
const queuedMessages = this.ctx.viewSession.getQueuedMessages() as QueuedMessages;
|
|
652
652
|
|
|
653
653
|
const steeringMessages: Array<{ message: string; label: string }> = [];
|
package/src/modes/workflow.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { prompt } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import workflowNoticeTemplate from "../prompts/system/workflow-notice.md" with { type: "text" };
|
|
2
3
|
import { createGradientHighlighter, type KeywordHighlighter } from "./gradient-highlight";
|
|
3
4
|
import { keywordInProse } from "./markdown-prose";
|
|
4
5
|
|
|
@@ -7,18 +8,23 @@ import { keywordInProse } from "./markdown-prose";
|
|
|
7
8
|
*
|
|
8
9
|
* Typing the standalone word in the input editor paints it with a warm
|
|
9
10
|
* amber→green gradient ({@link highlightWorkflow}); submitting a message that
|
|
10
|
-
* mentions it appends a hidden
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* mentions it appends a hidden workflow notice that steers the model to author
|
|
12
|
+
* a deterministic multi-subagent workflow through the active task schema.
|
|
13
|
+
* Matching is whitespace-delimited and case-sensitive (lowercase only) —
|
|
14
|
+
* "workflowz" triggers, but "workflowzed", "Workflowz", and "workflowz.ts"
|
|
15
|
+
* never do.
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
// Detection: lowercase keyword flanked by whitespace or a string edge. Non-global so `.test` stays stateless.
|
|
18
19
|
const WORKFLOW_WORD = /(?<!\S)workflowz(?!\S)/;
|
|
19
20
|
|
|
20
|
-
/**
|
|
21
|
-
export const WORKFLOW_NOTICE: string =
|
|
21
|
+
/** WORKFLOW_NOTICE is the default hidden notice for sessions with batched task calls enabled. */
|
|
22
|
+
export const WORKFLOW_NOTICE: string = renderWorkflowNotice({ taskBatch: true });
|
|
23
|
+
|
|
24
|
+
/** renderWorkflowNotice renders the workflow notice for the active task schema. */
|
|
25
|
+
export function renderWorkflowNotice({ taskBatch }: { taskBatch: boolean }): string {
|
|
26
|
+
return prompt.render(workflowNoticeTemplate, { taskBatch }).trim();
|
|
27
|
+
}
|
|
22
28
|
|
|
23
29
|
/**
|
|
24
30
|
* Whether `text` contains the standalone keyword "workflowz"
|
|
@@ -4,7 +4,6 @@ description: Software architect for complex multi-file architectural decisions.
|
|
|
4
4
|
tools: read, grep, glob, bash, lsp, web_search, ast_grep
|
|
5
5
|
spawns: explore
|
|
6
6
|
model: pi/plan, pi/slow
|
|
7
|
-
thinking-level: high
|
|
8
7
|
---
|
|
9
8
|
|
|
10
9
|
Analyze the codebase and the user's request. Produce a detailed implementation plan.
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
<critical>
|
|
2
|
-
Plan mode is active. You MUST
|
|
3
|
-
- You NEVER create, edit,
|
|
2
|
+
Plan mode is active. You MUST preserve read-only working-tree and system semantics:
|
|
3
|
+
- You NEVER create, edit, delete, or rename working-tree files.
|
|
4
4
|
- You NEVER run state-changing commands (`git commit`, `npm install`, migrations) or make any other system change.
|
|
5
|
+
- `local://` artifacts are session-local planning artifacts. You MAY create or update them when explicitly requested or needed for the plan.
|
|
6
|
+
- You NEVER delete or rename `local://` artifacts.
|
|
7
|
+
- You MUST write the canonical plan to `local://<slug>-plan.md`.
|
|
5
8
|
|
|
6
9
|
To leave plan mode and implement: call `resolve` with `action: "apply"`, a `reason`, and `extra: { title: "<slug>" }`, where `<slug>` matches your `local://<slug>-plan.md`. The user then picks an execution option and full write access is restored. `<slug>` may contain only letters, numbers, underscores, and hyphens.
|
|
7
10
|
|
|
@@ -110,9 +110,8 @@ You MUST use the specialized tool over its shell equivalent:
|
|
|
110
110
|
{{#has tools "lsp"}}- Code intelligence → `{{toolRefs.lsp}}`.{{/has}}
|
|
111
111
|
{{#has tools "grep"}}- Regex search → `{{toolRefs.grep}}`, not `grep`, `rg`, or `awk`.{{/has}}
|
|
112
112
|
{{#has tools "glob"}}- Globbing → `{{toolRefs.glob}}`, not `ls **/*.ext` or `fd`.{{/has}}
|
|
113
|
-
{{#has tools "eval"}}- Default for any compute: `{{toolRefs.eval}}` cells. Bash is the EXCEPTION — only single binary calls or short fact-computing pipelines (`wc -l`, `sort | uniq -c`, `diff`, checksums). The moment a command grows a loop, conditional, heredoc, `-e`/`-c` script, `$(…)` nesting, or >2 pipe stages, it's a program → `{{toolRefs.eval}}`. NEVER write multiline or inline-script bash.{{/has}}
|
|
114
113
|
{{#has tools "bash"}}- `{{toolRefs.bash}}`: real binaries and short fact pipelines only. Commands shadowing the specialized tools above are blocked.{{/has}}
|
|
115
|
-
{{#has tools "bash"}}- Litmus: one external-CLI call or short pipeline returning a count, frequency, set difference, or checksum → bash.
|
|
114
|
+
{{#has tools "bash"}}- Litmus: one external-CLI call or short pipeline returning a count, frequency, set difference, or checksum → bash. Merely moves, pages, or trims bytes a tool can fetch → use the tool.{{/has}}
|
|
116
115
|
|
|
117
116
|
{{#has tools "report_tool_issue"}}
|
|
118
117
|
<critical>
|
|
@@ -1,70 +1,89 @@
|
|
|
1
1
|
<system-notice>
|
|
2
|
-
The user's message above contains the **workflowz** keyword: drive this task as a deterministic multi-subagent workflow.
|
|
2
|
+
The user's message above contains the **workflowz** keyword: drive this task as a deterministic multi-subagent workflow. Use the `task` tool {{#if taskBatch}}for batched fan-out{{else}}once per independent subagent{{/if}} — to be comprehensive (decompose and cover in parallel), to be confident (independent perspectives and adversarial checks before you commit), or to take on scale one context can't hold (audits, migrations, broad sweeps). This overrides any default tendency to do the whole task inline when fanning out would be more thorough.
|
|
3
3
|
|
|
4
4
|
<when>
|
|
5
|
-
Worth it when the task benefits from decomposition + parallel coverage, or from independent/adversarial cross-checking before you commit. For a quick lookup or single edit, just do it directly — don't spin up agents. Scout inline
|
|
6
|
-
- **Understand** — parallel readers over subsystems → structured map
|
|
7
|
-
- **Design** —
|
|
8
|
-
- **Review** — split
|
|
9
|
-
- **Research** — multi-modal sweep → deep-read the hits → synthesize
|
|
10
|
-
- **Migrate** — discover sites → transform each → verify
|
|
5
|
+
Worth it when the task benefits from decomposition + parallel coverage, or from independent/adversarial cross-checking before you commit. For a quick lookup or single edit, just do it directly — don't spin up agents. Scout inline first (list the files, scope the diff, find the call sites) to discover the work list, then fan out over it. Common shapes:
|
|
6
|
+
- **Understand** — parallel readers over subsystems → structured map.
|
|
7
|
+
- **Design** — independent approaches → scored synthesis.
|
|
8
|
+
- **Review** — split dimensions → find per dimension → adversarially verify each finding.
|
|
9
|
+
- **Research** — multi-modal sweep → deep-read the hits → synthesize.
|
|
10
|
+
- **Migrate** — discover sites → transform each → verify.
|
|
11
11
|
</when>
|
|
12
12
|
|
|
13
|
-
<
|
|
14
|
-
|
|
13
|
+
<task-contract>
|
|
14
|
+
{{#if taskBatch}}
|
|
15
|
+
Call `task` once per independent fan-out batch. Put shared background in `context`, and put each independent work item in `tasks[]`. Do not emulate batching with shell loops or eval helper APIs.
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
- `parallel(thunks)` — run zero-arg callables concurrently through a bounded pool, preserving input order; returns once all finish. The pool is bounded by the session's `task` concurrency — don't hand-tune it; fan out as wide as the work divides. A thunk that raises propagates — wrap risky work in `try/except` inside the thunk to keep partial results. In a loop, bind each closure's value with a default arg (`lambda d=d: …`) or every thunk captures the last one.
|
|
18
|
-
- `pipeline(items, *stages)` — map items through `stages` left-to-right. There is a BARRIER between stages: ALL items clear stage N before stage N+1 begins. Each stage is a one-arg callable; stage 1 gets the original item, later stages get the previous result. Same pool width as `parallel()`.
|
|
19
|
-
- `completion(prompt, *, model="default", system=None, schema=None)` — oneshot, stateless model call (no tools, no history). Tiers: "smol", "default", "slow". Cheap classification/scoring inside a fan-out.
|
|
20
|
-
- `log(message)` — emit a progress line above the status tree. `phase(title)` — start a phase; the status lines that follow group under it.
|
|
21
|
-
- `budget` — `budget.total` (output-token ceiling, or `None` when none is set), `budget.spent()` (tokens spent this turn — main loop + eval subagents), `budget.remaining()` (`math.inf` when total is `None`), `budget.hard` (whether it's enforced). A ceiling is set by the user: `+Nk` in their message is advisory (you self-limit via `budget.remaining()`), `+Nk!` (or Goal Mode) is hard — `agent()` refuses to spawn once spent reaches it. Gate loops on `budget.total` first, since it's `None` when the user set no budget.
|
|
17
|
+
`context` must carry the shared contract:
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
# Goal
|
|
20
|
+
What the batch accomplishes.
|
|
21
|
+
# Constraints
|
|
22
|
+
Rules, non-goals, permissions, and verification limits.
|
|
23
|
+
# Contract
|
|
24
|
+
Shared interfaces, output shape, branch/base assumptions, and coordination rules.
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
Each task assignment must be self-contained:
|
|
27
|
+
|
|
28
|
+
# Target
|
|
29
|
+
Exact files, symbols, subsystem, or evidence surface; explicit non-goals.
|
|
30
|
+
# Change
|
|
31
|
+
What to inspect or modify, step by step, including APIs and patterns to reuse.
|
|
32
|
+
# Acceptance
|
|
33
|
+
Observable result, return packet, and local verification. Subagents skip formatters,
|
|
34
|
+
linters, and project-wide tests; the parent runs shared proof once.
|
|
35
|
+
{{else}}
|
|
36
|
+
Call `task` once per independent subagent. Put the full shared background and the leaf work in that call's `assignment`. Do not pass `context` or `tasks[]`: the flat task schema rejects them when batch calls are disabled.
|
|
28
37
|
|
|
29
|
-
|
|
30
|
-
def review_and_verify(d):
|
|
31
|
-
found = agent(d["prompt"], label=f"review:{d['key']}", schema=FINDINGS_SCHEMA)
|
|
32
|
-
return parallel([lambda f=f: {**f, "verdict": agent(
|
|
33
|
-
f"Refute if you can (default refuted when unsure): {f['title']}",
|
|
34
|
-
label=f"verify:{f['file']}", schema=VERDICT_SCHEMA)} for f in found["findings"]])
|
|
35
|
-
phase("Review")
|
|
36
|
-
results = parallel([lambda d=d: review_and_verify(d) for d in DIMENSIONS])
|
|
37
|
-
confirmed = [f for group in results for f in group if f["verdict"]["is_real"]]
|
|
38
|
+
Each assignment must be self-contained:
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
# Target
|
|
41
|
+
Exact files, symbols, subsystem, or evidence surface; explicit non-goals.
|
|
42
|
+
# Change
|
|
43
|
+
Shared background plus what to inspect or modify, step by step, including APIs and patterns to reuse.
|
|
44
|
+
# Acceptance
|
|
45
|
+
Observable result, return packet, and local verification. Subagents skip formatters,
|
|
46
|
+
linters, and project-wide tests; the parent runs shared proof once.
|
|
47
|
+
{{/if}}
|
|
40
48
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
findings = dedupe([f for r in found for f in r["findings"]]) # needs everything at once
|
|
44
|
-
phase("Verify")
|
|
45
|
-
verdicts = parallel([lambda f=f: agent(verify_prompt(f), schema=VERDICT_SCHEMA) for f in findings])
|
|
49
|
+
<structure>
|
|
50
|
+
Decompose first, then {{#if taskBatch}}batch the independent leaves{{else}}issue one independent task call per leaf in the same turn{{/if}}:
|
|
46
51
|
|
|
47
|
-
|
|
52
|
+
{{#if taskBatch}}
|
|
53
|
+
task(
|
|
54
|
+
context: "# Goal\nReview the auth diff...\n# Constraints\nRead-only...\n# Contract\nReturn findings as severity/file/line/fix...",
|
|
55
|
+
tasks: [
|
|
56
|
+
{ id: "AuthOwner", role: "Auth Storage Reviewer", assignment: "# Target\npackages/ai/src/auth-storage.ts\n# Change\nTrace credential selection...\n# Acceptance\nReturn confirmed findings only..." },
|
|
57
|
+
{ id: "PromptOwner", role: "Prompt Contract Reviewer", assignment: "# Target\npackages/coding-agent/src/prompts/**\n# Change\nCheck active-tool guidance...\n# Acceptance\nReturn mismatches and exact prompt lines..." },
|
|
58
|
+
]
|
|
59
|
+
)
|
|
60
|
+
{{else}}
|
|
61
|
+
task(
|
|
62
|
+
role: "Auth Storage Reviewer",
|
|
63
|
+
assignment: "# Target\npackages/ai/src/auth-storage.ts\n# Change\nReview the auth diff. Shared contract: read-only; return findings as severity/file/line/fix.\n# Acceptance\nReturn confirmed findings only..."
|
|
64
|
+
)
|
|
65
|
+
task(
|
|
66
|
+
role: "Prompt Contract Reviewer",
|
|
67
|
+
assignment: "# Target\npackages/coding-agent/src/prompts/**\n# Change\nCheck active-tool guidance. Shared contract: read-only; return mismatches and exact prompt lines.\n# Acceptance\nReturn confirmed findings only..."
|
|
68
|
+
)
|
|
69
|
+
{{/if}}
|
|
70
|
+
|
|
71
|
+
{{#if taskBatch}}Prefer one wide batch over serial subagent calls when work items do not share files. If tasks overlap, name the overlap and have agents coordinate through IRC before editing.{{else}}Prefer issuing all independent task calls in one assistant turn over serial dispatch when work items do not share files. If tasks overlap, name the overlap and have agents coordinate through IRC before editing.{{/if}}
|
|
48
72
|
</structure>
|
|
49
73
|
|
|
50
74
|
<patterns>
|
|
51
|
-
|
|
52
|
-
- **
|
|
53
|
-
- **
|
|
54
|
-
- **
|
|
55
|
-
- **
|
|
56
|
-
- **Multi-modal sweep** — parallel finders each searching a different way (by-container, by-content, by-entity, by-time), each blind to the others.
|
|
57
|
-
- **Completeness critic** — a final agent that asks "what's missing — modality not run, claim unverified, file unread?"; its answer is the next round.
|
|
58
|
-
- **Budget/count loops** — `while len(bugs) < 10:` to hit a target, or `while budget.total and budget.remaining() > 50_000:` to scale depth to the turn budget; `log()` each round.
|
|
59
|
-
- **No silent caps** — if you bound coverage (top-N, no-retry, sampling), `log()` what you dropped; silent truncation reads as "covered everything" when it didn't.
|
|
60
|
-
|
|
61
|
-
Scale to the ask: "find any bugs" → a few finders, single-vote verify. "thoroughly audit / be comprehensive" → larger finder pool, 3–5-vote adversarial pass, a synthesis stage.
|
|
75
|
+
- **Adversarial verify** — dispatch skeptical reviewers with distinct targets, then keep only findings the parent can verify against source.
|
|
76
|
+
- **Perspective-diverse review** — use separate correctness, security, performance, and maintainability roles instead of identical reviewers.
|
|
77
|
+
- **Completeness critic** — after the first batch, dispatch one read-only critic that asks what modality, file, claim, or proof was missed.
|
|
78
|
+
- **No silent caps** — if you bound coverage (top-N, no retry, sampling), state what was dropped and why before acting.
|
|
79
|
+
- **Parent owns closure** — subagents return evidence; the parent reads it, resolves contradictions, runs proof, and makes the final decision.
|
|
62
80
|
</patterns>
|
|
63
81
|
|
|
64
82
|
<execution>
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
83
|
+
- Capture multi-phase workflow state in the visible todo system when available.
|
|
84
|
+
{{#if taskBatch}}- Batch independent subagents in one `task` call.{{else}}- Dispatch independent subagents as separate `task` calls in the same turn.{{/if}}
|
|
85
|
+
- Give every subagent a narrow target, explicit non-goals, and a concrete return packet.
|
|
86
|
+
- After fan-out returns, read the artifacts, patch or decide, and run the shared gate.
|
|
87
|
+
- Keep going until the task is closed — returned fan-out is a step, not a stopping point.
|
|
69
88
|
</execution>
|
|
70
89
|
</system-notice>
|