@oh-my-pi/pi-coding-agent 5.5.0 → 5.6.7
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 +98 -0
- package/docs/python-repl.md +77 -0
- package/examples/hooks/snake.ts +7 -7
- package/package.json +5 -5
- package/src/bun-imports.d.ts +6 -0
- package/src/cli/args.ts +7 -0
- package/src/cli/setup-cli.ts +231 -0
- package/src/cli.ts +2 -0
- package/src/core/agent-session.ts +118 -15
- package/src/core/bash-executor.ts +3 -84
- package/src/core/compaction/compaction.ts +10 -5
- package/src/core/extensions/index.ts +2 -0
- package/src/core/extensions/loader.ts +13 -1
- package/src/core/extensions/runner.ts +50 -2
- package/src/core/extensions/types.ts +67 -2
- package/src/core/keybindings.ts +51 -1
- package/src/core/prompt-templates.ts +15 -0
- package/src/core/python-executor-display.test.ts +42 -0
- package/src/core/python-executor-lifecycle.test.ts +99 -0
- package/src/core/python-executor-mapping.test.ts +41 -0
- package/src/core/python-executor-per-call.test.ts +49 -0
- package/src/core/python-executor-session.test.ts +103 -0
- package/src/core/python-executor-streaming.test.ts +77 -0
- package/src/core/python-executor-timeout.test.ts +35 -0
- package/src/core/python-executor.lifecycle.test.ts +139 -0
- package/src/core/python-executor.result.test.ts +49 -0
- package/src/core/python-executor.test.ts +180 -0
- package/src/core/python-executor.ts +313 -0
- package/src/core/python-gateway-coordinator.ts +832 -0
- package/src/core/python-kernel-display.test.ts +54 -0
- package/src/core/python-kernel-env.test.ts +138 -0
- package/src/core/python-kernel-session.test.ts +87 -0
- package/src/core/python-kernel-ws.test.ts +104 -0
- package/src/core/python-kernel.lifecycle.test.ts +249 -0
- package/src/core/python-kernel.test.ts +549 -0
- package/src/core/python-kernel.ts +1178 -0
- package/src/core/python-prelude.py +889 -0
- package/src/core/python-prelude.test.ts +140 -0
- package/src/core/python-prelude.ts +3 -0
- package/src/core/sdk.ts +24 -6
- package/src/core/session-manager.ts +174 -82
- package/src/core/settings-manager-python.test.ts +23 -0
- package/src/core/settings-manager.ts +202 -0
- package/src/core/streaming-output.test.ts +26 -0
- package/src/core/streaming-output.ts +100 -0
- package/src/core/system-prompt.python.test.ts +17 -0
- package/src/core/system-prompt.ts +3 -1
- package/src/core/timings.ts +1 -1
- package/src/core/tools/bash.ts +13 -2
- package/src/core/tools/edit-diff.ts +9 -1
- package/src/core/tools/index.test.ts +50 -23
- package/src/core/tools/index.ts +83 -1
- package/src/core/tools/python-execution.test.ts +68 -0
- package/src/core/tools/python-fallback.test.ts +72 -0
- package/src/core/tools/python-renderer.test.ts +36 -0
- package/src/core/tools/python-tool-mode.test.ts +43 -0
- package/src/core/tools/python.test.ts +121 -0
- package/src/core/tools/python.ts +760 -0
- package/src/core/tools/renderers.ts +2 -0
- package/src/core/tools/schema-validation.test.ts +1 -0
- package/src/core/tools/task/executor.ts +146 -3
- package/src/core/tools/task/worker-protocol.ts +32 -2
- package/src/core/tools/task/worker.ts +182 -15
- package/src/index.ts +6 -0
- package/src/main.ts +136 -40
- package/src/modes/interactive/components/custom-editor.ts +16 -31
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +5 -16
- package/src/modes/interactive/components/extensions/extension-list.ts +5 -13
- package/src/modes/interactive/components/history-search.ts +5 -8
- package/src/modes/interactive/components/hook-editor.ts +3 -4
- package/src/modes/interactive/components/hook-input.ts +3 -3
- package/src/modes/interactive/components/hook-selector.ts +5 -15
- package/src/modes/interactive/components/index.ts +1 -0
- package/src/modes/interactive/components/keybinding-hints.ts +66 -0
- package/src/modes/interactive/components/model-selector.ts +53 -66
- package/src/modes/interactive/components/oauth-selector.ts +5 -5
- package/src/modes/interactive/components/session-selector.ts +29 -23
- package/src/modes/interactive/components/settings-defs.ts +404 -196
- package/src/modes/interactive/components/settings-selector.ts +14 -10
- package/src/modes/interactive/components/status-line-segment-editor.ts +7 -7
- package/src/modes/interactive/components/tool-execution.ts +8 -0
- package/src/modes/interactive/components/tree-selector.ts +29 -23
- package/src/modes/interactive/components/user-message-selector.ts +6 -17
- package/src/modes/interactive/controllers/command-controller.ts +86 -37
- package/src/modes/interactive/controllers/event-controller.ts +8 -0
- package/src/modes/interactive/controllers/extension-ui-controller.ts +51 -0
- package/src/modes/interactive/controllers/input-controller.ts +42 -6
- package/src/modes/interactive/interactive-mode.ts +56 -30
- package/src/modes/interactive/theme/theme-schema.json +2 -2
- package/src/modes/interactive/types.ts +6 -1
- package/src/modes/interactive/utils/ui-helpers.ts +2 -1
- package/src/modes/print-mode.ts +23 -0
- package/src/modes/rpc/rpc-mode.ts +21 -0
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/system/system-prompt.md +32 -1
- package/src/prompts/tools/python.md +91 -0
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
calculateContextTokens,
|
|
26
26
|
collectEntriesForBranchSummary,
|
|
27
27
|
compact,
|
|
28
|
+
estimateTokens,
|
|
28
29
|
generateBranchSummary,
|
|
29
30
|
prepareCompaction,
|
|
30
31
|
shouldCompact,
|
|
@@ -43,6 +44,7 @@ import type {
|
|
|
43
44
|
TurnEndEvent,
|
|
44
45
|
TurnStartEvent,
|
|
45
46
|
} from "./extensions";
|
|
47
|
+
import type { CompactOptions, ContextUsage } from "./extensions/types";
|
|
46
48
|
import { extractFileMentions, generateFileMentionMessages } from "./file-mentions";
|
|
47
49
|
import type { HookCommandContext } from "./hooks/types";
|
|
48
50
|
import { logger } from "./logger";
|
|
@@ -65,7 +67,13 @@ import type { TtsrManager } from "./ttsr";
|
|
|
65
67
|
export type AgentSessionEvent =
|
|
66
68
|
| AgentEvent
|
|
67
69
|
| { type: "auto_compaction_start"; reason: "threshold" | "overflow" }
|
|
68
|
-
| {
|
|
70
|
+
| {
|
|
71
|
+
type: "auto_compaction_end";
|
|
72
|
+
result: CompactionResult | undefined;
|
|
73
|
+
aborted: boolean;
|
|
74
|
+
willRetry: boolean;
|
|
75
|
+
errorMessage?: string;
|
|
76
|
+
}
|
|
69
77
|
| { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
|
|
70
78
|
| { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }
|
|
71
79
|
| { type: "ttsr_triggered"; rules: Rule[] }
|
|
@@ -906,6 +914,7 @@ export class AgentSession {
|
|
|
906
914
|
process.exit(0);
|
|
907
915
|
},
|
|
908
916
|
hasQueuedMessages: () => this.queuedMessageCount > 0,
|
|
917
|
+
getContextUsage: () => this.getContextUsage(),
|
|
909
918
|
waitForIdle: () => this.agent.waitForIdle(),
|
|
910
919
|
newSession: async (options) => {
|
|
911
920
|
const success = await this.newSession({ parentSession: options?.parentSession });
|
|
@@ -925,6 +934,12 @@ export class AgentSession {
|
|
|
925
934
|
const result = await this.navigateTree(targetId, { summarize: options?.summarize });
|
|
926
935
|
return { cancelled: result.cancelled };
|
|
927
936
|
},
|
|
937
|
+
compact: async (instructionsOrOptions) => {
|
|
938
|
+
const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
|
|
939
|
+
const options =
|
|
940
|
+
instructionsOrOptions && typeof instructionsOrOptions === "object" ? instructionsOrOptions : undefined;
|
|
941
|
+
await this.compact(instructions, options);
|
|
942
|
+
},
|
|
928
943
|
};
|
|
929
944
|
}
|
|
930
945
|
|
|
@@ -967,7 +982,7 @@ export class AgentSession {
|
|
|
967
982
|
});
|
|
968
983
|
} else {
|
|
969
984
|
const message = err instanceof Error ? err.message : String(err);
|
|
970
|
-
|
|
985
|
+
logger.error("Custom command failed", { commandName, error: message });
|
|
971
986
|
}
|
|
972
987
|
return ""; // Command was handled (with error)
|
|
973
988
|
}
|
|
@@ -1533,8 +1548,9 @@ export class AgentSession {
|
|
|
1533
1548
|
* Manually compact the session context.
|
|
1534
1549
|
* Aborts current agent operation first.
|
|
1535
1550
|
* @param customInstructions Optional instructions for the compaction summary
|
|
1551
|
+
* @param options Optional callbacks for completion/error handling
|
|
1536
1552
|
*/
|
|
1537
|
-
async compact(customInstructions?: string): Promise<CompactionResult> {
|
|
1553
|
+
async compact(customInstructions?: string, options?: CompactOptions): Promise<CompactionResult> {
|
|
1538
1554
|
this._disconnectFromAgent();
|
|
1539
1555
|
await this.abort();
|
|
1540
1556
|
this._compactionAbortController = new AbortController();
|
|
@@ -1632,12 +1648,18 @@ export class AgentSession {
|
|
|
1632
1648
|
});
|
|
1633
1649
|
}
|
|
1634
1650
|
|
|
1635
|
-
|
|
1651
|
+
const compactionResult: CompactionResult = {
|
|
1636
1652
|
summary,
|
|
1637
1653
|
firstKeptEntryId,
|
|
1638
1654
|
tokensBefore,
|
|
1639
1655
|
details,
|
|
1640
1656
|
};
|
|
1657
|
+
options?.onComplete?.(compactionResult);
|
|
1658
|
+
return compactionResult;
|
|
1659
|
+
} catch (error) {
|
|
1660
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1661
|
+
options?.onError?.(err);
|
|
1662
|
+
throw error;
|
|
1641
1663
|
} finally {
|
|
1642
1664
|
this._compactionAbortController = undefined;
|
|
1643
1665
|
this._reconnectToAgent();
|
|
@@ -2046,15 +2068,17 @@ export class AgentSession {
|
|
|
2046
2068
|
}, 100);
|
|
2047
2069
|
}
|
|
2048
2070
|
} catch (error) {
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2071
|
+
const errorMessage = error instanceof Error ? error.message : "compaction failed";
|
|
2072
|
+
this._emit({
|
|
2073
|
+
type: "auto_compaction_end",
|
|
2074
|
+
result: undefined,
|
|
2075
|
+
aborted: false,
|
|
2076
|
+
willRetry: false,
|
|
2077
|
+
errorMessage:
|
|
2078
|
+
reason === "overflow"
|
|
2079
|
+
? `Context overflow recovery failed: ${errorMessage}`
|
|
2080
|
+
: `Auto-compaction failed: ${errorMessage}`,
|
|
2081
|
+
});
|
|
2058
2082
|
} finally {
|
|
2059
2083
|
this._autoCompactionAbortController = undefined;
|
|
2060
2084
|
}
|
|
@@ -2092,8 +2116,8 @@ export class AgentSession {
|
|
|
2092
2116
|
}
|
|
2093
2117
|
|
|
2094
2118
|
private _isRetryableErrorMessage(errorMessage: string): boolean {
|
|
2095
|
-
// Match: overloaded_error, rate limit, usage limit, 429, 500, 502, 503, 504, service unavailable, connection error
|
|
2096
|
-
return /overloaded|rate.?limit|usage.?limit|too many requests|429|500|502|503|504|service.?unavailable|server error|internal error|connection.?error/i.test(
|
|
2119
|
+
// Match: overloaded_error, rate limit, usage limit, 429, 500, 502, 503, 504, service unavailable, connection error, fetch failed
|
|
2120
|
+
return /overloaded|rate.?limit|usage.?limit|too many requests|429|500|502|503|504|service.?unavailable|server error|internal error|connection.?error|fetch failed/i.test(
|
|
2097
2121
|
errorMessage,
|
|
2098
2122
|
);
|
|
2099
2123
|
}
|
|
@@ -2804,6 +2828,85 @@ export class AgentSession {
|
|
|
2804
2828
|
};
|
|
2805
2829
|
}
|
|
2806
2830
|
|
|
2831
|
+
/**
|
|
2832
|
+
* Get current context usage statistics.
|
|
2833
|
+
* Uses the last assistant message's usage data when available,
|
|
2834
|
+
* otherwise estimates tokens for all messages.
|
|
2835
|
+
*/
|
|
2836
|
+
getContextUsage(): ContextUsage | undefined {
|
|
2837
|
+
const model = this.model;
|
|
2838
|
+
if (!model) return undefined;
|
|
2839
|
+
|
|
2840
|
+
const contextWindow = model.contextWindow ?? 0;
|
|
2841
|
+
if (contextWindow <= 0) return undefined;
|
|
2842
|
+
|
|
2843
|
+
const estimate = this._estimateContextTokens();
|
|
2844
|
+
const percent = (estimate.tokens / contextWindow) * 100;
|
|
2845
|
+
|
|
2846
|
+
return {
|
|
2847
|
+
tokens: estimate.tokens,
|
|
2848
|
+
contextWindow,
|
|
2849
|
+
percent,
|
|
2850
|
+
usageTokens: estimate.usageTokens,
|
|
2851
|
+
trailingTokens: estimate.trailingTokens,
|
|
2852
|
+
lastUsageIndex: estimate.lastUsageIndex,
|
|
2853
|
+
};
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
/**
|
|
2857
|
+
* Estimate context tokens from messages, using the last assistant usage when available.
|
|
2858
|
+
*/
|
|
2859
|
+
private _estimateContextTokens(): {
|
|
2860
|
+
tokens: number;
|
|
2861
|
+
usageTokens: number;
|
|
2862
|
+
trailingTokens: number;
|
|
2863
|
+
lastUsageIndex: number | null;
|
|
2864
|
+
} {
|
|
2865
|
+
const messages = this.messages;
|
|
2866
|
+
|
|
2867
|
+
// Find last assistant message with usage
|
|
2868
|
+
let lastUsageIndex: number | null = null;
|
|
2869
|
+
let lastUsage: Usage | undefined;
|
|
2870
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2871
|
+
const msg = messages[i];
|
|
2872
|
+
if (msg.role === "assistant") {
|
|
2873
|
+
const assistantMsg = msg as AssistantMessage;
|
|
2874
|
+
if (assistantMsg.usage) {
|
|
2875
|
+
lastUsage = assistantMsg.usage;
|
|
2876
|
+
lastUsageIndex = i;
|
|
2877
|
+
break;
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
if (!lastUsage || lastUsageIndex === null) {
|
|
2883
|
+
// No usage data - estimate all messages
|
|
2884
|
+
let estimated = 0;
|
|
2885
|
+
for (const message of messages) {
|
|
2886
|
+
estimated += estimateTokens(message);
|
|
2887
|
+
}
|
|
2888
|
+
return {
|
|
2889
|
+
tokens: estimated,
|
|
2890
|
+
usageTokens: 0,
|
|
2891
|
+
trailingTokens: estimated,
|
|
2892
|
+
lastUsageIndex: null,
|
|
2893
|
+
};
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
const usageTokens = calculateContextTokens(lastUsage);
|
|
2897
|
+
let trailingTokens = 0;
|
|
2898
|
+
for (let i = lastUsageIndex + 1; i < messages.length; i++) {
|
|
2899
|
+
trailingTokens += estimateTokens(messages[i]);
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
return {
|
|
2903
|
+
tokens: usageTokens + trailingTokens,
|
|
2904
|
+
usageTokens,
|
|
2905
|
+
trailingTokens,
|
|
2906
|
+
lastUsageIndex,
|
|
2907
|
+
};
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2807
2910
|
/**
|
|
2808
2911
|
* Export session to HTML.
|
|
2809
2912
|
* @param outputPath Optional output path (defaults to session directory)
|
|
@@ -6,16 +6,12 @@
|
|
|
6
6
|
* - Direct calls from modes that need bash execution
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { createWriteStream, type WriteStream } from "node:fs";
|
|
10
|
-
import { tmpdir } from "node:os";
|
|
11
|
-
import { join } from "node:path";
|
|
12
9
|
import type { Subprocess } from "bun";
|
|
13
|
-
import {
|
|
14
|
-
import stripAnsi from "strip-ansi";
|
|
15
|
-
import { getShellConfig, killProcessTree, sanitizeBinaryOutput } from "../utils/shell";
|
|
10
|
+
import { getShellConfig, killProcessTree } from "../utils/shell";
|
|
16
11
|
import { getOrCreateSnapshot, getSnapshotSourceCommand } from "../utils/shell-snapshot";
|
|
12
|
+
import { createOutputSink, pumpStream } from "./streaming-output";
|
|
17
13
|
import type { BashOperations } from "./tools/bash";
|
|
18
|
-
import { DEFAULT_MAX_BYTES
|
|
14
|
+
import { DEFAULT_MAX_BYTES } from "./tools/truncate";
|
|
19
15
|
import { ScopeSignal } from "./utils";
|
|
20
16
|
|
|
21
17
|
// ============================================================================
|
|
@@ -50,83 +46,6 @@ export interface BashResult {
|
|
|
50
46
|
// Implementation
|
|
51
47
|
// ============================================================================
|
|
52
48
|
|
|
53
|
-
function createSanitizer(): TransformStream<Uint8Array, string> {
|
|
54
|
-
const decoder = new TextDecoder();
|
|
55
|
-
return new TransformStream({
|
|
56
|
-
transform(chunk, controller) {
|
|
57
|
-
const text = sanitizeBinaryOutput(stripAnsi(decoder.decode(chunk, { stream: true }))).replace(/\r/g, "");
|
|
58
|
-
controller.enqueue(text);
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function pumpStream(readable: ReadableStream<Uint8Array>, writer: WritableStreamDefaultWriter<string>) {
|
|
64
|
-
const reader = readable.pipeThrough(createSanitizer()).getReader();
|
|
65
|
-
try {
|
|
66
|
-
while (true) {
|
|
67
|
-
const { done, value } = await reader.read();
|
|
68
|
-
if (done) break;
|
|
69
|
-
await writer.write(value);
|
|
70
|
-
}
|
|
71
|
-
} finally {
|
|
72
|
-
reader.releaseLock();
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function createOutputSink(
|
|
77
|
-
spillThreshold: number,
|
|
78
|
-
maxBuffer: number,
|
|
79
|
-
onChunk?: (text: string) => void,
|
|
80
|
-
): WritableStream<string> & {
|
|
81
|
-
dump: (annotation?: string) => { output: string; truncated: boolean; fullOutputPath?: string };
|
|
82
|
-
} {
|
|
83
|
-
const chunks: string[] = [];
|
|
84
|
-
let chunkBytes = 0;
|
|
85
|
-
let totalBytes = 0;
|
|
86
|
-
let fullOutputPath: string | undefined;
|
|
87
|
-
let fullOutputStream: WriteStream | undefined;
|
|
88
|
-
|
|
89
|
-
const sink = new WritableStream<string>({
|
|
90
|
-
write(text) {
|
|
91
|
-
totalBytes += text.length;
|
|
92
|
-
|
|
93
|
-
// Spill to temp file if needed
|
|
94
|
-
if (totalBytes > spillThreshold && !fullOutputPath) {
|
|
95
|
-
fullOutputPath = join(tmpdir(), `omp-${nanoid()}.buffer`);
|
|
96
|
-
const ts = createWriteStream(fullOutputPath);
|
|
97
|
-
chunks.forEach((c) => {
|
|
98
|
-
ts.write(c);
|
|
99
|
-
});
|
|
100
|
-
fullOutputStream = ts;
|
|
101
|
-
}
|
|
102
|
-
fullOutputStream?.write(text);
|
|
103
|
-
|
|
104
|
-
// Rolling buffer
|
|
105
|
-
chunks.push(text);
|
|
106
|
-
chunkBytes += text.length;
|
|
107
|
-
while (chunkBytes > maxBuffer && chunks.length > 1) {
|
|
108
|
-
chunkBytes -= chunks.shift()!.length;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
onChunk?.(text);
|
|
112
|
-
},
|
|
113
|
-
close() {
|
|
114
|
-
fullOutputStream?.end();
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
return Object.assign(sink, {
|
|
119
|
-
dump(annotation?: string) {
|
|
120
|
-
if (annotation) {
|
|
121
|
-
chunks.push(`\n\n${annotation}`);
|
|
122
|
-
}
|
|
123
|
-
const full = chunks.join("");
|
|
124
|
-
const { content, truncated } = truncateTail(full);
|
|
125
|
-
return { output: truncated ? content : full, truncated, fullOutputPath: fullOutputPath };
|
|
126
|
-
},
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
49
|
/**
|
|
131
50
|
* Execute a bash command with optional streaming and cancellation support.
|
|
132
51
|
*
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
9
9
|
import type { AssistantMessage, Model, Usage } from "@oh-my-pi/pi-ai";
|
|
10
|
-
import {
|
|
10
|
+
import { completeSimple } from "@oh-my-pi/pi-ai";
|
|
11
11
|
import compactionSummaryPrompt from "../../prompts/compaction/compaction-summary.md" with { type: "text" };
|
|
12
12
|
import compactionTurnPrefixPrompt from "../../prompts/compaction/compaction-turn-prefix.md" with { type: "text" };
|
|
13
13
|
import compactionUpdateSummaryPrompt from "../../prompts/compaction/compaction-update-summary.md" with { type: "text" };
|
|
@@ -642,17 +642,22 @@ async function generateTurnPrefixSummary(
|
|
|
642
642
|
): Promise<string> {
|
|
643
643
|
const maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix
|
|
644
644
|
|
|
645
|
-
const
|
|
645
|
+
const llmMessages = convertToLlm(messages);
|
|
646
|
+
const conversationText = serializeConversation(llmMessages);
|
|
647
|
+
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
|
646
648
|
const summarizationMessages = [
|
|
647
|
-
...transformedMessages,
|
|
648
649
|
{
|
|
649
650
|
role: "user" as const,
|
|
650
|
-
content: [{ type: "text" as const, text:
|
|
651
|
+
content: [{ type: "text" as const, text: promptText }],
|
|
651
652
|
timestamp: Date.now(),
|
|
652
653
|
},
|
|
653
654
|
];
|
|
654
655
|
|
|
655
|
-
const response = await
|
|
656
|
+
const response = await completeSimple(
|
|
657
|
+
model,
|
|
658
|
+
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
|
|
659
|
+
{ maxTokens, signal, apiKey, reasoning: "high" },
|
|
660
|
+
);
|
|
656
661
|
|
|
657
662
|
if (response.stopReason === "error") {
|
|
658
663
|
throw new Error(`Turn prefix summarization failed: ${response.errorMessage || "Unknown error"}`);
|
|
@@ -49,6 +49,7 @@ export function createExtensionRuntime(): ExtensionRuntime {
|
|
|
49
49
|
sendMessage: notInitialized,
|
|
50
50
|
sendUserMessage: notInitialized,
|
|
51
51
|
appendEntry: notInitialized,
|
|
52
|
+
setLabel: notInitialized,
|
|
52
53
|
getActiveTools: notInitialized,
|
|
53
54
|
getAllTools: notInitialized,
|
|
54
55
|
setActiveTools: notInitialized,
|
|
@@ -88,10 +89,21 @@ function createExtensionAPI(
|
|
|
88
89
|
});
|
|
89
90
|
},
|
|
90
91
|
|
|
91
|
-
registerCommand(
|
|
92
|
+
registerCommand(
|
|
93
|
+
name: string,
|
|
94
|
+
options: {
|
|
95
|
+
description?: string;
|
|
96
|
+
getArgumentCompletions?: RegisteredCommand["getArgumentCompletions"];
|
|
97
|
+
handler: RegisteredCommand["handler"];
|
|
98
|
+
},
|
|
99
|
+
): void {
|
|
92
100
|
extension.commands.set(name, { name, ...options });
|
|
93
101
|
},
|
|
94
102
|
|
|
103
|
+
setLabel(label: string): void {
|
|
104
|
+
extension.label = label;
|
|
105
|
+
},
|
|
106
|
+
|
|
95
107
|
registerShortcut(
|
|
96
108
|
shortcut: KeyId,
|
|
97
109
|
options: {
|
|
@@ -12,8 +12,10 @@ import type { SessionManager } from "../session-manager";
|
|
|
12
12
|
import type {
|
|
13
13
|
BeforeAgentStartEvent,
|
|
14
14
|
BeforeAgentStartEventResult,
|
|
15
|
+
CompactOptions,
|
|
15
16
|
ContextEvent,
|
|
16
17
|
ContextEventResult,
|
|
18
|
+
ContextUsage,
|
|
17
19
|
Extension,
|
|
18
20
|
ExtensionActions,
|
|
19
21
|
ExtensionCommandContext,
|
|
@@ -26,6 +28,8 @@ import type {
|
|
|
26
28
|
ExtensionRuntime,
|
|
27
29
|
ExtensionShortcut,
|
|
28
30
|
ExtensionUIContext,
|
|
31
|
+
InputEvent,
|
|
32
|
+
InputEventResult,
|
|
29
33
|
MessageRenderer,
|
|
30
34
|
RegisteredCommand,
|
|
31
35
|
RegisteredTool,
|
|
@@ -110,6 +114,8 @@ export class ExtensionRunner {
|
|
|
110
114
|
private waitForIdleFn: () => Promise<void> = async () => {};
|
|
111
115
|
private abortFn: () => void = () => {};
|
|
112
116
|
private hasPendingMessagesFn: () => boolean = () => false;
|
|
117
|
+
private getContextUsageFn: () => ContextUsage | undefined = () => undefined;
|
|
118
|
+
private compactFn: (instructionsOrOptions?: string | CompactOptions) => Promise<void> = async () => {};
|
|
113
119
|
private newSessionHandler: NewSessionHandler = async () => ({ cancelled: false });
|
|
114
120
|
private branchHandler: BranchHandler = async () => ({ cancelled: false });
|
|
115
121
|
private navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });
|
|
@@ -160,6 +166,8 @@ export class ExtensionRunner {
|
|
|
160
166
|
this.newSessionHandler = commandContextActions.newSession;
|
|
161
167
|
this.branchHandler = commandContextActions.branch;
|
|
162
168
|
this.navigateTreeHandler = commandContextActions.navigateTree;
|
|
169
|
+
this.getContextUsageFn = commandContextActions.getContextUsage;
|
|
170
|
+
this.compactFn = commandContextActions.compact;
|
|
163
171
|
}
|
|
164
172
|
|
|
165
173
|
this.uiContext = uiContext ?? noOpUIContext;
|
|
@@ -299,18 +307,23 @@ export class ExtensionRunner {
|
|
|
299
307
|
}
|
|
300
308
|
|
|
301
309
|
createContext(): ExtensionContext {
|
|
310
|
+
const getModel = this.getModel;
|
|
302
311
|
return {
|
|
303
312
|
ui: this.uiContext,
|
|
313
|
+
getContextUsage: () => this.getContextUsageFn(),
|
|
314
|
+
compact: (instructionsOrOptions) => this.compactFn(instructionsOrOptions),
|
|
304
315
|
hasUI: this.hasUI(),
|
|
305
316
|
cwd: this.cwd,
|
|
306
317
|
sessionManager: this.sessionManager,
|
|
307
318
|
modelRegistry: this.modelRegistry,
|
|
308
|
-
model
|
|
319
|
+
get model() {
|
|
320
|
+
return getModel();
|
|
321
|
+
},
|
|
309
322
|
isIdle: () => this.isIdleFn(),
|
|
310
323
|
abort: () => this.abortFn(),
|
|
311
324
|
hasPendingMessages: () => this.hasPendingMessagesFn(),
|
|
312
325
|
shutdown: () => this.shutdownHandler(),
|
|
313
|
-
hasQueuedMessages: () => this.hasPendingMessagesFn(),
|
|
326
|
+
hasQueuedMessages: () => this.hasPendingMessagesFn(), // deprecated alias
|
|
314
327
|
};
|
|
315
328
|
}
|
|
316
329
|
|
|
@@ -324,10 +337,12 @@ export class ExtensionRunner {
|
|
|
324
337
|
createCommandContext(): ExtensionCommandContext {
|
|
325
338
|
return {
|
|
326
339
|
...this.createContext(),
|
|
340
|
+
getContextUsage: () => this.getContextUsageFn(),
|
|
327
341
|
waitForIdle: () => this.waitForIdleFn(),
|
|
328
342
|
newSession: (options) => this.newSessionHandler(options),
|
|
329
343
|
branch: (entryId) => this.branchHandler(entryId),
|
|
330
344
|
navigateTree: (targetId, options) => this.navigateTreeHandler(targetId, options),
|
|
345
|
+
compact: (instructionsOrOptions) => this.compactFn(instructionsOrOptions),
|
|
331
346
|
};
|
|
332
347
|
}
|
|
333
348
|
|
|
@@ -446,6 +461,39 @@ export class ExtensionRunner {
|
|
|
446
461
|
return undefined;
|
|
447
462
|
}
|
|
448
463
|
|
|
464
|
+
/** Emit input event. Transforms chain, "handled" short-circuits. */
|
|
465
|
+
async emitInput(
|
|
466
|
+
text: string,
|
|
467
|
+
images: ImageContent[] | undefined,
|
|
468
|
+
source: "interactive" | "rpc" | "extension",
|
|
469
|
+
): Promise<InputEventResult> {
|
|
470
|
+
const ctx = this.createContext();
|
|
471
|
+
let currentText = text;
|
|
472
|
+
let currentImages = images;
|
|
473
|
+
|
|
474
|
+
for (const ext of this.extensions) {
|
|
475
|
+
for (const handler of ext.handlers.get("input") ?? []) {
|
|
476
|
+
try {
|
|
477
|
+
const event: InputEvent = { type: "input", text: currentText, images: currentImages, source };
|
|
478
|
+
const result = (await handler(event, ctx)) as InputEventResult | undefined;
|
|
479
|
+
if (result?.handled) return result;
|
|
480
|
+
if (result?.text !== undefined) {
|
|
481
|
+
currentText = result.text;
|
|
482
|
+
currentImages = result.images ?? currentImages;
|
|
483
|
+
}
|
|
484
|
+
} catch (err) {
|
|
485
|
+
this.emitError({
|
|
486
|
+
extensionPath: ext.path,
|
|
487
|
+
event: "input",
|
|
488
|
+
error: err instanceof Error ? err.message : String(err),
|
|
489
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return currentText !== text || currentImages !== images ? { text: currentText, images: currentImages } : {};
|
|
495
|
+
}
|
|
496
|
+
|
|
449
497
|
async emitContext(messages: AgentMessage[]): Promise<AgentMessage[]> {
|
|
450
498
|
const ctx = this.createContext();
|
|
451
499
|
let currentMessages = structuredClone(messages);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
12
12
|
import type { ImageContent, Model, TextContent, ToolResultMessage } from "@oh-my-pi/pi-ai";
|
|
13
|
-
import type { Component, EditorComponent, EditorTheme, KeyId, TUI } from "@oh-my-pi/pi-tui";
|
|
13
|
+
import type { AutocompleteItem, Component, EditorComponent, EditorTheme, KeyId, TUI } from "@oh-my-pi/pi-tui";
|
|
14
14
|
import type { Static, TSchema } from "@sinclair/typebox";
|
|
15
15
|
import type * as piCodingAgent from "../../index";
|
|
16
16
|
import type { Theme } from "../../modes/interactive/theme/theme";
|
|
@@ -125,12 +125,30 @@ export interface ExtensionUIContext {
|
|
|
125
125
|
// Extension Context
|
|
126
126
|
// ============================================================================
|
|
127
127
|
|
|
128
|
+
export interface ContextUsage {
|
|
129
|
+
tokens: number;
|
|
130
|
+
contextWindow: number;
|
|
131
|
+
percent: number;
|
|
132
|
+
usageTokens: number;
|
|
133
|
+
trailingTokens: number;
|
|
134
|
+
lastUsageIndex: number | null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface CompactOptions {
|
|
138
|
+
onComplete?: (result: CompactionResult) => void;
|
|
139
|
+
onError?: (error: Error) => void;
|
|
140
|
+
}
|
|
141
|
+
|
|
128
142
|
/**
|
|
129
143
|
* Context passed to extension event handlers.
|
|
130
144
|
*/
|
|
131
145
|
export interface ExtensionContext {
|
|
132
146
|
/** UI methods for user interaction */
|
|
133
147
|
ui: ExtensionUIContext;
|
|
148
|
+
/** Get current context usage for the active model. */
|
|
149
|
+
getContextUsage(): ContextUsage | undefined;
|
|
150
|
+
/** Compact the session context (interactive mode shows UI). */
|
|
151
|
+
compact(instructionsOrOptions?: string | CompactOptions): Promise<void>;
|
|
134
152
|
/** Whether UI is available (false in print/RPC mode) */
|
|
135
153
|
hasUI: boolean;
|
|
136
154
|
/** Current working directory */
|
|
@@ -158,6 +176,9 @@ export interface ExtensionContext {
|
|
|
158
176
|
* Includes session control methods only safe in user-initiated commands.
|
|
159
177
|
*/
|
|
160
178
|
export interface ExtensionCommandContext extends ExtensionContext {
|
|
179
|
+
/** Get current context usage for the active model. */
|
|
180
|
+
getContextUsage(): ContextUsage | undefined;
|
|
181
|
+
|
|
161
182
|
/** Wait for the agent to finish streaming */
|
|
162
183
|
waitForIdle(): Promise<void>;
|
|
163
184
|
|
|
@@ -172,6 +193,9 @@ export interface ExtensionCommandContext extends ExtensionContext {
|
|
|
172
193
|
|
|
173
194
|
/** Navigate to a different point in the session tree. */
|
|
174
195
|
navigateTree(targetId: string, options?: { summarize?: boolean }): Promise<{ cancelled: boolean }>;
|
|
196
|
+
|
|
197
|
+
/** Compact the session context (interactive mode shows UI). */
|
|
198
|
+
compact(instructionsOrOptions?: string | CompactOptions): Promise<void>;
|
|
175
199
|
}
|
|
176
200
|
|
|
177
201
|
// ============================================================================
|
|
@@ -382,6 +406,18 @@ export interface UserBashEvent {
|
|
|
382
406
|
cwd: string;
|
|
383
407
|
}
|
|
384
408
|
|
|
409
|
+
// ============================================================================
|
|
410
|
+
// Input Events
|
|
411
|
+
// ============================================================================
|
|
412
|
+
|
|
413
|
+
/** Fired when the user submits input (interactive mode only). */
|
|
414
|
+
export interface InputEvent {
|
|
415
|
+
type: "input";
|
|
416
|
+
text: string;
|
|
417
|
+
images?: ImageContent[];
|
|
418
|
+
source: "interactive" | "rpc" | "extension";
|
|
419
|
+
}
|
|
420
|
+
|
|
385
421
|
// ============================================================================
|
|
386
422
|
// Tool Events
|
|
387
423
|
// ============================================================================
|
|
@@ -486,6 +522,7 @@ export type ExtensionEvent =
|
|
|
486
522
|
| TurnStartEvent
|
|
487
523
|
| TurnEndEvent
|
|
488
524
|
| UserBashEvent
|
|
525
|
+
| InputEvent
|
|
489
526
|
| ToolCallEvent
|
|
490
527
|
| ToolResultEvent;
|
|
491
528
|
|
|
@@ -502,6 +539,16 @@ export interface ToolCallEventResult {
|
|
|
502
539
|
reason?: string;
|
|
503
540
|
}
|
|
504
541
|
|
|
542
|
+
/** Result from input event handler */
|
|
543
|
+
export interface InputEventResult {
|
|
544
|
+
/** If true, the input was handled and should not continue through normal flow */
|
|
545
|
+
handled?: boolean;
|
|
546
|
+
/** Replace the input text */
|
|
547
|
+
text?: string;
|
|
548
|
+
/** Replace any pending images */
|
|
549
|
+
images?: ImageContent[];
|
|
550
|
+
}
|
|
551
|
+
|
|
505
552
|
/** Result from user_bash event handler */
|
|
506
553
|
export interface UserBashEventResult {
|
|
507
554
|
/** Custom operations to use for execution */
|
|
@@ -565,6 +612,7 @@ export type MessageRenderer<T = unknown> = (
|
|
|
565
612
|
export interface RegisteredCommand {
|
|
566
613
|
name: string;
|
|
567
614
|
description?: string;
|
|
615
|
+
getArgumentCompletions?: (argumentPrefix: string) => AutocompleteItem[] | null;
|
|
568
616
|
handler: (args: string, ctx: ExtensionCommandContext) => Promise<void>;
|
|
569
617
|
}
|
|
570
618
|
|
|
@@ -622,6 +670,7 @@ export interface ExtensionAPI {
|
|
|
622
670
|
on(event: "agent_end", handler: ExtensionHandler<AgentEndEvent>): void;
|
|
623
671
|
on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
|
|
624
672
|
on(event: "turn_end", handler: ExtensionHandler<TurnEndEvent>): void;
|
|
673
|
+
on(event: "input", handler: ExtensionHandler<InputEvent, InputEventResult>): void;
|
|
625
674
|
on(event: "tool_call", handler: ExtensionHandler<ToolCallEvent, ToolCallEventResult>): void;
|
|
626
675
|
on(event: "tool_result", handler: ExtensionHandler<ToolResultEvent, ToolResultEventResult>): void;
|
|
627
676
|
on(event: "user_bash", handler: ExtensionHandler<UserBashEvent, UserBashEventResult>): void;
|
|
@@ -638,7 +687,14 @@ export interface ExtensionAPI {
|
|
|
638
687
|
// =========================================================================
|
|
639
688
|
|
|
640
689
|
/** Register a custom command. */
|
|
641
|
-
registerCommand(
|
|
690
|
+
registerCommand(
|
|
691
|
+
name: string,
|
|
692
|
+
options: {
|
|
693
|
+
description?: string;
|
|
694
|
+
getArgumentCompletions?: RegisteredCommand["getArgumentCompletions"];
|
|
695
|
+
handler: RegisteredCommand["handler"];
|
|
696
|
+
},
|
|
697
|
+
): void;
|
|
642
698
|
|
|
643
699
|
/** Register a keyboard shortcut. */
|
|
644
700
|
registerShortcut(
|
|
@@ -659,6 +715,9 @@ export interface ExtensionAPI {
|
|
|
659
715
|
},
|
|
660
716
|
): void;
|
|
661
717
|
|
|
718
|
+
/** Set the display label for this extension, or set a label on a specific entry. */
|
|
719
|
+
setLabel(entryIdOrLabel: string, label?: string | undefined): void;
|
|
720
|
+
|
|
662
721
|
/** Get the value of a registered CLI flag. */
|
|
663
722
|
getFlag(name: string): boolean | string | undefined;
|
|
664
723
|
|
|
@@ -776,6 +835,7 @@ export interface ExtensionActions {
|
|
|
776
835
|
sendMessage: SendMessageHandler;
|
|
777
836
|
sendUserMessage: SendUserMessageHandler;
|
|
778
837
|
appendEntry: AppendEntryHandler;
|
|
838
|
+
setLabel: (targetId: string, label: string | undefined) => void;
|
|
779
839
|
getActiveTools: GetActiveToolsHandler;
|
|
780
840
|
getAllTools: GetAllToolsHandler;
|
|
781
841
|
setActiveTools: SetActiveToolsHandler;
|
|
@@ -791,10 +851,13 @@ export interface ExtensionContextActions {
|
|
|
791
851
|
abort: () => void;
|
|
792
852
|
hasPendingMessages: () => boolean;
|
|
793
853
|
shutdown: () => void;
|
|
854
|
+
getContextUsage: () => ContextUsage | undefined;
|
|
855
|
+
compact: (instructionsOrOptions?: string | CompactOptions) => Promise<void>;
|
|
794
856
|
}
|
|
795
857
|
|
|
796
858
|
/** Actions for ExtensionCommandContext (ctx.* in command handlers). */
|
|
797
859
|
export interface ExtensionCommandContextActions {
|
|
860
|
+
getContextUsage: () => ContextUsage | undefined;
|
|
798
861
|
waitForIdle: () => Promise<void>;
|
|
799
862
|
newSession: (options?: {
|
|
800
863
|
parentSession?: string;
|
|
@@ -802,6 +865,7 @@ export interface ExtensionCommandContextActions {
|
|
|
802
865
|
}) => Promise<{ cancelled: boolean }>;
|
|
803
866
|
branch: (entryId: string) => Promise<{ cancelled: boolean }>;
|
|
804
867
|
navigateTree: (targetId: string, options?: { summarize?: boolean }) => Promise<{ cancelled: boolean }>;
|
|
868
|
+
compact: (instructionsOrOptions?: string | CompactOptions) => Promise<void>;
|
|
805
869
|
}
|
|
806
870
|
|
|
807
871
|
/** Full runtime = state + actions. */
|
|
@@ -811,6 +875,7 @@ export interface ExtensionRuntime extends ExtensionRuntimeState, ExtensionAction
|
|
|
811
875
|
export interface Extension {
|
|
812
876
|
path: string;
|
|
813
877
|
resolvedPath: string;
|
|
878
|
+
label?: string;
|
|
814
879
|
handlers: Map<string, HandlerFn[]>;
|
|
815
880
|
tools: Map<string, RegisteredTool>;
|
|
816
881
|
messageRenderers: Map<string, MessageRenderer>;
|