localpi 0.4.0 → 0.5.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/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# localpi
|
|
2
2
|
|
|
3
|
-
Localpi is a
|
|
3
|
+
Localpi is a Swiss army knife for running Pi with local inference engines.
|
|
4
4
|
|
|
5
5
|
By default, Localpi discovers available local providers, lets you choose when more than one model is loaded, points Pi at the selected model, and writes Pi config for the other discovered models so `/model` can switch among them during the session.
|
|
6
6
|
|
|
7
|
-
Localpi
|
|
7
|
+
Localpi is meant to be the practical bridge from Pi to local inference stacks such as llama.cpp/`llama-server`, vLLM, SGLang, LM Studio, Ollama, and custom provider endpoints.
|
|
8
8
|
|
|
9
9
|
Localpi is intentionally generic. It does not contain classifier prompts, dataset workflows, GitHub routing logic, or final-schema output machinery. Structured classifier runs belong in caller tools such as `localpager-agent`.
|
|
10
10
|
|
package/dist/src/pi/app.js
CHANGED
|
@@ -4,6 +4,18 @@ const localpiAppIdentity = {
|
|
|
4
4
|
name: "localpi",
|
|
5
5
|
version: localpiVersion
|
|
6
6
|
};
|
|
7
|
+
const demoLaunchOverrideBooleanFlags = new Set([
|
|
8
|
+
"--no-tools",
|
|
9
|
+
"-nt",
|
|
10
|
+
"--no-builtin-tools",
|
|
11
|
+
"-nbt",
|
|
12
|
+
"--approve",
|
|
13
|
+
"-a",
|
|
14
|
+
"--no-approve",
|
|
15
|
+
"-na"
|
|
16
|
+
]);
|
|
17
|
+
const demoLaunchOverrideValueFlags = new Set(["--tools", "-t", "--exclude-tools", "-xt"]);
|
|
18
|
+
const demoLaunchOverrideEqualsFlags = ["--tools=", "--exclude-tools="];
|
|
7
19
|
export function createLocalpiAppDefinition(options, connection, extensions) {
|
|
8
20
|
return {
|
|
9
21
|
...localpiAppIdentity,
|
|
@@ -22,9 +34,37 @@ function appDirectories(options) {
|
|
|
22
34
|
function piCommand(options) {
|
|
23
35
|
return {
|
|
24
36
|
piCommand: options.piCommand,
|
|
25
|
-
forwardedArgs: options.forwardedArgs
|
|
37
|
+
forwardedArgs: options.demo ? demoForwardedArgs(options.forwardedArgs) : options.forwardedArgs
|
|
26
38
|
};
|
|
27
39
|
}
|
|
40
|
+
function demoForwardedArgs(args) {
|
|
41
|
+
return [...withoutConflictingDemoFlags(args), "--no-tools", "--no-approve"];
|
|
42
|
+
}
|
|
43
|
+
function withoutConflictingDemoFlags(args) {
|
|
44
|
+
const filtered = [];
|
|
45
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
46
|
+
const arg = args[index];
|
|
47
|
+
if (arg === undefined) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (isDemoLaunchOverrideFlag(arg)) {
|
|
51
|
+
if (isValueFlag(arg) && !arg.includes("=")) {
|
|
52
|
+
index += 1;
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
filtered.push(arg);
|
|
57
|
+
}
|
|
58
|
+
return filtered;
|
|
59
|
+
}
|
|
60
|
+
function isDemoLaunchOverrideFlag(arg) {
|
|
61
|
+
return (demoLaunchOverrideBooleanFlags.has(arg) ||
|
|
62
|
+
isValueFlag(arg) ||
|
|
63
|
+
demoLaunchOverrideEqualsFlags.some((flag) => arg.startsWith(flag)));
|
|
64
|
+
}
|
|
65
|
+
function isValueFlag(arg) {
|
|
66
|
+
return demoLaunchOverrideValueFlags.has(arg);
|
|
67
|
+
}
|
|
28
68
|
function runtimeSelection(options, connection) {
|
|
29
69
|
const selection = {
|
|
30
70
|
providers: providersForConnection(options, connection),
|
|
@@ -1,29 +1,67 @@
|
|
|
1
1
|
export function demoModeExtensionSource(prompts) {
|
|
2
2
|
const initialPromptSource = JSON.stringify(prompts.initial);
|
|
3
3
|
const followupPromptSource = JSON.stringify(prompts.followup);
|
|
4
|
-
return `import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
return `import type { ExtensionAPI, ExtensionContext, TurnEndEvent } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
|
|
6
6
|
const initialPrompt = ${initialPromptSource};
|
|
7
7
|
const followupPrompt = ${followupPromptSource};
|
|
8
|
+
const compactAtContextPercent = 70;
|
|
9
|
+
const demoCompactionInstructions = [
|
|
10
|
+
"Preserve the demo narrative state, named entities, current setting,",
|
|
11
|
+
"unresolved plot threads, and latest user direction.",
|
|
12
|
+
"Keep the summary concise so the story can continue after compaction."
|
|
13
|
+
].join(" ");
|
|
8
14
|
|
|
9
15
|
export default function localpiDemoMode(pi: ExtensionAPI): void {
|
|
10
16
|
let started = false;
|
|
11
17
|
let stopped = false;
|
|
18
|
+
let compacting = false;
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
if (started || stopped || event.reason !== "startup" || ctx.mode !== "tui") {
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
started = true;
|
|
20
|
+
function queueInitialPrompt(): void {
|
|
18
21
|
queueMicrotask(() => {
|
|
19
22
|
if (!stopped) {
|
|
20
23
|
pi.sendUserMessage(initialPrompt);
|
|
21
24
|
}
|
|
22
25
|
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function queueFollowup(): void {
|
|
29
|
+
queueMicrotask(() => {
|
|
30
|
+
if (!stopped && !compacting) {
|
|
31
|
+
pi.sendUserMessage(followupPrompt, { deliverAs: "followUp" });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function compactThenFollowup(ctx: ExtensionContext): void {
|
|
37
|
+
if (compacting) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
compacting = true;
|
|
41
|
+
ctx.compact({
|
|
42
|
+
customInstructions: demoCompactionInstructions,
|
|
43
|
+
onComplete: () => {
|
|
44
|
+
compacting = false;
|
|
45
|
+
queueFollowup();
|
|
46
|
+
},
|
|
47
|
+
onError: (error) => {
|
|
48
|
+
compacting = false;
|
|
49
|
+
stopped = true;
|
|
50
|
+
ctx.ui.notify("Demo compaction failed: " + error.message, "error");
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pi.on("session_start", (event, ctx) => {
|
|
56
|
+
if (started || stopped || event.reason !== "startup" || ctx.mode !== "tui") {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
started = true;
|
|
60
|
+
queueInitialPrompt();
|
|
23
61
|
});
|
|
24
62
|
|
|
25
63
|
pi.on("turn_end", (event, ctx) => {
|
|
26
|
-
if (!started || stopped || ctx.mode !== "tui") {
|
|
64
|
+
if (!started || stopped || compacting || ctx.mode !== "tui") {
|
|
27
65
|
return;
|
|
28
66
|
}
|
|
29
67
|
if (event.message.role !== "assistant") {
|
|
@@ -37,16 +75,36 @@ export default function localpiDemoMode(pi: ExtensionAPI): void {
|
|
|
37
75
|
case "toolUse":
|
|
38
76
|
return;
|
|
39
77
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
78
|
+
if (shouldCompactBeforeFollowup(event, ctx)) {
|
|
79
|
+
compactThenFollowup(ctx);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
queueFollowup();
|
|
45
83
|
});
|
|
46
84
|
|
|
47
85
|
pi.on("session_shutdown", () => {
|
|
48
86
|
stopped = true;
|
|
49
87
|
});
|
|
50
88
|
}
|
|
89
|
+
|
|
90
|
+
function shouldCompactBeforeFollowup(event: TurnEndEvent, ctx: ExtensionContext): boolean {
|
|
91
|
+
const contextPercent = currentContextPercent(event, ctx);
|
|
92
|
+
return contextPercent !== undefined && contextPercent >= compactAtContextPercent;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function currentContextPercent(event: TurnEndEvent, ctx: ExtensionContext): number | undefined {
|
|
96
|
+
const usage = ctx.getContextUsage();
|
|
97
|
+
if (usage?.percent !== undefined && usage.percent !== null) {
|
|
98
|
+
return usage.percent;
|
|
99
|
+
}
|
|
100
|
+
if (event.message.role !== "assistant") {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
const contextWindow = ctx.model?.contextWindow;
|
|
104
|
+
if (contextWindow === undefined || contextWindow <= 0) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
return (event.message.usage.totalTokens / contextWindow) * 100;
|
|
108
|
+
}
|
|
51
109
|
`;
|
|
52
110
|
}
|
|
@@ -11,6 +11,7 @@ type Usage = {
|
|
|
11
11
|
|
|
12
12
|
type TurnState = {
|
|
13
13
|
startedAt: number;
|
|
14
|
+
firstOutputAt?: number;
|
|
14
15
|
outputText: string;
|
|
15
16
|
estimatedOutputTokens: number;
|
|
16
17
|
lastStatusAt: number;
|
|
@@ -33,6 +34,7 @@ export default function localpiTokenStatus(pi: ExtensionAPI): void {
|
|
|
33
34
|
if (!ctx.hasUI || state === undefined) {
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
37
|
+
const now = Date.now();
|
|
36
38
|
const update = textUpdateFromUnknown(event.assistantMessageEvent ?? event.message ?? event);
|
|
37
39
|
if (update.kind === "delta") {
|
|
38
40
|
state.outputText += update.text;
|
|
@@ -40,11 +42,14 @@ export default function localpiTokenStatus(pi: ExtensionAPI): void {
|
|
|
40
42
|
state.outputText = update.text;
|
|
41
43
|
}
|
|
42
44
|
state.estimatedOutputTokens = Math.ceil(state.outputText.length / 4);
|
|
43
|
-
if (
|
|
45
|
+
if (state.firstOutputAt === undefined && state.outputText.length > 0) {
|
|
46
|
+
state.firstOutputAt = now;
|
|
47
|
+
}
|
|
48
|
+
if (now - state.lastStatusAt < 250) {
|
|
44
49
|
return;
|
|
45
50
|
}
|
|
46
|
-
state.lastStatusAt =
|
|
47
|
-
ctx.ui.setStatus("localpi-perf", ctx.ui.theme.fg("dim", statusText(state)));
|
|
51
|
+
state.lastStatusAt = now;
|
|
52
|
+
ctx.ui.setStatus("localpi-perf", ctx.ui.theme.fg("dim", statusText(state, now)));
|
|
48
53
|
});
|
|
49
54
|
|
|
50
55
|
pi.on("turn_end", (event, ctx) => {
|
|
@@ -65,7 +70,10 @@ export default function localpiTokenStatus(pi: ExtensionAPI): void {
|
|
|
65
70
|
const input = usage?.input ?? 0;
|
|
66
71
|
const cacheRead = usage?.cacheRead ?? 0;
|
|
67
72
|
const cacheWrite = usage?.cacheWrite ?? 0;
|
|
68
|
-
const
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
const elapsedSeconds = elapsed(state, now);
|
|
75
|
+
const decodeSeconds = generationElapsed(state, now);
|
|
76
|
+
const prefillText = prefillStatusText(state, input, cacheWrite);
|
|
69
77
|
const context = ctx.getContextUsage();
|
|
70
78
|
const contextText =
|
|
71
79
|
context && context.percent !== null
|
|
@@ -77,7 +85,8 @@ export default function localpiTokenStatus(pi: ExtensionAPI): void {
|
|
|
77
85
|
ctx.ui.theme.fg(
|
|
78
86
|
"dim",
|
|
79
87
|
[
|
|
80
|
-
|
|
88
|
+
\`gen \${(output / decodeSeconds).toFixed(1)} tok/s\`,
|
|
89
|
+
prefillText,
|
|
81
90
|
\`out \${output}\`,
|
|
82
91
|
\`in \${input}\`,
|
|
83
92
|
cacheRead > 0 ? \`cache \${cacheRead}\` : undefined,
|
|
@@ -98,13 +107,48 @@ export default function localpiTokenStatus(pi: ExtensionAPI): void {
|
|
|
98
107
|
});
|
|
99
108
|
}
|
|
100
109
|
|
|
101
|
-
function statusText(state: TurnState): string {
|
|
102
|
-
const elapsedSeconds = elapsed(state);
|
|
103
|
-
|
|
110
|
+
function statusText(state: TurnState, now: number): string {
|
|
111
|
+
const elapsedSeconds = elapsed(state, now);
|
|
112
|
+
if (state.firstOutputAt === undefined) {
|
|
113
|
+
return \`prefill \${elapsedSeconds.toFixed(1)}s | out ~\${state.estimatedOutputTokens}\`;
|
|
114
|
+
}
|
|
115
|
+
const decodeSeconds = generationElapsed(state, now);
|
|
116
|
+
const prefillSeconds = secondsBetween(state.startedAt, state.firstOutputAt);
|
|
117
|
+
return [
|
|
118
|
+
\`gen \${(state.estimatedOutputTokens / decodeSeconds).toFixed(1)} tok/s\`,
|
|
119
|
+
\`out ~\${state.estimatedOutputTokens}\`,
|
|
120
|
+
\`prefill \${prefillSeconds.toFixed(1)}s\`,
|
|
121
|
+
\`total \${elapsedSeconds.toFixed(1)}s\`
|
|
122
|
+
].join(" | ");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function prefillStatusText(
|
|
126
|
+
state: TurnState,
|
|
127
|
+
input: number,
|
|
128
|
+
cacheWrite: number
|
|
129
|
+
): string | undefined {
|
|
130
|
+
const tokens = prefillTokenCount(input, cacheWrite);
|
|
131
|
+
if (state.firstOutputAt === undefined || tokens <= 0) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
const seconds = secondsBetween(state.startedAt, state.firstOutputAt);
|
|
135
|
+
return \`prefill \${(tokens / seconds).toFixed(1)} tok/s\`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function prefillTokenCount(input: number, cacheWrite: number): number {
|
|
139
|
+
return Math.max(input + cacheWrite, 0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function generationElapsed(state: TurnState, now: number): number {
|
|
143
|
+
return secondsBetween(state.firstOutputAt ?? state.startedAt, now);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function elapsed(state: TurnState, now: number): number {
|
|
147
|
+
return secondsBetween(state.startedAt, now);
|
|
104
148
|
}
|
|
105
149
|
|
|
106
|
-
function
|
|
107
|
-
return Math.max((
|
|
150
|
+
function secondsBetween(start: number, end: number): number {
|
|
151
|
+
return Math.max((end - start) / 1000, 0.001);
|
|
108
152
|
}
|
|
109
153
|
|
|
110
154
|
type TextUpdate = {
|
|
@@ -150,7 +150,7 @@ session dir: ~/.local/state/localpi/sessions
|
|
|
150
150
|
Localpi installs two default extensions:
|
|
151
151
|
|
|
152
152
|
- tool approval gate: ask before each tool call, and tell the model clearly when a tool call was blocked
|
|
153
|
-
- token status: show live
|
|
153
|
+
- token status: show live generation speed while streaming, then final prefill and generation rates when usage data is available
|
|
154
154
|
|
|
155
155
|
## System Prompt
|
|
156
156
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "localpi",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Swiss army knife for running Pi with local inference engines.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|