@meetopenbot/pi 0.0.3 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -9
- package/dist/config.js +7 -30
- package/dist/credits.js +51 -0
- package/dist/index.js +13 -173
- package/dist/runtime.js +17 -0
- package/dist/session.js +34 -11
- package/dist/stream.js +29 -19
- package/package.json +3 -3
- package/dist/diff.js +0 -1
- package/dist/format.js +0 -148
package/README.md
CHANGED
|
@@ -22,13 +22,21 @@ plugins:
|
|
|
22
22
|
tools: read,bash,edit,write,grep,find,ls
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
## Auth
|
|
26
|
+
|
|
27
|
+
On cloud, `authMode: credits` (the default) uses your workspace credit balance via OpenBot for `openai`, `anthropic`, and `deepseek`. For BYOK, set `authMode: byok` and add the matching provider key under workspace settings (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `DEEPSEEK_API_KEY`, or `GEMINI_API_KEY`).
|
|
28
|
+
|
|
29
|
+
Locally, Pi always uses BYOK from environment variables or `~/.pi/agent/auth.json`.
|
|
30
|
+
|
|
31
|
+
Credits does not cover Google Gemini — Pi uses Gemini's native API, while OpenBot Credits proxies the OpenAI-compatible Gemini surface. Use openai/anthropic/deepseek on credits, or switch to BYOK for Gemini.
|
|
32
|
+
|
|
25
33
|
## Configuration
|
|
26
34
|
|
|
27
35
|
| Option | Description |
|
|
28
36
|
|--------|-------------|
|
|
29
|
-
| `
|
|
37
|
+
| `authMode` | Cloud only: `credits` (default) or `byok`. |
|
|
30
38
|
| `agentDir` | Pi config directory (credentials, settings, sessions). Default: `~/.pi/agent`. |
|
|
31
|
-
| `provider` | Model provider (e.g. `anthropic`, `openai`). |
|
|
39
|
+
| `provider` | Model provider (e.g. `anthropic`, `openai`, `deepseek`). |
|
|
32
40
|
| `model` | Model id (e.g. `claude-opus-4-5`). |
|
|
33
41
|
| `thinkingLevel` | Extended thinking: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`. |
|
|
34
42
|
| `tools` | Comma-separated built-in tools to enable. |
|
|
@@ -36,13 +44,7 @@ plugins:
|
|
|
36
44
|
| `noTools` | `all` or `builtin` to disable tools. |
|
|
37
45
|
| `systemPrompt` | Override Pi's system prompt for this agent. |
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
Pi resolves credentials via `AuthStorage` (see [Pi SDK docs](https://pi.dev/docs/latest/sdk)):
|
|
42
|
-
|
|
43
|
-
1. Runtime overrides
|
|
44
|
-
2. `~/.pi/agent/auth.json`
|
|
45
|
-
3. Environment variables (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.)
|
|
47
|
+
Working directory always comes from the OpenBot channel `cwd`.
|
|
46
48
|
|
|
47
49
|
## Sessions
|
|
48
50
|
|
package/dist/config.js
CHANGED
|
@@ -1,34 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
'off',
|
|
3
|
-
'minimal',
|
|
4
|
-
'low',
|
|
5
|
-
'medium',
|
|
6
|
-
'high',
|
|
7
|
-
'xhigh',
|
|
8
|
-
]);
|
|
1
|
+
import { trimmedString, vendorModelId } from '@meetopenbot/plugin-sdk';
|
|
9
2
|
export const resolveConfig = (context, channelCwd) => {
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
: undefined;
|
|
3
|
+
const raw = trimmedString(context.config.model);
|
|
4
|
+
const provider = raw?.includes('/') ? raw.split('/')[0] : undefined;
|
|
5
|
+
const model = vendorModelId(raw);
|
|
14
6
|
return {
|
|
15
|
-
cwd: channelCwd ||
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
model: config.model?.trim() || undefined,
|
|
19
|
-
thinkingLevel,
|
|
20
|
-
tools: config.tools?.trim() || undefined,
|
|
21
|
-
excludeTools: config.excludeTools?.trim() || undefined,
|
|
22
|
-
noTools: config.noTools,
|
|
23
|
-
systemPrompt: config.systemPrompt?.trim() || undefined,
|
|
7
|
+
cwd: channelCwd || process.cwd(),
|
|
8
|
+
provider,
|
|
9
|
+
model,
|
|
24
10
|
};
|
|
25
11
|
};
|
|
26
|
-
export const parseToolList = (value) => {
|
|
27
|
-
if (!value)
|
|
28
|
-
return undefined;
|
|
29
|
-
const tools = value
|
|
30
|
-
.split(',')
|
|
31
|
-
.map((tool) => tool.trim())
|
|
32
|
-
.filter(Boolean);
|
|
33
|
-
return tools.length > 0 ? tools : undefined;
|
|
34
|
-
};
|
package/dist/credits.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { CREDITS_API_KEY_PLACEHOLDER, CREDITS_NOT_CONFIGURED_MESSAGE, INTEGRATIONS_TOKEN_HEADER, creditsErrorMessage as mapCreditsError, creditsAuthFailedMessage, creditsProviderBaseUrl as sdkCreditsProviderBaseUrl, isAuthErrorMessage, llmAuthNotConfiguredMessage, resolveByokApiKey, resolveCreditsAuthConfig, } from '@meetopenbot/plugin-sdk';
|
|
2
|
+
export { CREDITS_API_KEY_PLACEHOLDER, CREDITS_NOT_CONFIGURED_MESSAGE, INTEGRATIONS_TOKEN_HEADER, isAuthErrorMessage, llmAuthNotConfiguredMessage, resolveByokApiKey, resolveCreditsAuthConfig, };
|
|
3
|
+
/**
|
|
4
|
+
* Gateway paths matching each Pi provider's default base URL shape:
|
|
5
|
+
* - openai models use `https://api.openai.com/v1`
|
|
6
|
+
* - anthropic models use `https://api.anthropic.com` (SDK appends `/v1`)
|
|
7
|
+
* - deepseek is OpenAI-compat; `/v1` keeps metering on `/v1/chat/completions`
|
|
8
|
+
*
|
|
9
|
+
* Google Gemini is omitted: Pi speaks the native Generative Language API, while
|
|
10
|
+
* the credits gateway only proxies Gemini's OpenAI-compatible surface.
|
|
11
|
+
*/
|
|
12
|
+
export const CREDITS_PROVIDERS = [
|
|
13
|
+
{ id: 'openai', basePath: 'openai/v1', envVar: 'OPENAI_API_KEY' },
|
|
14
|
+
{ id: 'anthropic', basePath: 'anthropic', envVar: 'ANTHROPIC_API_KEY' },
|
|
15
|
+
{ id: 'deepseek', basePath: 'deepseek/v1', envVar: 'DEEPSEEK_API_KEY' },
|
|
16
|
+
];
|
|
17
|
+
const CREDITS_PROVIDER_IDS = new Set(CREDITS_PROVIDERS.map((provider) => provider.id));
|
|
18
|
+
const PI_CREDITS_PATHS = Object.fromEntries(CREDITS_PROVIDERS.map((provider) => [provider.id, provider.basePath]));
|
|
19
|
+
export function isCreditsProvider(provider) {
|
|
20
|
+
return CREDITS_PROVIDER_IDS.has(provider);
|
|
21
|
+
}
|
|
22
|
+
export function creditsProviderBaseUrl(config, provider) {
|
|
23
|
+
return sdkCreditsProviderBaseUrl(config, provider, PI_CREDITS_PATHS);
|
|
24
|
+
}
|
|
25
|
+
export function applyCreditsProviders(modelRegistry, authStorage, config) {
|
|
26
|
+
const apiKey = config.token || CREDITS_API_KEY_PLACEHOLDER;
|
|
27
|
+
const headers = { [INTEGRATIONS_TOKEN_HEADER]: config.token };
|
|
28
|
+
for (const provider of CREDITS_PROVIDERS) {
|
|
29
|
+
modelRegistry.registerProvider(provider.id, {
|
|
30
|
+
baseUrl: creditsProviderBaseUrl(config, provider.id),
|
|
31
|
+
headers,
|
|
32
|
+
});
|
|
33
|
+
authStorage.setRuntimeApiKey(provider.id, apiKey);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export const CREDITS_AUTH_FAILED_MESSAGE = creditsAuthFailedMessage('Pi');
|
|
37
|
+
export function creditsErrorMessage(message) {
|
|
38
|
+
return mapCreditsError(message, { agentName: 'Pi' });
|
|
39
|
+
}
|
|
40
|
+
export function remapPiError(message) {
|
|
41
|
+
if (resolveByokApiKey('openai') ||
|
|
42
|
+
resolveByokApiKey('anthropic') ||
|
|
43
|
+
resolveByokApiKey('google') ||
|
|
44
|
+
resolveByokApiKey('deepseek')) {
|
|
45
|
+
return message;
|
|
46
|
+
}
|
|
47
|
+
return creditsErrorMessage(message) ?? message;
|
|
48
|
+
}
|
|
49
|
+
export function unsupportedCreditsProviderMessage(provider) {
|
|
50
|
+
return `OpenBot Credits does not support the "${provider}" provider. Use openai, anthropic, or deepseek.`;
|
|
51
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,178 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import { streamPiPrompt } from './stream.js';
|
|
6
|
-
export default definePlugin({
|
|
7
|
-
id: 'pi',
|
|
1
|
+
import { defineAgentPlugin } from '@meetopenbot/plugin-sdk';
|
|
2
|
+
import { remapPiError } from './credits.js';
|
|
3
|
+
import { runPiTurn } from './runtime.js';
|
|
4
|
+
export const plugin = await defineAgentPlugin({
|
|
8
5
|
name: 'Pi',
|
|
9
6
|
description: 'Pi coding agent — read, edit, and run code in your workspace.',
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
cwd: {
|
|
14
|
-
type: 'string',
|
|
15
|
-
description: 'Working directory for Pi tools and resource discovery.',
|
|
16
|
-
},
|
|
17
|
-
agentDir: {
|
|
18
|
-
type: 'string',
|
|
19
|
-
description: 'Pi config directory (default: ~/.pi/agent).',
|
|
20
|
-
},
|
|
21
|
-
provider: {
|
|
22
|
-
type: 'string',
|
|
23
|
-
description: 'Model provider (e.g. anthropic, openai).',
|
|
24
|
-
},
|
|
25
|
-
model: {
|
|
26
|
-
type: 'string',
|
|
27
|
-
description: 'Model id (e.g. claude-opus-4-5).',
|
|
28
|
-
},
|
|
29
|
-
thinkingLevel: {
|
|
30
|
-
type: 'string',
|
|
31
|
-
description: 'Extended thinking level.',
|
|
32
|
-
enum: ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'],
|
|
33
|
-
default: 'off',
|
|
34
|
-
},
|
|
35
|
-
tools: {
|
|
36
|
-
type: 'string',
|
|
37
|
-
description: 'Comma-separated built-in tools to enable (e.g. read,bash,edit,write,grep,find,ls).',
|
|
38
|
-
},
|
|
39
|
-
excludeTools: {
|
|
40
|
-
type: 'string',
|
|
41
|
-
description: 'Comma-separated tool names to disable.',
|
|
42
|
-
},
|
|
43
|
-
noTools: {
|
|
44
|
-
type: 'string',
|
|
45
|
-
description: 'Disable tools: "all" or "builtin".',
|
|
46
|
-
enum: ['all', 'builtin'],
|
|
47
|
-
},
|
|
48
|
-
systemPrompt: {
|
|
49
|
-
type: 'string',
|
|
50
|
-
description: 'Override Pi system prompt for this agent.',
|
|
51
|
-
},
|
|
52
|
-
},
|
|
7
|
+
models: {
|
|
8
|
+
providers: ['openai', 'anthropic', 'deepseek'],
|
|
9
|
+
default: 'openai/gpt-5.6-luna',
|
|
53
10
|
},
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const prompt = (event.data.content ?? '').trim();
|
|
60
|
-
if (!prompt) {
|
|
61
|
-
yield agentOutput({
|
|
62
|
-
agentId: context.agentId,
|
|
63
|
-
content: 'Send a message to run Pi in this workspace — for example, "List the files here" or "Fix the failing test in src/foo.test.ts".',
|
|
64
|
-
threadId,
|
|
65
|
-
});
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
const channelCwd = handlerCtx.state.channelDetails?.cwd;
|
|
69
|
-
const config = resolveConfig(context, channelCwd);
|
|
70
|
-
try {
|
|
71
|
-
const { session } = await getOrCreatePiSession({
|
|
72
|
-
config,
|
|
73
|
-
state: handlerCtx.state,
|
|
74
|
-
storage: context.storage,
|
|
75
|
-
});
|
|
76
|
-
let fullTextContent = '';
|
|
77
|
-
const toolInfoMap = new Map();
|
|
78
|
-
const changedFiles = new Map();
|
|
79
|
-
const snapshot = snapshotWorkspace(config.cwd);
|
|
80
|
-
for await (const chunk of streamPiPrompt(session, prompt, {
|
|
81
|
-
streaming: session.isStreaming,
|
|
82
|
-
})) {
|
|
83
|
-
switch (chunk.type) {
|
|
84
|
-
case 'tool_start':
|
|
85
|
-
toolInfoMap.set(chunk.toolCallId, {
|
|
86
|
-
statusLine: chunk.statusLine,
|
|
87
|
-
args: chunk.args,
|
|
88
|
-
});
|
|
89
|
-
yield uiWidget({
|
|
90
|
-
agentId: context.agentId,
|
|
91
|
-
threadId,
|
|
92
|
-
widget: toolTraceWidget({
|
|
93
|
-
widgetId: chunk.toolCallId,
|
|
94
|
-
groupId: 'pi:tools',
|
|
95
|
-
title: chunk.statusLine,
|
|
96
|
-
body: `**Input**\n\`\`\`json\n${JSON.stringify(chunk.args, null, 2)}\n\`\`\``,
|
|
97
|
-
}),
|
|
98
|
-
});
|
|
99
|
-
break;
|
|
100
|
-
case 'tool_end': {
|
|
101
|
-
const info = toolInfoMap.get(chunk.toolCallId);
|
|
102
|
-
const inputMd = info
|
|
103
|
-
? `**Input**\n\`\`\`json\n${JSON.stringify(info.args, null, 2)}\n\`\`\`\n\n`
|
|
104
|
-
: '';
|
|
105
|
-
const outputMd = `**Output**\n${formatToolResult(chunk.result)}`;
|
|
106
|
-
yield uiWidget({
|
|
107
|
-
agentId: context.agentId,
|
|
108
|
-
threadId,
|
|
109
|
-
widget: toolTraceWidget({
|
|
110
|
-
widgetId: chunk.toolCallId,
|
|
111
|
-
groupId: 'pi:tools',
|
|
112
|
-
title: chunk.statusLine || info?.statusLine || `Tool ${chunk.toolName} finished`,
|
|
113
|
-
body: inputMd + outputMd,
|
|
114
|
-
state: chunk.isError ? 'error' : 'submitted',
|
|
115
|
-
}),
|
|
116
|
-
});
|
|
117
|
-
if (!chunk.isError) {
|
|
118
|
-
const file = diffFileFromMutationTool({
|
|
119
|
-
toolName: chunk.toolName,
|
|
120
|
-
input: info?.args,
|
|
121
|
-
result: chunk.result,
|
|
122
|
-
});
|
|
123
|
-
if (file)
|
|
124
|
-
changedFiles.set(file.path, file);
|
|
125
|
-
}
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
case 'status':
|
|
129
|
-
yield uiWidget({
|
|
130
|
-
agentId: context.agentId,
|
|
131
|
-
threadId,
|
|
132
|
-
widget: toolTraceWidget({
|
|
133
|
-
widgetId: `status-${Date.now()}`,
|
|
134
|
-
groupId: 'pi:tools',
|
|
135
|
-
title: 'Status',
|
|
136
|
-
body: chunk.statusLine,
|
|
137
|
-
}),
|
|
138
|
-
});
|
|
139
|
-
break;
|
|
140
|
-
case 'text':
|
|
141
|
-
if (fullTextContent)
|
|
142
|
-
fullTextContent += '\n\n';
|
|
143
|
-
fullTextContent += chunk.content;
|
|
144
|
-
break;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
if (fullTextContent.trim()) {
|
|
148
|
-
yield agentOutput({
|
|
149
|
-
agentId: context.agentId,
|
|
150
|
-
content: fullTextContent.trim(),
|
|
151
|
-
threadId,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
const diff = buildDiffWidget({
|
|
155
|
-
widgetId: `pi-diff:${threadId ?? 'run'}:${Date.now()}`,
|
|
156
|
-
files: resolveRunDiffFiles({
|
|
157
|
-
snapshot,
|
|
158
|
-
fallback: changedFiles.values(),
|
|
159
|
-
}),
|
|
160
|
-
});
|
|
161
|
-
if (diff) {
|
|
162
|
-
yield uiWidget({
|
|
163
|
-
agentId: context.agentId,
|
|
164
|
-
threadId,
|
|
165
|
-
widget: diff,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
catch (error) {
|
|
170
|
-
yield agentOutput({
|
|
171
|
-
agentId: context.agentId,
|
|
172
|
-
content: `**Pi error:** ${formatPiError(error)}`,
|
|
173
|
-
threadId,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
});
|
|
11
|
+
emptyPrompt: 'Send a message to run Pi in this workspace — for example, "List the files here" or "Fix the failing test in src/foo.test.ts".',
|
|
12
|
+
run: runPiTurn,
|
|
13
|
+
mapError: (error) => {
|
|
14
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
15
|
+
return { kind: 'reply', content: `Pi error: ${remapPiError(message)}` };
|
|
177
16
|
},
|
|
178
17
|
});
|
|
18
|
+
export default plugin;
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { resolveConfig } from './config.js';
|
|
2
|
+
import { remapPiError } from './credits.js';
|
|
3
|
+
import { getOrCreatePiSession } from './session.js';
|
|
4
|
+
import { runPiPrompt } from './stream.js';
|
|
5
|
+
export async function runPiTurn({ prompt, handlerCtx, context }) {
|
|
6
|
+
const config = resolveConfig(context, handlerCtx.state.channelDetails?.cwd);
|
|
7
|
+
const { session } = await getOrCreatePiSession({
|
|
8
|
+
config,
|
|
9
|
+
state: handlerCtx.state,
|
|
10
|
+
storage: context.storage,
|
|
11
|
+
});
|
|
12
|
+
const text = await runPiPrompt(session, prompt, { streaming: session.isStreaming });
|
|
13
|
+
if (text.startsWith('Pi error: ')) {
|
|
14
|
+
return `Pi error: ${remapPiError(text.slice('Pi error: '.length))}`;
|
|
15
|
+
}
|
|
16
|
+
return text;
|
|
17
|
+
}
|
package/dist/session.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { AuthStorage, createAgentSession, DefaultResourceLoader, getAgentDir, ModelRegistry, SessionManager, SettingsManager, } from '@earendil-works/pi-coding-agent';
|
|
2
|
-
import {
|
|
2
|
+
import { isCloudMode } from '@meetopenbot/plugin-sdk';
|
|
3
|
+
import { applyCreditsProviders, isCreditsProvider, llmAuthNotConfiguredMessage, resolveByokApiKey, resolveCreditsAuthConfig, unsupportedCreditsProviderMessage, } from './credits.js';
|
|
3
4
|
import { persistPiState, readPersistedState } from './state.js';
|
|
4
5
|
const sessionCache = new Map();
|
|
5
|
-
const
|
|
6
|
+
const hasPiByok = (provider) => {
|
|
7
|
+
if (provider)
|
|
8
|
+
return Boolean(resolveByokApiKey(provider));
|
|
9
|
+
return Boolean(resolveByokApiKey('openai') ||
|
|
10
|
+
resolveByokApiKey('anthropic') ||
|
|
11
|
+
resolveByokApiKey('google') ||
|
|
12
|
+
resolveByokApiKey('deepseek'));
|
|
13
|
+
};
|
|
14
|
+
const buildSessionKey = (state, byok) => {
|
|
15
|
+
const scope = state.threadId ? `${state.channelId}:${state.threadId}` : state.channelId;
|
|
16
|
+
return `${scope}:${byok ? 'byok' : 'credits'}`;
|
|
17
|
+
};
|
|
6
18
|
const resolveModel = async (config, modelRegistry) => {
|
|
7
19
|
if (config.provider && config.model) {
|
|
8
20
|
return modelRegistry.find(config.provider, config.model) ?? undefined;
|
|
@@ -12,24 +24,32 @@ const resolveModel = async (config, modelRegistry) => {
|
|
|
12
24
|
};
|
|
13
25
|
export const getOrCreatePiSession = async (args) => {
|
|
14
26
|
const { config, state, storage } = args;
|
|
15
|
-
const
|
|
27
|
+
const byok = hasPiByok(config.provider);
|
|
28
|
+
const sessionKey = buildSessionKey(state, byok);
|
|
16
29
|
const cached = sessionCache.get(sessionKey);
|
|
17
30
|
if (cached)
|
|
18
31
|
return cached;
|
|
32
|
+
if (!byok && config.provider && !isCreditsProvider(config.provider)) {
|
|
33
|
+
throw new Error(unsupportedCreditsProviderMessage(config.provider));
|
|
34
|
+
}
|
|
19
35
|
const cwd = config.cwd || process.cwd();
|
|
20
|
-
const agentDir =
|
|
36
|
+
const agentDir = getAgentDir();
|
|
21
37
|
const persisted = readPersistedState(state);
|
|
22
38
|
const authStorage = AuthStorage.create(`${agentDir}/auth.json`);
|
|
23
39
|
const modelRegistry = ModelRegistry.create(authStorage, `${agentDir}/models.json`);
|
|
40
|
+
const credits = !byok && isCloudMode() ? resolveCreditsAuthConfig() : undefined;
|
|
41
|
+
if (!byok && !credits) {
|
|
42
|
+
throw new Error(llmAuthNotConfiguredMessage(config.provider === 'anthropic' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY'));
|
|
43
|
+
}
|
|
44
|
+
if (credits) {
|
|
45
|
+
applyCreditsProviders(modelRegistry, authStorage, credits);
|
|
46
|
+
}
|
|
24
47
|
const model = await resolveModel(config, modelRegistry);
|
|
25
48
|
const settingsManager = SettingsManager.create(cwd, agentDir);
|
|
26
49
|
const loader = new DefaultResourceLoader({
|
|
27
50
|
cwd,
|
|
28
51
|
agentDir,
|
|
29
52
|
settingsManager,
|
|
30
|
-
...(config.systemPrompt
|
|
31
|
-
? { systemPromptOverride: () => config.systemPrompt }
|
|
32
|
-
: {}),
|
|
33
53
|
});
|
|
34
54
|
await loader.reload();
|
|
35
55
|
const sessionManager = persisted.sessionFile
|
|
@@ -41,10 +61,6 @@ export const getOrCreatePiSession = async (args) => {
|
|
|
41
61
|
authStorage,
|
|
42
62
|
modelRegistry,
|
|
43
63
|
model,
|
|
44
|
-
thinkingLevel: config.thinkingLevel,
|
|
45
|
-
tools: parseToolList(config.tools),
|
|
46
|
-
excludeTools: parseToolList(config.excludeTools),
|
|
47
|
-
noTools: config.noTools,
|
|
48
64
|
resourceLoader: loader,
|
|
49
65
|
sessionManager,
|
|
50
66
|
settingsManager,
|
|
@@ -66,3 +82,10 @@ export const disposePiSession = (sessionKey) => {
|
|
|
66
82
|
cached.session.dispose();
|
|
67
83
|
sessionCache.delete(sessionKey);
|
|
68
84
|
};
|
|
85
|
+
export const disposePiSessionsForThread = (state) => {
|
|
86
|
+
const prefix = state.threadId ? `${state.channelId}:${state.threadId}:` : `${state.channelId}:`;
|
|
87
|
+
for (const key of sessionCache.keys()) {
|
|
88
|
+
if (key.startsWith(prefix))
|
|
89
|
+
disposePiSession(key);
|
|
90
|
+
}
|
|
91
|
+
};
|
package/dist/stream.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
export async function
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
const isAssistantMessage = (message) => message.role === 'assistant';
|
|
2
|
+
/** Run a Pi prompt and return the final assistant text. */
|
|
3
|
+
export async function runPiPrompt(session, prompt, options) {
|
|
4
|
+
let assistantText = '';
|
|
5
|
+
let result = '';
|
|
6
6
|
let wake;
|
|
7
7
|
let finished = false;
|
|
8
8
|
let error;
|
|
@@ -10,17 +10,26 @@ export async function* streamPiPrompt(session, prompt, options) {
|
|
|
10
10
|
wake?.();
|
|
11
11
|
wake = undefined;
|
|
12
12
|
};
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
const onEvent = (event) => {
|
|
14
|
+
if (event.type === 'message_update') {
|
|
15
|
+
const assistantEvent = event.assistantMessageEvent;
|
|
16
|
+
if (assistantEvent.type === 'text_delta')
|
|
17
|
+
assistantText += assistantEvent.delta;
|
|
18
|
+
return;
|
|
18
19
|
}
|
|
19
20
|
if (event.type === 'agent_end') {
|
|
21
|
+
const lastAssistant = [...event.messages].reverse().find(isAssistantMessage);
|
|
22
|
+
if (lastAssistant?.stopReason === 'error' && lastAssistant.errorMessage?.trim()) {
|
|
23
|
+
result = `Pi error: ${lastAssistant.errorMessage.trim()}`;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
result = assistantText;
|
|
27
|
+
}
|
|
20
28
|
finished = true;
|
|
21
29
|
wakeUp();
|
|
22
30
|
}
|
|
23
|
-
}
|
|
31
|
+
};
|
|
32
|
+
const unsubscribe = session.subscribe(onEvent);
|
|
24
33
|
const run = (async () => {
|
|
25
34
|
try {
|
|
26
35
|
if (options?.streaming && session.isStreaming) {
|
|
@@ -39,18 +48,19 @@ export async function* streamPiPrompt(session, prompt, options) {
|
|
|
39
48
|
}
|
|
40
49
|
})();
|
|
41
50
|
try {
|
|
42
|
-
while (!finished
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
while (!finished) {
|
|
52
|
+
await new Promise((resolve) => {
|
|
53
|
+
if (finished) {
|
|
54
|
+
resolve();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
wake = resolve;
|
|
58
|
+
});
|
|
50
59
|
}
|
|
51
60
|
await run;
|
|
52
61
|
if (error)
|
|
53
62
|
throw error;
|
|
63
|
+
return result.trim();
|
|
54
64
|
}
|
|
55
65
|
finally {
|
|
56
66
|
unsubscribe();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meetopenbot/pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "OpenBot agent plugin powered by the Pi coding agent SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@earendil-works/pi-ai": "^0.79.1",
|
|
23
23
|
"@earendil-works/pi-coding-agent": "^0.79.1",
|
|
24
|
-
"@meetopenbot/plugin-sdk": "^0.
|
|
24
|
+
"@meetopenbot/plugin-sdk": "^0.3.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^25.9.2",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsc -p tsconfig.json && node ../../scripts/write-plugin-declaration.mjs",
|
|
33
33
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
34
|
-
"test": "node --experimental-strip-types --test src
|
|
34
|
+
"test": "node --experimental-strip-types --test src/*.test.ts",
|
|
35
35
|
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput"
|
|
36
36
|
}
|
|
37
37
|
}
|
package/dist/diff.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { buildDiffWidget as runDiffWidget, diffFileFromMutationTool as diffFileFromPiTool, } from '@meetopenbot/plugin-sdk';
|
package/dist/format.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
const isAssistantMessage = (message) => message.role === 'assistant';
|
|
2
|
-
export const createStreamState = () => ({
|
|
3
|
-
assistantText: '',
|
|
4
|
-
});
|
|
5
|
-
const strArg = (args, ...keys) => {
|
|
6
|
-
if (!args || typeof args !== 'object')
|
|
7
|
-
return undefined;
|
|
8
|
-
const record = args;
|
|
9
|
-
for (const key of keys) {
|
|
10
|
-
const value = record[key];
|
|
11
|
-
if (typeof value === 'string' && value.trim())
|
|
12
|
-
return value.trim();
|
|
13
|
-
if (typeof value === 'number' && Number.isFinite(value))
|
|
14
|
-
return String(value);
|
|
15
|
-
}
|
|
16
|
-
return undefined;
|
|
17
|
-
};
|
|
18
|
-
const truncate = (text, max = 80) => text.length > max ? `${text.slice(0, max - 3)}…` : text;
|
|
19
|
-
const quoteDetail = (detail) => detail.includes('`') ? `"${detail}"` : `\`${detail}\``;
|
|
20
|
-
const formatToolDetail = (toolName, args) => {
|
|
21
|
-
switch (toolName) {
|
|
22
|
-
case 'bash': {
|
|
23
|
-
const command = strArg(args, 'command');
|
|
24
|
-
return command ? truncate(command.replace(/\s+/g, ' ')) : undefined;
|
|
25
|
-
}
|
|
26
|
-
case 'read':
|
|
27
|
-
case 'edit':
|
|
28
|
-
case 'write': {
|
|
29
|
-
const path = strArg(args, 'path', 'file_path');
|
|
30
|
-
if (!path)
|
|
31
|
-
return undefined;
|
|
32
|
-
let detail = truncate(path, 120);
|
|
33
|
-
if (toolName === 'read') {
|
|
34
|
-
const offset = strArg(args, 'offset');
|
|
35
|
-
const limit = strArg(args, 'limit');
|
|
36
|
-
const parts = [offset && `offset=${offset}`, limit && `limit=${limit}`].filter(Boolean);
|
|
37
|
-
if (parts.length)
|
|
38
|
-
detail += ` (${parts.join(', ')})`;
|
|
39
|
-
}
|
|
40
|
-
return detail;
|
|
41
|
-
}
|
|
42
|
-
case 'grep': {
|
|
43
|
-
const pattern = strArg(args, 'pattern');
|
|
44
|
-
const path = strArg(args, 'path') ?? '.';
|
|
45
|
-
if (!pattern)
|
|
46
|
-
return undefined;
|
|
47
|
-
return `${truncate(pattern)} in ${truncate(path, 60)}`;
|
|
48
|
-
}
|
|
49
|
-
case 'find': {
|
|
50
|
-
const pattern = strArg(args, 'pattern');
|
|
51
|
-
const path = strArg(args, 'path') ?? '.';
|
|
52
|
-
if (!pattern)
|
|
53
|
-
return undefined;
|
|
54
|
-
return `${truncate(pattern)} in ${truncate(path, 60)}`;
|
|
55
|
-
}
|
|
56
|
-
case 'ls': {
|
|
57
|
-
const path = strArg(args, 'path') ?? '.';
|
|
58
|
-
return truncate(path, 120);
|
|
59
|
-
}
|
|
60
|
-
default:
|
|
61
|
-
return undefined;
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
const formatToolStart = (toolName, args) => {
|
|
65
|
-
const detail = formatToolDetail(toolName, args);
|
|
66
|
-
if (detail)
|
|
67
|
-
return `Running **${toolName}** (${quoteDetail(detail)})…`;
|
|
68
|
-
return `Running **${toolName}**…`;
|
|
69
|
-
};
|
|
70
|
-
/** Map Pi session events to user-visible output chunks. */
|
|
71
|
-
export const formatPiEvent = (event, state) => {
|
|
72
|
-
switch (event.type) {
|
|
73
|
-
case 'message_update': {
|
|
74
|
-
const assistantEvent = event.assistantMessageEvent;
|
|
75
|
-
if (assistantEvent.type !== 'text_delta')
|
|
76
|
-
return undefined;
|
|
77
|
-
state.assistantText += assistantEvent.delta;
|
|
78
|
-
return undefined;
|
|
79
|
-
}
|
|
80
|
-
case 'tool_execution_start': {
|
|
81
|
-
const statusLine = formatToolStart(event.toolName, event.args);
|
|
82
|
-
return {
|
|
83
|
-
type: 'tool_start',
|
|
84
|
-
toolCallId: event.toolCallId,
|
|
85
|
-
toolName: event.toolName,
|
|
86
|
-
args: event.args,
|
|
87
|
-
statusLine,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
case 'tool_execution_end': {
|
|
91
|
-
let statusLine;
|
|
92
|
-
if (event.isError) {
|
|
93
|
-
statusLine = `Tool **${event.toolName}** failed.`;
|
|
94
|
-
}
|
|
95
|
-
return {
|
|
96
|
-
type: 'tool_end',
|
|
97
|
-
toolCallId: event.toolCallId,
|
|
98
|
-
toolName: event.toolName,
|
|
99
|
-
result: event.result,
|
|
100
|
-
isError: event.isError,
|
|
101
|
-
statusLine,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
case 'auto_retry_start': {
|
|
105
|
-
const statusLine = `Retrying (${event.attempt}/${event.maxAttempts})…`;
|
|
106
|
-
return { type: 'status', statusLine };
|
|
107
|
-
}
|
|
108
|
-
case 'compaction_start': {
|
|
109
|
-
const statusLine = 'Compacting conversation context…';
|
|
110
|
-
return { type: 'status', statusLine };
|
|
111
|
-
}
|
|
112
|
-
case 'agent_end': {
|
|
113
|
-
const lastAssistant = [...event.messages].reverse().find(isAssistantMessage);
|
|
114
|
-
if (lastAssistant?.stopReason === 'error' && lastAssistant.errorMessage?.trim()) {
|
|
115
|
-
return { type: 'text', content: `**Pi error:** ${lastAssistant.errorMessage.trim()}` };
|
|
116
|
-
}
|
|
117
|
-
if (state.assistantText.trim()) {
|
|
118
|
-
return { type: 'text', content: state.assistantText };
|
|
119
|
-
}
|
|
120
|
-
return undefined;
|
|
121
|
-
}
|
|
122
|
-
default:
|
|
123
|
-
return undefined;
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
export const formatPiError = (error) => {
|
|
127
|
-
if (error instanceof Error)
|
|
128
|
-
return error.message;
|
|
129
|
-
return String(error);
|
|
130
|
-
};
|
|
131
|
-
export const formatToolResult = (result) => {
|
|
132
|
-
if (!result)
|
|
133
|
-
return '';
|
|
134
|
-
if (typeof result === 'string')
|
|
135
|
-
return result;
|
|
136
|
-
if (result && typeof result === 'object' && Array.isArray(result.content)) {
|
|
137
|
-
return result.content
|
|
138
|
-
.map((c) => {
|
|
139
|
-
if (c.type === 'text')
|
|
140
|
-
return c.text;
|
|
141
|
-
if (c.type === 'image')
|
|
142
|
-
return `[Image: ${c.source?.data?.slice(0, 20)}...]`;
|
|
143
|
-
return `[${c.type}]`;
|
|
144
|
-
})
|
|
145
|
-
.join('\n');
|
|
146
|
-
}
|
|
147
|
-
return JSON.stringify(result, null, 2);
|
|
148
|
-
};
|