pi2web 0.2.0 → 0.2.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 +169 -17
- package/dist/agents.js +126 -25
- package/dist/auth.js +83 -20
- package/dist/collab/dispatcher.js +186 -0
- package/dist/collab/hub.js +1880 -0
- package/dist/collab/pi-extension.js +237 -0
- package/dist/collab/review-flow.js +310 -0
- package/dist/collab/routes.js +214 -0
- package/dist/collab/schemas.js +225 -0
- package/dist/collab/scoring-flow.js +230 -0
- package/dist/collab/store.js +563 -0
- package/dist/collab/types.js +53 -0
- package/dist/collab/validate.js +148 -0
- package/dist/drafts.js +30 -0
- package/dist/mail-notifier.js +36 -8
- package/dist/main.js +83 -4
- package/dist/mcp.js +113 -0
- package/dist/metadata-store.js +12 -3
- package/dist/sdk-backend.js +82 -19
- package/dist/server.js +434 -58
- package/dist/session-namer.js +114 -0
- package/dist/voice.js +47 -9
- package/package.json +8 -6
- package/packages/edge-tts-server/src/remote_pi_edge_tts/__pycache__/__init__.cpython-312.pyc +0 -0
- package/packages/edge-tts-server/src/remote_pi_edge_tts/__pycache__/__main__.cpython-312.pyc +0 -0
- package/packages/protocol/openapi.yaml +2 -0
- package/packages/protocol/websocket.schema.json +47 -6
- package/web/app.js +1187 -176
- package/web/backend-state.js +237 -0
- package/web/collab.html +95 -0
- package/web/collab.js +667 -0
- package/web/hid-keyboard.js +420 -0
- package/web/index.html +11 -9
- package/web/styles.css +59 -0
- package/ANDROID_APP_FEATURES.md +0 -256
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { assistantText, authHeaders, clipSummaryField, deadline, endpoint, prepareSummaryInput } from './voice.js';
|
|
3
|
+
export const DEFAULT_SESSION_NAMER = {
|
|
4
|
+
baseUrl: 'http://localhost:8313/', model: 'gpt-5-mini', language: 'zh-CN',
|
|
5
|
+
// Reasoning models (gpt-5-mini and friends) burn this budget on hidden reasoning tokens before they
|
|
6
|
+
// emit the name, so a tight cap yields an empty message with finish_reason "length".
|
|
7
|
+
maxInputChars: 8000, maxOutputTokens: 2000, requestTimeoutMs: 60000
|
|
8
|
+
};
|
|
9
|
+
export function parseSessionName(text) {
|
|
10
|
+
const trimmed = text.trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '');
|
|
11
|
+
let candidate = trimmed;
|
|
12
|
+
const start = trimmed.indexOf('{'), end = trimmed.lastIndexOf('}');
|
|
13
|
+
if (start >= 0 && end > start) {
|
|
14
|
+
try {
|
|
15
|
+
const value = JSON.parse(trimmed.slice(start, end + 1));
|
|
16
|
+
if (typeof value?.sessionName === 'string')
|
|
17
|
+
candidate = value.sessionName;
|
|
18
|
+
}
|
|
19
|
+
catch { /* fall back to the raw text below */ }
|
|
20
|
+
}
|
|
21
|
+
const name = candidate.replace(/[\r\n\t]+/g, ' ').replace(/\s{2,}/g, ' ').replace(/^["'“”‘’]|["'“”‘’]$/g, '').trim().slice(0, 60).trim();
|
|
22
|
+
return name || undefined;
|
|
23
|
+
}
|
|
24
|
+
export class SessionNamer {
|
|
25
|
+
fetcher;
|
|
26
|
+
emitter = new EventEmitter();
|
|
27
|
+
lastAssistant = new Map();
|
|
28
|
+
lastPrompt = new Map();
|
|
29
|
+
pending = new Set();
|
|
30
|
+
closed = false;
|
|
31
|
+
config;
|
|
32
|
+
constructor(config = {}, fetcher = fetch) {
|
|
33
|
+
this.fetcher = fetcher;
|
|
34
|
+
this.config = { apiKey: '', ...DEFAULT_SESSION_NAMER, ...strip(config) };
|
|
35
|
+
}
|
|
36
|
+
subscribe(listener) { this.emitter.on('event', listener); return () => this.emitter.off('event', listener); }
|
|
37
|
+
close() { this.closed = true; this.emitter.removeAllListeners(); this.lastAssistant.clear(); this.lastPrompt.clear(); }
|
|
38
|
+
recordUserPrompt(agentId, prompt) { const value = prompt.trim(); if (value)
|
|
39
|
+
this.lastPrompt.set(agentId, value); }
|
|
40
|
+
emit(agentId, event) { this.emitter.emit('event', agentId, event); }
|
|
41
|
+
handleAgentEvent(agentId, event, context = {}) {
|
|
42
|
+
if (this.closed)
|
|
43
|
+
return;
|
|
44
|
+
if (event.type === 'before_agent_start' && typeof event.prompt === 'string')
|
|
45
|
+
return void this.recordUserPrompt(agentId, event.prompt);
|
|
46
|
+
if (event.type === 'agent_start') {
|
|
47
|
+
if (typeof event.message === 'string')
|
|
48
|
+
this.recordUserPrompt(agentId, event.message);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (event.type === 'message_end') {
|
|
52
|
+
const text = assistantText(event.message);
|
|
53
|
+
if (text)
|
|
54
|
+
this.lastAssistant.set(agentId, text);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (event.type !== 'agent_settled')
|
|
58
|
+
return;
|
|
59
|
+
const finalOutput = this.lastAssistant.get(agentId) ?? '', userPrompt = this.lastPrompt.get(agentId) ?? '';
|
|
60
|
+
this.lastAssistant.delete(agentId);
|
|
61
|
+
this.lastPrompt.delete(agentId);
|
|
62
|
+
if (context.sessionName?.trim() || !context.setSessionName)
|
|
63
|
+
return;
|
|
64
|
+
if (!userPrompt && !finalOutput)
|
|
65
|
+
return;
|
|
66
|
+
void this.nameSession(agentId, userPrompt, finalOutput, context.setSessionName);
|
|
67
|
+
}
|
|
68
|
+
/** Generates and applies a name. Exposed so callers can trigger naming outside the event stream. */
|
|
69
|
+
async nameSession(agentId, userPrompt, finalOutput, apply) {
|
|
70
|
+
if (this.pending.has(agentId))
|
|
71
|
+
return;
|
|
72
|
+
this.pending.add(agentId);
|
|
73
|
+
try {
|
|
74
|
+
const name = await this.requestName(userPrompt, finalOutput);
|
|
75
|
+
if (!name)
|
|
76
|
+
throw new Error('Naming endpoint returned no session name');
|
|
77
|
+
await apply(name);
|
|
78
|
+
this.emit(agentId, { type: 'session_name_generated', name });
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
this.emit(agentId, { type: 'session_name_error', message: error.message });
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
this.pending.delete(agentId);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async requestName(userPrompt, finalOutput) {
|
|
88
|
+
const promptBudget = Math.max(1, Math.floor(this.config.maxInputChars * 0.6));
|
|
89
|
+
const body = JSON.stringify({
|
|
90
|
+
userPrompt: clipSummaryField(userPrompt, promptBudget),
|
|
91
|
+
piFinalOutput: prepareSummaryInput(finalOutput, Math.max(1, this.config.maxInputChars - promptBudget))
|
|
92
|
+
});
|
|
93
|
+
const response = await this.fetcher(endpoint(this.config.baseUrl, '/chat/completions'), {
|
|
94
|
+
method: 'POST', signal: deadline(this.config.requestTimeoutMs),
|
|
95
|
+
headers: { 'content-type': 'application/json', ...authHeaders(this.config.apiKey || undefined) },
|
|
96
|
+
body: JSON.stringify({ model: this.config.model, stream: false, temperature: 0.2, max_tokens: this.config.maxOutputTokens,
|
|
97
|
+
messages: [{ role: 'system', content: this.prompt() }, { role: 'user', content: body }] })
|
|
98
|
+
});
|
|
99
|
+
if (!response.ok)
|
|
100
|
+
throw new Error(`Session naming request failed (${response.status}): ${(await response.text()).slice(0, 300)}`);
|
|
101
|
+
const value = await response.json();
|
|
102
|
+
const choice = value?.choices?.[0];
|
|
103
|
+
const content = choice?.message?.content;
|
|
104
|
+
const text = typeof content === 'string' ? content : Array.isArray(content) ? content.filter((part) => part?.type === 'text' && typeof part.text === 'string').map((part) => part.text).join('') : '';
|
|
105
|
+
if (!text.trim())
|
|
106
|
+
throw new Error(choice?.finish_reason === 'length' ? `Naming endpoint returned no content: the model hit the ${this.config.maxOutputTokens}-token output budget (raise --session-name-max-output-tokens)` : 'Naming endpoint returned no content');
|
|
107
|
+
return parseSessionName(text);
|
|
108
|
+
}
|
|
109
|
+
prompt() {
|
|
110
|
+
const language = this.config.language.toLowerCase().startsWith('zh') ? '简体中文' : '与用户输入相同的主要语言';
|
|
111
|
+
return `你负责为一次 Pi 编程会话生成标题。\n\n输入是一个 JSON 对象:\n- userPrompt:用户本轮真正想完成的任务。\n- piFinalOutput:Pi 最后一次回复。\n\n主要依据 userPrompt 概括任务主题,必要时参考 piFinalOutput。标题使用${language}:中文 6 到 18 个字,英文 3 到 8 个词;具体、便于检索,不写“新会话”“任务总结”等空泛名称,不带句号、引号、Emoji、代码或路径。\n\n只输出一个合法 JSON 对象:{"sessionName":"简短标题"},不要输出 Markdown 代码块、解释或任何额外文字。`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const strip = (value) => Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined && v !== ''));
|
package/dist/voice.js
CHANGED
|
@@ -2,12 +2,12 @@ import { EventEmitter } from 'node:events';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
const punctuation = new Set(['。', '!', '?', '!', '?', '.']);
|
|
4
4
|
const closing = new Set(['”', '’', '"', '\'', ')', ')', '】', ']']);
|
|
5
|
-
const endpoint = (base, path) => `${base.replace(/\/$/, '')}${base.replace(/\/$/, '').endsWith('/v1') ? '' : '/v1'}${path}`;
|
|
6
|
-
const authHeaders = (key) => key ? { authorization: `Bearer ${key}` } : {};
|
|
5
|
+
export const endpoint = (base, path) => `${base.replace(/\/$/, '')}${base.replace(/\/$/, '').endsWith('/v1') ? '' : '/v1'}${path}`;
|
|
6
|
+
export const authHeaders = (key) => key ? { authorization: `Bearer ${key}` } : {};
|
|
7
7
|
const abortError = (error) => error instanceof Error && error.name === 'AbortError';
|
|
8
8
|
const timeoutError = (error) => error instanceof Error && error.name === 'TimeoutError';
|
|
9
9
|
/** Combines caller cancellation with a hard timeout so a stalled speech server cannot block the announcement queue forever. */
|
|
10
|
-
const deadline = (ms, signal) => signal ? AbortSignal.any([signal, AbortSignal.timeout(ms)]) : AbortSignal.timeout(ms);
|
|
10
|
+
export const deadline = (ms, signal) => signal ? AbortSignal.any([signal, AbortSignal.timeout(ms)]) : AbortSignal.timeout(ms);
|
|
11
11
|
export function assistantText(message) {
|
|
12
12
|
if (typeof message === 'string')
|
|
13
13
|
return message.trim() || undefined;
|
|
@@ -42,7 +42,7 @@ export function prepareSummaryInput(text, maxChars = 32000) {
|
|
|
42
42
|
cleaned = `${cleaned.slice(0, head)}\n\n[中间内容已省略]\n\n${cleaned.slice(-tail)}`;
|
|
43
43
|
return cleaned;
|
|
44
44
|
}
|
|
45
|
-
function clipSummaryField(text, maxChars) {
|
|
45
|
+
export function clipSummaryField(text, maxChars) {
|
|
46
46
|
const normalized = text.replace(/[ \t]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
|
|
47
47
|
if (normalized.length <= maxChars)
|
|
48
48
|
return normalized;
|
|
@@ -169,10 +169,12 @@ export class VoiceManager {
|
|
|
169
169
|
generations = new Map();
|
|
170
170
|
announcementQueue = Promise.resolve();
|
|
171
171
|
closed = false;
|
|
172
|
+
/** Some backends rename or reject optional sampling parameters; the working shape is learned once and reused. */
|
|
173
|
+
summaryParams = { temperature: true, tokenLimitField: 'max_tokens' };
|
|
172
174
|
config;
|
|
173
175
|
constructor(config, fetcher = fetch) {
|
|
174
176
|
this.fetcher = fetcher;
|
|
175
|
-
this.config = { language: 'zh-CN', maxInputChars: 32000,
|
|
177
|
+
this.config = { language: 'zh-CN', maxInputChars: 32000, sampleRate: 24000, ttsFormat: 'pcm', requestTimeoutMs: 120000, ...config };
|
|
176
178
|
}
|
|
177
179
|
subscribe(listener) { this.emitter.on('event', listener); return () => this.emitter.off('event', listener); }
|
|
178
180
|
capabilities() { return { tts: true, stt: !!this.config.sttModel }; }
|
|
@@ -200,7 +202,8 @@ export class VoiceManager {
|
|
|
200
202
|
const finalOutput = this.lastAssistant.get(agentId), userPrompt = this.lastPrompt.get(agentId) ?? '';
|
|
201
203
|
this.lastAssistant.delete(agentId);
|
|
202
204
|
this.lastPrompt.delete(agentId);
|
|
203
|
-
|
|
205
|
+
// A run the user cancelled has no result worth speaking; Pi 0.99+ reports that as `aborted`.
|
|
206
|
+
if (finalOutput && event.aborted !== true)
|
|
204
207
|
void this.announce(agentId, { userPrompt, finalOutput, ...context });
|
|
205
208
|
}
|
|
206
209
|
}
|
|
@@ -244,9 +247,7 @@ export class VoiceManager {
|
|
|
244
247
|
let rawResult = '', spokenSummary = '';
|
|
245
248
|
try {
|
|
246
249
|
const summaryInput = prepareSummaryContext(input, this.config.maxInputChars);
|
|
247
|
-
const response = await this.
|
|
248
|
-
if (!response.ok)
|
|
249
|
-
throw new Error(`Summary request failed (${response.status}): ${(await response.text()).slice(0, 500)}`);
|
|
250
|
+
const response = await this.requestSummary(summaryInput, controller.signal);
|
|
250
251
|
for await (const delta of sseText(response))
|
|
251
252
|
rawResult += delta;
|
|
252
253
|
const result = parseSummaryResult(rawResult), sessionName = input.sessionName?.trim() || result.sessionName;
|
|
@@ -288,6 +289,43 @@ export class VoiceManager {
|
|
|
288
289
|
controller.abort();
|
|
289
290
|
}
|
|
290
291
|
}
|
|
292
|
+
/** Retries once per unsupported parameter so a strict model still produces a summary instead of failing the announcement. */
|
|
293
|
+
async requestSummary(summaryInput, signal) {
|
|
294
|
+
for (let attempt = 0;; attempt++) {
|
|
295
|
+
const body = { model: this.config.summaryModel, stream: true, messages: [{ role: 'system', content: this.summaryPrompt() }, { role: 'user', content: summaryInput }] };
|
|
296
|
+
if (this.summaryParams.temperature && typeof this.config.summaryTemperature === 'number')
|
|
297
|
+
body.temperature = this.config.summaryTemperature;
|
|
298
|
+
if (this.summaryParams.tokenLimitField && this.config.maxOutputTokens)
|
|
299
|
+
body[this.summaryParams.tokenLimitField] = this.config.maxOutputTokens;
|
|
300
|
+
const response = await this.fetcher(endpoint(this.config.summaryBaseUrl, '/chat/completions'), { method: 'POST', signal: deadline(this.config.requestTimeoutMs, signal), headers: { 'content-type': 'application/json', ...authHeaders(this.config.summaryApiKey) }, body: JSON.stringify(body) });
|
|
301
|
+
if (response.ok)
|
|
302
|
+
return response;
|
|
303
|
+
const detail = (await response.text()).slice(0, 500);
|
|
304
|
+
if (response.status === 400 && attempt < 2 && this.relaxSummaryParams(detail))
|
|
305
|
+
continue;
|
|
306
|
+
throw new Error(`Summary request failed (${response.status}): ${detail}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
relaxSummaryParams(detail) {
|
|
310
|
+
const text = detail.toLowerCase();
|
|
311
|
+
if (!/unsupported|not supported|unrecognized|unknown|invalid/.test(text))
|
|
312
|
+
return false;
|
|
313
|
+
let changed = false;
|
|
314
|
+
if (this.summaryParams.temperature && text.includes('temperature')) {
|
|
315
|
+
this.summaryParams.temperature = false;
|
|
316
|
+
changed = true;
|
|
317
|
+
}
|
|
318
|
+
const field = this.summaryParams.tokenLimitField;
|
|
319
|
+
if (field === 'max_tokens' && text.includes('max_tokens')) {
|
|
320
|
+
this.summaryParams.tokenLimitField = text.includes('max_completion_tokens') ? 'max_completion_tokens' : null;
|
|
321
|
+
changed = true;
|
|
322
|
+
}
|
|
323
|
+
else if (field === 'max_completion_tokens' && text.includes('max_completion_tokens')) {
|
|
324
|
+
this.summaryParams.tokenLimitField = null;
|
|
325
|
+
changed = true;
|
|
326
|
+
}
|
|
327
|
+
return changed;
|
|
328
|
+
}
|
|
291
329
|
summaryPrompt() { const language = this.config.language.toLowerCase().startsWith('zh') ? '简体中文' : '与用户输入相同的主要语言'; return `你负责把一次 Pi 编程任务压缩成准确、自然、可直接朗读的完成通知,并在需要时为 Session 命名。\n\n输入是一个 JSON 对象:\n- userPrompt:用户本轮真正想完成的任务。\n- piFinalOutput:Pi 最后一次回复,包含完成情况、改动、验证结果和后续事项。\n- sessionNeedsName:只有为 true 时才需要生成 Session 名称。\n\n先结合 userPrompt 判断目标,再以 piFinalOutput 为事实依据总结。不要把用户的要求误说成已经完成;没有明确证据时不要声称测试通过或任务成功。摘要使用${language},写 2 到 4 个短句,最多 120 个中文字符或 70 个英文单词。优先交代:是否完成、最重要的结果或修改、测试/验证结果、失败原因,以及用户必须采取的下一步。省略代码、命令、URL、完整路径、哈希、冗长文件清单、日志和 Markdown;不要使用标题、列表、“总结如下”等套话。\n\n如果 sessionNeedsName 为 true,请根据 userPrompt 生成一个具体、简短、便于检索的名称:中文建议 6 到 18 个字,英文建议 3 到 8 个词;使用任务主题或目标,不写“新会话”“任务总结”等空泛名称,不带句号、引号、Emoji 或路径。如果 sessionNeedsName 为 false,sessionName 必须为 null。\n\n只输出一个合法 JSON 对象,不要输出 Markdown 代码块、解释或任何额外文字。有名称时使用 {"summary":"适合直接朗读的摘要","sessionName":"简短名称"};无需命名时使用 {"summary":"适合直接朗读的摘要","sessionName":null}。summary 必须是 JSON 字符串,sessionName 必须是 JSON 字符串或 null。`; }
|
|
292
330
|
async speak(agentId, playbackId, text, signal) {
|
|
293
331
|
const response = await this.fetcher(endpoint(this.config.speechBaseUrl, '/audio/speech'), { method: 'POST', signal: deadline(this.config.requestTimeoutMs, signal), headers: { 'content-type': 'application/json', ...authHeaders(this.config.speechApiKey) }, body: JSON.stringify({ model: this.config.ttsModel, voice: this.config.ttsVoice, input: text, response_format: this.config.ttsFormat, sample_rate: this.config.sampleRate, stream_format: 'audio' }) });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi2web",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Secure remote web
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Secure remote web client server for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "sxwxs",
|
|
7
7
|
"license": "MIT",
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
"packages/edge-tts-server/README.md",
|
|
26
26
|
"README.md",
|
|
27
27
|
"VOICE_SERVER_SETUP.md",
|
|
28
|
-
"ANDROID_APP_FEATURES.md",
|
|
29
28
|
"LICENSE"
|
|
30
29
|
],
|
|
31
30
|
"publishConfig": {
|
|
@@ -34,25 +33,28 @@
|
|
|
34
33
|
},
|
|
35
34
|
"scripts": {
|
|
36
35
|
"clean": "rm -rf dist",
|
|
37
|
-
"build": "npm run clean && tsc -p tsconfig.json",
|
|
36
|
+
"build": "npm run clean && tsc -p tsconfig.json && chmod +x dist/main.js",
|
|
38
37
|
"check": "npm run build",
|
|
39
38
|
"test": "vitest run",
|
|
40
39
|
"dev": "tsx src/main.ts",
|
|
40
|
+
"dev:watch": "tsx watch src/main.ts",
|
|
41
41
|
"start": "node dist/main.js",
|
|
42
42
|
"prepack": "npm run build",
|
|
43
43
|
"prepublishOnly": "npm run build && npm test"
|
|
44
44
|
},
|
|
45
45
|
"engines": {
|
|
46
|
-
"node": ">=
|
|
46
|
+
"node": ">=22.19.0"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@earendil-works/pi-
|
|
49
|
+
"@earendil-works/pi-ai": "1.1.0",
|
|
50
|
+
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
50
51
|
"@xterm/addon-fit": "^0.11.0",
|
|
51
52
|
"@xterm/xterm": "^6.0.0",
|
|
52
53
|
"better-sqlite3": "^12.11.1",
|
|
53
54
|
"dompurify": "^3.4.12",
|
|
54
55
|
"marked": "^18.0.6",
|
|
55
56
|
"node-pty": "^1.1.0",
|
|
57
|
+
"typebox": "1.3.27",
|
|
56
58
|
"ws": "^8.18.0"
|
|
57
59
|
},
|
|
58
60
|
"devDependencies": {
|
|
Binary file
|
|
Binary file
|
|
@@ -22,6 +22,8 @@ paths:
|
|
|
22
22
|
/agents/{id}/{action}:
|
|
23
23
|
get: {operationId: getAgentResource, parameters: [{$ref: '#/components/parameters/Id'}, {name: action, in: path, required: true, schema: {enum: [state,messages,capabilities,session]}}], responses: {'200': {$ref: '#/components/responses/Success'}}}
|
|
24
24
|
post: {operationId: agentCommand, parameters: [{$ref: '#/components/parameters/Id'}, {name: action, in: path, required: true, schema: {enum: [prompt,steer,follow-up,abort,compact,model,thinking,session-name,navigate,fork,extension-response]}}], requestBody: {$ref: '#/components/requestBodies/Json'}, responses: {'200': {$ref: '#/components/responses/Success'}}}
|
|
25
|
+
/draft:
|
|
26
|
+
post: {operationId: draftText, description: 'Insert text into the currently displayed Web UI input box on this backend, without sending it to an Agent. Body {text, mode?: append|replace, source?}; explicit agent/slot targets are rejected. Pending text is kept in memory until a page acknowledges it.', requestBody: {$ref: '#/components/requestBodies/Json'}, responses: {'200': {$ref: '#/components/responses/Success'}}}
|
|
25
27
|
components:
|
|
26
28
|
securitySchemes: {bearerAuth: {type: http, scheme: bearer}}
|
|
27
29
|
parameters:
|
|
@@ -4,14 +4,55 @@
|
|
|
4
4
|
"title": "Remote Pi WebSocket v1",
|
|
5
5
|
"oneOf": [
|
|
6
6
|
{"$ref":"#/$defs/subscribe"}, {"$ref":"#/$defs/command"}, {"$ref":"#/$defs/subscribed"},
|
|
7
|
-
{"$ref":"#/$defs/commandResult"}, {"$ref":"#/$defs/agentEvent"}, {"$ref":"#/$defs/agentSnapshot"}
|
|
7
|
+
{"$ref":"#/$defs/commandResult"}, {"$ref":"#/$defs/agentEvent"}, {"$ref":"#/$defs/agentSnapshot"}, {"$ref":"#/$defs/agentStateUpdate"},
|
|
8
|
+
{"$ref":"#/$defs/subscribeDrafts"}, {"$ref":"#/$defs/subscribedDrafts"}, {"$ref":"#/$defs/draftAck"}, {"$ref":"#/$defs/draftEvent"}
|
|
8
9
|
],
|
|
9
10
|
"$defs": {
|
|
10
|
-
"
|
|
11
|
-
"
|
|
11
|
+
"subscribeDrafts": {"type":"object","additionalProperties":false,"required":["type"],"properties":{"type":{"const":"subscribe_drafts"}},"description":"Full pairing code only: receive pending drafts from voice clients."},
|
|
12
|
+
"subscribedDrafts": {"type":"object","required":["type","protocolVersion"],"properties":{"type":{"const":"subscribed_drafts"},"protocolVersion":{"const":1}}},
|
|
13
|
+
"draftAck": {"type":"object","additionalProperties":false,"required":["type","id"],"properties":{"type":{"const":"draft_ack"},"id":{"type":"string"}}},
|
|
14
|
+
"draftEvent": {"type":"object","required":["type","draft"],"properties":{"type":{"const":"draft_event"},"replay":{"type":"boolean"},"draft":{"type":"object","required":["id","text","mode","createdAt"],"properties":{"id":{"type":"string"},"text":{"type":"string"},"mode":{"enum":["append","replace"]},"source":{"type":"string"},"createdAt":{"type":"string"}}}}},
|
|
15
|
+
"subscribe": {"type":"object","additionalProperties":false,"required":["type","agentId"],"properties":{"type":{"const":"subscribe"},"agentId":{"type":"string"},"lastSequence":{"type":"integer","minimum":0},"fromNow":{"type":"boolean"},"messageLimit":{"type":"integer","minimum":1,"maximum":200}}},
|
|
16
|
+
"command": {"type":"object","additionalProperties":false,"required":["type","requestId","agentId"],"properties":{"type":{"enum":["prompt","steer","follow-up","abort","clear-queue"]},"requestId":{"type":"string"},"agentId":{"type":"string"},"message":{"type":"string","maxLength":2097152}}},
|
|
12
17
|
"subscribed": {"type":"object","required":["type","agentId","protocolVersion"],"properties":{"type":{"const":"subscribed"},"agentId":{"type":"string"},"protocolVersion":{"const":1},"currentSequence":{"type":"integer","minimum":0}}},
|
|
13
|
-
"commandResult": {"type":"object","required":["type","success"],"properties":{"type":{"const":"command_result"},"requestId":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object"}}},
|
|
14
|
-
"
|
|
15
|
-
"
|
|
18
|
+
"commandResult": {"type":"object","required":["type","success"],"properties":{"type":{"const":"command_result"},"requestId":{"type":"string"},"success":{"type":"boolean"},"error":{"type":"object"},"disposition":{"enum":["handled","queued"],"description":"For steer and follow-up: whether the running turn took the message or it waits in the queue. Omitted by older servers."},"queue":{"$ref":"#/$defs/queue"}}},
|
|
19
|
+
"queue": {"type":"object","required":["steering","followUp"],"description":"Messages waiting for the running turn.","properties":{"steering":{"type":"array","items":{"type":"string"}},"followUp":{"type":"array","items":{"type":"string"}}}},
|
|
20
|
+
"agentEvent": {"type":"object","required":["type","agentId","eventId","sequence","event"],"properties":{"type":{"const":"agent_event"},"agentId":{"type":"string"},"eventId":{"type":"string"},"sequence":{"type":"integer","minimum":1},"timestamp":{"type":"integer","minimum":0},"status":{"$ref":"#/$defs/agentStatus","description":"Agent status after this event. Omitted by older servers."},"event":{"type":"object","required":["type"]}}},
|
|
21
|
+
"agentStatus": {"enum":["unloaded","starting","idle","streaming","waiting_for_user","error","stopping","stopped"]},
|
|
22
|
+
"agentSnapshot": {"type":"object","required":["type","agentId","lastSequence","state","messages"],"properties":{"type":{"const":"agent_snapshot"},"agentId":{"type":"string"},"lastSequence":{"type":"integer","minimum":0},"state":{"$ref":"#/$defs/agentState"},"messages":{"type":"array"}}},
|
|
23
|
+
"agentStateUpdate": {"type":"object","required":["type","agentId","sequence","state"],"description":"Current state after initial replay/snapshot and before live events, also sent for fromNow or empty replay. Does not replace transcript messages.","properties":{"type":{"const":"agent_state"},"agentId":{"type":"string"},"sequence":{"type":"integer","minimum":0},"state":{"$ref":"#/$defs/agentState"}}},
|
|
24
|
+
"agentState": {
|
|
25
|
+
"type":"object","required":["status","agentId","sessionId","cwd"],
|
|
26
|
+
"properties":{
|
|
27
|
+
"status":{"$ref":"#/$defs/agentStatus"},
|
|
28
|
+
"agentId":{"type":"string"},
|
|
29
|
+
"sessionId":{"type":"string"},
|
|
30
|
+
"sessionFile":{"type":"string"},
|
|
31
|
+
"sessionName":{"type":"string"},
|
|
32
|
+
"cwd":{"type":"string"},
|
|
33
|
+
"activity":{
|
|
34
|
+
"type":"object","required":["bashIds","dialogIds"],
|
|
35
|
+
"description":"Runtime operations independent of LLM status. Omitted by older servers; never persisted.",
|
|
36
|
+
"properties":{
|
|
37
|
+
"bashIds":{"type":"array","uniqueItems":true,"items":{"type":"string"}},
|
|
38
|
+
"dialogIds":{"type":"array","uniqueItems":true,"items":{"type":"string"}},
|
|
39
|
+
"dialogRequests":{
|
|
40
|
+
"type":"array","description":"Pending request payloads for rebuilding dialog controls, without submitted answers. Omitted by older servers.",
|
|
41
|
+
"items":{
|
|
42
|
+
"type":"object","required":["type","requestId","kind"],
|
|
43
|
+
"properties":{
|
|
44
|
+
"type":{"const":"extension_ui_request"},"requestId":{"type":"string"},
|
|
45
|
+
"kind":{"enum":["select","confirm","input","editor"]},
|
|
46
|
+
"title":{"type":"string"},"message":{"type":"string"},
|
|
47
|
+
"options":{"type":"array","items":{"type":"string"}},
|
|
48
|
+
"placeholder":{"type":"string"},"prefill":{"type":"string"},"opts":{"type":"object"}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"queue":{"$ref":"#/$defs/queue"}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
16
57
|
}
|
|
17
58
|
}
|