@pasko70/pibo 2.5.2 → 3.0.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/context/pibo-native-tooling.md +1 -0
- package/dist/agent-runtime/contract.js +2 -1
- package/dist/agent-runtime/history.js +53 -1
- package/dist/agent-runtime/routed-session.js +100 -26
- package/dist/agent-runtimes/codex-native/adapter.js +304 -30
- package/dist/agent-runtimes/codex-native/client.js +22 -3
- package/dist/agent-runtimes/codex-native/first-use.js +294 -0
- package/dist/agent-runtimes/codex-native/history.js +26 -7
- package/dist/agent-runtimes/codex-native/process.js +14 -0
- package/dist/agent-runtimes/codex-native/thread.js +59 -25
- package/dist/agent-runtimes/codex-native/turn.js +27 -0
- package/dist/agent-runtimes/history-proof.js +8 -0
- package/dist/agent-runtimes/omp/adapter.js +61 -14
- package/dist/agent-runtimes/omp/history.js +189 -34
- package/dist/agent-runtimes/omp/thread.js +82 -36
- package/dist/agent-runtimes/pi/adapter.js +30 -3
- package/dist/agent-runtimes/pi/history.js +66 -5
- package/dist/agent-runtimes/pi/openai-response-observer.js +102 -0
- package/dist/agent-runtimes/pi/routed-session.js +344 -37
- package/dist/apps/chat/chat-api-routes.js +1 -1
- package/dist/apps/chat/chat-files.js +130 -9
- package/dist/apps/chat/chat-settings-routes.js +27 -2
- package/dist/apps/chat/chat-speech.js +100 -0
- package/dist/apps/chat/data/chat-data-mappers.js +25 -5
- package/dist/apps/chat/data/timeline-query-service.js +89 -1
- package/dist/apps/chat/trace-v2.js +210 -8
- package/dist/apps/chat/trace.js +3 -0
- package/dist/apps/chat/web-app.js +108 -3
- package/dist/apps/chat-ui/assets/{dist-DTWJf37A.js → dist-BBDMeU5-.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CkYWKnvy.js → dist-BDIATCQt.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DZ8Wpj-H.js → dist-JnW4v8UE.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Dmblz15N.js → dist-lRiLP9rr.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DjcwoyNY.js → dist-mqCviI-c.js} +1 -1
- package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +228 -0
- package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +1 -0
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +43 -0
- package/dist/apps/chat-vscode-web/assets/{index-CF1-9PKU.css → index-SluZr_-r.css} +1 -1
- package/dist/apps/chat-vscode-web/index.html +2 -2
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.0.vsix +0 -0
- package/dist/cli.js +49 -5
- package/dist/config/config.js +59 -8
- package/dist/core/preview-server-settings.js +20 -0
- package/dist/core/session-router.js +457 -37
- package/dist/core/user-settings.js +10 -0
- package/dist/data/payload-store.js +57 -1
- package/dist/data/schema.js +18 -1
- package/dist/debug/trace.js +5 -0
- package/dist/gateway/client.js +368 -65
- package/dist/gateway/protocol.js +2 -0
- package/dist/gateway/server.js +8 -0
- package/dist/gateway/web.js +2 -0
- package/dist/index.js +1 -1
- package/dist/loops/store.js +6 -4
- package/dist/mcp/config.js +89 -17
- package/dist/mcp/daemon-client.js +473 -153
- package/dist/mcp/daemon.js +401 -37
- package/dist/plugins/builtin.js +2 -2
- package/dist/plugins/codex-native.js +2 -0
- package/dist/plugins/registry.js +259 -1
- package/dist/previews/cli.js +345 -0
- package/dist/previews/config.js +61 -0
- package/dist/previews/manager.js +644 -0
- package/dist/previews/network.js +198 -0
- package/dist/previews/plugin.js +11 -0
- package/dist/previews/process-supervisor.js +38 -0
- package/dist/previews/proxy.js +387 -0
- package/dist/previews/store.js +750 -0
- package/dist/previews/types.js +1 -0
- package/dist/previews/web-app.js +446 -0
- package/dist/runs/registry.js +22 -0
- package/dist/runs/tools.js +46 -7
- package/dist/session-ui/terminalRows.js +187 -13
- package/dist/sessions/pibo-data-store.js +37 -0
- package/dist/sessions/runtime-binding-persistence.js +34 -0
- package/dist/sessions/runtime-binding.js +6 -0
- package/dist/sessions/sqlite-store.js +37 -0
- package/dist/shared/trace-engine.js +12 -64
- package/dist/shared/trace-event-projection.js +16 -0
- package/dist/shared/trace-history.js +684 -184
- package/dist/shared/trace-limits.js +7 -0
- package/dist/shared/trace-live-patch.js +61 -0
- package/dist/shared/trace-tool-identity.js +43 -0
- package/dist/speech/openai-codex-realtime-call-proxy.js +248 -0
- package/dist/speech/openai-codex.js +407 -0
- package/dist/speech/types.js +8 -0
- package/dist/subagents/context.js +5 -3
- package/dist/subagents/observations.js +94 -6
- package/dist/subagents/tool.js +103 -20
- package/dist/tools/codex-image-artifacts.js +24 -0
- package/dist/tools/codex-image-generation.js +4 -22
- package/dist/web/channel.js +88 -4
- package/dist/web/http.js +2 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +0 -1
- package/dist/apps/chat-ui/assets/index-DsgTm1Aw.js +0 -228
- package/dist/apps/chat-vscode-web/assets/index-DvTSSvzN.js +0 -43
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-2.5.2.vsix +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { piboHomePath } from "./pibo-home.js";
|
|
4
|
+
import { sanitizePreviewServerSettings } from "./preview-server-settings.js";
|
|
4
5
|
import { sanitizeTelemetryRetentionSettings } from "./telemetry-retention-settings.js";
|
|
5
6
|
import { sanitizeTelemetryStaleThresholdSettings } from "./telemetry-staleness.js";
|
|
6
7
|
export const DEFAULT_USER_TIMEZONE = "UTC";
|
|
7
8
|
export const DEFAULT_WEB_ANNOTATIONS_TOGGLE_SHORTCUT = "Alt+Shift+A";
|
|
8
9
|
export const DEFAULT_TRANSCRIPTION_PROVIDER_ID = "openai-chatgpt";
|
|
10
|
+
export const DEFAULT_SPEECH_PROVIDER_ID = "openai-codex";
|
|
9
11
|
const LEGACY_OPENAI_TRANSCRIPTION_PROVIDER_ID = "openai";
|
|
10
12
|
export const DEFAULT_PIBO_USER_SETTINGS_PATH = "user-settings.json";
|
|
11
13
|
export function loadPiboUserSettings() {
|
|
@@ -56,6 +58,12 @@ export function sanitizeTranscriptionSettings(value) {
|
|
|
56
58
|
: providerId ?? DEFAULT_TRANSCRIPTION_PROVIDER_ID,
|
|
57
59
|
};
|
|
58
60
|
}
|
|
61
|
+
export function sanitizeSpeechSettings(value) {
|
|
62
|
+
const raw = value && typeof value === "object" && !Array.isArray(value)
|
|
63
|
+
? value
|
|
64
|
+
: {};
|
|
65
|
+
return { providerId: sanitizeTranscriptionProviderId(raw.providerId) ?? DEFAULT_SPEECH_PROVIDER_ID };
|
|
66
|
+
}
|
|
59
67
|
export function sanitizeTimezone(value) {
|
|
60
68
|
if (typeof value !== "string")
|
|
61
69
|
return undefined;
|
|
@@ -78,6 +86,8 @@ function sanitizeUserSettings(value) {
|
|
|
78
86
|
timezone: sanitizeTimezone(raw.timezone) ?? DEFAULT_USER_TIMEZONE,
|
|
79
87
|
shortcuts: sanitizeShortcutSettings(raw.shortcuts),
|
|
80
88
|
transcription: sanitizeTranscriptionSettings(raw.transcription),
|
|
89
|
+
speech: sanitizeSpeechSettings(raw.speech),
|
|
90
|
+
previewServers: sanitizePreviewServerSettings(raw.previewServers),
|
|
81
91
|
telemetryStaleThresholds: sanitizeTelemetryStaleThresholdSettings(raw.telemetryStaleThresholds),
|
|
82
92
|
telemetryRetention: sanitizeTelemetryRetentionSettings(raw.telemetryRetention),
|
|
83
93
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { gunzipSync, gzipSync } from "node:zlib";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, extname, join, resolve } from "node:path";
|
|
5
5
|
import { piboHomePath } from "../core/pibo-home.js";
|
|
6
6
|
const MAX_SYNC_PAYLOAD_GZIP_BYTES = 512 * 1024;
|
|
@@ -78,11 +78,67 @@ export class PayloadStore {
|
|
|
78
78
|
readPayloadJson(id) {
|
|
79
79
|
return JSON.parse(this.readPayloadText(id));
|
|
80
80
|
}
|
|
81
|
+
readPayloadBytesBounded(id, maxBytes) {
|
|
82
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1)
|
|
83
|
+
throw new RangeError("maxBytes must be a positive safe integer");
|
|
84
|
+
const payload = this.getPayload(id);
|
|
85
|
+
if (!payload)
|
|
86
|
+
throw new Error(`Unknown payload \"${id}\"`);
|
|
87
|
+
if (!payload.storagePath)
|
|
88
|
+
throw new Error(`Payload \"${id}\" has no storage path`);
|
|
89
|
+
if (payload.byteSize > maxBytes)
|
|
90
|
+
throw new RangeError(`Payload \"${id}\" exceeds the ${maxBytes}-byte read limit`);
|
|
91
|
+
const absolutePath = this.rootDir === ":memory:" ? payload.storagePath : join(this.rootDir, payload.storagePath);
|
|
92
|
+
const storedBytes = readFileBounded(absolutePath, maxBytes + 64 * 1024);
|
|
93
|
+
let bytes;
|
|
94
|
+
if (payload.encoding === "gzip")
|
|
95
|
+
bytes = gunzipSync(storedBytes, { maxOutputLength: maxBytes });
|
|
96
|
+
else if (payload.encoding === "identity")
|
|
97
|
+
bytes = storedBytes;
|
|
98
|
+
else
|
|
99
|
+
throw new Error(`Unsupported payload encoding \"${payload.encoding}\"`);
|
|
100
|
+
if (bytes.byteLength !== payload.byteSize)
|
|
101
|
+
throw new Error(`Payload \"${id}\" byte length does not match its metadata`);
|
|
102
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
103
|
+
if (sha256 !== payload.sha256)
|
|
104
|
+
throw new Error(`Payload \"${id}\" checksum does not match its metadata`);
|
|
105
|
+
return bytes;
|
|
106
|
+
}
|
|
107
|
+
readPayloadJsonBounded(id, maxBytes) {
|
|
108
|
+
return JSON.parse(Buffer.from(this.readPayloadBytesBounded(id, maxBytes)).toString("utf8"));
|
|
109
|
+
}
|
|
81
110
|
findBySha256(sha256) {
|
|
82
111
|
const row = this.db.prepare("SELECT * FROM payloads WHERE sha256 = ?").get(sha256);
|
|
83
112
|
return row ? payloadFromRow(row) : undefined;
|
|
84
113
|
}
|
|
85
114
|
}
|
|
115
|
+
function readFileBounded(path, maxBytes) {
|
|
116
|
+
const descriptor = openSync(path, "r");
|
|
117
|
+
try {
|
|
118
|
+
const stats = fstatSync(descriptor);
|
|
119
|
+
if (!stats.isFile())
|
|
120
|
+
throw new Error(`Payload path is not a file: ${path}`);
|
|
121
|
+
if (stats.size > maxBytes)
|
|
122
|
+
throw new RangeError(`Stored payload exceeds the ${maxBytes}-byte read limit`);
|
|
123
|
+
const bytes = Buffer.alloc(stats.size);
|
|
124
|
+
let offset = 0;
|
|
125
|
+
while (offset < bytes.byteLength) {
|
|
126
|
+
const read = readSync(descriptor, bytes, offset, bytes.byteLength - offset, offset);
|
|
127
|
+
if (read === 0)
|
|
128
|
+
break;
|
|
129
|
+
offset += read;
|
|
130
|
+
}
|
|
131
|
+
if (offset !== bytes.byteLength)
|
|
132
|
+
throw new Error(`Stored payload changed while it was being read: ${path}`);
|
|
133
|
+
const extra = Buffer.alloc(1);
|
|
134
|
+
if (readSync(descriptor, extra, 0, 1, offset) !== 0)
|
|
135
|
+
throw new RangeError(`Stored payload exceeds the ${maxBytes}-byte read limit`);
|
|
136
|
+
return bytes;
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
closeSync(descriptor);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
86
142
|
function defaultContentType(value) {
|
|
87
143
|
if (typeof value === "string")
|
|
88
144
|
return "text/plain; charset=utf-8";
|
package/dist/data/schema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const PIBO_DATA_SCHEMA_VERSION =
|
|
1
|
+
export const PIBO_DATA_SCHEMA_VERSION = 6;
|
|
2
2
|
export function applyPiboDataSchema(db) {
|
|
3
3
|
const previousVersion = Number(db.prepare("PRAGMA user_version").get()?.user_version ?? 0);
|
|
4
4
|
const existingSessionCount = db.prepare("SELECT COUNT(*) AS count FROM sqlite_schema WHERE type = 'table' AND name = 'sessions'").get();
|
|
@@ -423,6 +423,23 @@ export function applyPiboDataSchema(db) {
|
|
|
423
423
|
ON event_log(session_id, session_sequence DESC, stream_id DESC);
|
|
424
424
|
CREATE INDEX IF NOT EXISTS idx_event_log_session_type_sequence_stream
|
|
425
425
|
ON event_log(session_id, type, session_sequence ASC, stream_id ASC);
|
|
426
|
+
CREATE INDEX IF NOT EXISTS idx_event_log_session_tool_event_sequence_stream
|
|
427
|
+
ON event_log(session_id, tool_call_id, event_id, session_sequence ASC, stream_id ASC)
|
|
428
|
+
WHERE tool_call_id IS NOT NULL
|
|
429
|
+
AND type IN ('tool_execution_started', 'tool_execution_updated', 'tool_execution_finished');
|
|
430
|
+
CREATE INDEX IF NOT EXISTS idx_event_log_session_tool_sequence_stream
|
|
431
|
+
ON event_log(session_id, tool_call_id, session_sequence ASC, stream_id ASC)
|
|
432
|
+
WHERE tool_call_id IS NOT NULL
|
|
433
|
+
AND type IN ('tool_execution_started', 'tool_execution_updated', 'tool_execution_finished');
|
|
434
|
+
CREATE INDEX IF NOT EXISTS idx_event_log_session_attribute_tool_event_sequence_stream
|
|
435
|
+
ON event_log(
|
|
436
|
+
session_id,
|
|
437
|
+
json_extract(attributes_json, '$.toolCallId'),
|
|
438
|
+
event_id,
|
|
439
|
+
session_sequence ASC,
|
|
440
|
+
stream_id ASC
|
|
441
|
+
)
|
|
442
|
+
WHERE type IN ('tool_execution_started', 'tool_execution_updated', 'tool_execution_finished');
|
|
426
443
|
CREATE INDEX IF NOT EXISTS idx_event_log_room_stream
|
|
427
444
|
ON event_log(room_id, stream_id);
|
|
428
445
|
CREATE INDEX IF NOT EXISTS idx_event_log_topic_stream
|
package/dist/debug/trace.js
CHANGED
|
@@ -46,10 +46,14 @@ export async function inspectDebugTrace(piboSessionId, stores, options = {}) {
|
|
|
46
46
|
const productHistory = readProductHistoryEntries(chatDb, payloadStore, piboSessionId);
|
|
47
47
|
let nativeHistoryEntries = [];
|
|
48
48
|
let nativeHistoryInspection;
|
|
49
|
+
let nativeHistoryReconciliationProof;
|
|
49
50
|
if (options.nativeHistory || (events.length === 0 && productHistory.length === 0)) {
|
|
50
51
|
const native = await readDebugNativeHistory(binding, session.workspace ?? process.cwd(), adapterIssues);
|
|
51
52
|
nativeHistoryEntries = native?.entries ?? [];
|
|
52
53
|
nativeHistoryInspection = native?.inspection;
|
|
54
|
+
nativeHistoryReconciliationProof = native
|
|
55
|
+
? native.reconciliationProof ?? { complete: false, entries: native.entries }
|
|
56
|
+
: undefined;
|
|
53
57
|
}
|
|
54
58
|
const historyEntries = nativeHistoryEntries.length ? nativeHistoryEntries : productHistory;
|
|
55
59
|
const historySource = nativeHistoryEntries.length
|
|
@@ -62,6 +66,7 @@ export async function inspectDebugTrace(piboSessionId, stores, options = {}) {
|
|
|
62
66
|
events,
|
|
63
67
|
historyEntries,
|
|
64
68
|
historyInspection: nativeHistoryInspection,
|
|
69
|
+
historyReconciliationProof: nativeHistoryEntries.length ? nativeHistoryReconciliationProof : undefined,
|
|
65
70
|
status: sessionStatus.status,
|
|
66
71
|
});
|
|
67
72
|
const rows = flattenTraceNodes(view.nodes);
|
package/dist/gateway/client.js
CHANGED
|
@@ -4,6 +4,44 @@ import readline from "node:readline/promises";
|
|
|
4
4
|
import { stdin as input, stdout as output } from "node:process";
|
|
5
5
|
import { DEFAULT_GATEWAY_HOST, DEFAULT_GATEWAY_PORT, encodeFrame, } from "./protocol.js";
|
|
6
6
|
import { parsePiboThinkingLevel } from "../core/thinking.js";
|
|
7
|
+
const DEFAULT_ACKNOWLEDGEMENT_TIMEOUT_MS = 5_000;
|
|
8
|
+
const DEFAULT_TERMINAL_TIMEOUT_MS = 120_000;
|
|
9
|
+
export class GatewayClientExpectedError extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "GatewayClientExpectedError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function isGatewayClientExpectedError(error) {
|
|
16
|
+
return error instanceof GatewayClientExpectedError;
|
|
17
|
+
}
|
|
18
|
+
function errorMessage(error) {
|
|
19
|
+
return error instanceof Error ? error.message : String(error);
|
|
20
|
+
}
|
|
21
|
+
export function parseGatewayClientMessage(value) {
|
|
22
|
+
const text = value.trim();
|
|
23
|
+
if (!text)
|
|
24
|
+
return { ok: false, error: "Message text is required" };
|
|
25
|
+
const command = /^\/(queue|steer)(?:\s+|$)/.exec(text);
|
|
26
|
+
if (!command)
|
|
27
|
+
return { ok: true, text };
|
|
28
|
+
const delivery = command[1];
|
|
29
|
+
const message = text.slice(command[0].length).trim();
|
|
30
|
+
return message
|
|
31
|
+
? { ok: true, text: message, delivery }
|
|
32
|
+
: { ok: false, error: `Usage: /${delivery} <message>` };
|
|
33
|
+
}
|
|
34
|
+
export function createGatewayClientRequestFrame(event) {
|
|
35
|
+
const id = randomUUID();
|
|
36
|
+
return { type: "req", id, event: { ...event, id: event.id ?? id } };
|
|
37
|
+
}
|
|
38
|
+
export function createGatewayClientSubscriptionFrame(piboSessionId) {
|
|
39
|
+
return {
|
|
40
|
+
type: "subscribe",
|
|
41
|
+
id: randomUUID(),
|
|
42
|
+
subscription: { type: "session", piboSessionId },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
7
45
|
function parseJsonLine(line) {
|
|
8
46
|
try {
|
|
9
47
|
return JSON.parse(line);
|
|
@@ -16,9 +54,22 @@ function writeFrame(socket, frame) {
|
|
|
16
54
|
socket.write(encodeFrame(frame));
|
|
17
55
|
}
|
|
18
56
|
function printResponse(frame) {
|
|
19
|
-
if (frame.ok)
|
|
57
|
+
if (!frame.ok) {
|
|
58
|
+
console.error(`\nerror: ${frame.error?.message ?? "request failed"}`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!frame.payload || typeof frame.payload !== "object")
|
|
20
62
|
return;
|
|
21
|
-
|
|
63
|
+
const payload = frame.payload;
|
|
64
|
+
if (payload.type !== "message_steered" || typeof payload.activeEventId !== "string" || payload.activeEventId.length === 0)
|
|
65
|
+
return;
|
|
66
|
+
console.error(`steer: delivered to active turn ${payload.activeEventId}`);
|
|
67
|
+
}
|
|
68
|
+
function printGatewayClientHelp() {
|
|
69
|
+
console.error("messages queue by default; /queue <message> queues explicitly; /steer <message> steers the active turn");
|
|
70
|
+
console.error("commands: /status, /clear, /abort, /thinking [level], /thinking-show, /goal [command], /help, /quit");
|
|
71
|
+
console.error("piped EOF waits for acknowledgements and terminal completion of every accepted message while assistant output keeps streaming");
|
|
72
|
+
console.error("security: raw gateway TCP is unauthenticated and unencrypted; use remote hosts only on trusted networks or through a secure tunnel");
|
|
22
73
|
}
|
|
23
74
|
function isSessionStatus(value) {
|
|
24
75
|
if (!value || typeof value !== "object")
|
|
@@ -109,26 +160,237 @@ function printEvent(frame, piboSessionId, state) {
|
|
|
109
160
|
output.write("assistant> ");
|
|
110
161
|
}
|
|
111
162
|
}
|
|
163
|
+
function isExpectedSubscriptionResponse(frame, piboSessionId) {
|
|
164
|
+
if (!frame.payload || typeof frame.payload !== "object")
|
|
165
|
+
return false;
|
|
166
|
+
const payload = frame.payload;
|
|
167
|
+
return payload.subscription?.type === "session" && payload.subscription.piboSessionId === piboSessionId;
|
|
168
|
+
}
|
|
169
|
+
function steeringTargetEventId(frame) {
|
|
170
|
+
if (!frame.payload || typeof frame.payload !== "object")
|
|
171
|
+
return undefined;
|
|
172
|
+
const payload = frame.payload;
|
|
173
|
+
return payload.type === "message_steered" && typeof payload.activeEventId === "string" && payload.activeEventId.length > 0
|
|
174
|
+
? payload.activeEventId
|
|
175
|
+
: undefined;
|
|
176
|
+
}
|
|
177
|
+
function handleGatewayClientLine(value, sendRequest, piboSessionId, renderState) {
|
|
178
|
+
const text = value.trim();
|
|
179
|
+
if (!text)
|
|
180
|
+
return "continue";
|
|
181
|
+
if (text === "/quit" || text === "/exit")
|
|
182
|
+
return "quit";
|
|
183
|
+
if (text === "/help") {
|
|
184
|
+
printGatewayClientHelp();
|
|
185
|
+
return "continue";
|
|
186
|
+
}
|
|
187
|
+
if (text === "/thinking-show") {
|
|
188
|
+
renderState.showThinking = !renderState.showThinking;
|
|
189
|
+
console.error(`thinking display: ${renderState.showThinking ? "on" : "off"}`);
|
|
190
|
+
return "continue";
|
|
191
|
+
}
|
|
192
|
+
if (text === "/thinking" || text.startsWith("/thinking ")) {
|
|
193
|
+
const level = text.slice("/thinking".length).trim();
|
|
194
|
+
let params;
|
|
195
|
+
try {
|
|
196
|
+
params = level ? { level: parsePiboThinkingLevel(level) } : undefined;
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
console.error(errorMessage(error));
|
|
200
|
+
return "error";
|
|
201
|
+
}
|
|
202
|
+
sendRequest(params === undefined
|
|
203
|
+
? { type: "execution", piboSessionId, action: "thinking" }
|
|
204
|
+
: { type: "execution", piboSessionId, action: "thinking", params });
|
|
205
|
+
return "continue";
|
|
206
|
+
}
|
|
207
|
+
if (text === "/status" || text === "/clear" || text === "/abort") {
|
|
208
|
+
const action = text === "/status" ? "status" : text === "/clear" ? "clear_queue" : "abort";
|
|
209
|
+
sendRequest({ type: "execution", piboSessionId, action });
|
|
210
|
+
return "continue";
|
|
211
|
+
}
|
|
212
|
+
if (text === "/goal" || text.startsWith("/goal ")) {
|
|
213
|
+
sendRequest({
|
|
214
|
+
type: "execution",
|
|
215
|
+
piboSessionId,
|
|
216
|
+
action: "goal",
|
|
217
|
+
params: { command: text.slice("/goal".length).trim() },
|
|
218
|
+
});
|
|
219
|
+
return "continue";
|
|
220
|
+
}
|
|
221
|
+
const message = parseGatewayClientMessage(text);
|
|
222
|
+
if (!message.ok) {
|
|
223
|
+
console.error(message.error);
|
|
224
|
+
return "error";
|
|
225
|
+
}
|
|
226
|
+
sendRequest({
|
|
227
|
+
type: "message",
|
|
228
|
+
piboSessionId,
|
|
229
|
+
text: message.text,
|
|
230
|
+
source: "user",
|
|
231
|
+
...(message.delivery ? { delivery: message.delivery } : {}),
|
|
232
|
+
});
|
|
233
|
+
return "continue";
|
|
234
|
+
}
|
|
112
235
|
export async function runGatewayClient(options = {}) {
|
|
236
|
+
const interactive = input.isTTY === true && output.isTTY === true;
|
|
113
237
|
const host = options.host ?? DEFAULT_GATEWAY_HOST;
|
|
114
238
|
const port = options.port ?? DEFAULT_GATEWAY_PORT;
|
|
115
|
-
const piboSessionId = options.piboSessionId ?? "default";
|
|
239
|
+
const piboSessionId = (options.piboSessionId ?? "default").trim();
|
|
240
|
+
const acknowledgementTimeoutMs = options.acknowledgementTimeoutMs ?? DEFAULT_ACKNOWLEDGEMENT_TIMEOUT_MS;
|
|
241
|
+
const terminalTimeoutMs = options.terminalTimeoutMs ?? DEFAULT_TERMINAL_TIMEOUT_MS;
|
|
242
|
+
if (!piboSessionId)
|
|
243
|
+
throw new GatewayClientExpectedError("Pibo Session ID must not be empty");
|
|
116
244
|
const socket = connect({ host, port });
|
|
117
245
|
socket.setEncoding("utf-8");
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
246
|
+
try {
|
|
247
|
+
await new Promise((resolve, reject) => {
|
|
248
|
+
const onError = (error) => reject(error);
|
|
249
|
+
socket.once("error", onError);
|
|
250
|
+
socket.once("connect", () => {
|
|
251
|
+
socket.off("error", onError);
|
|
252
|
+
resolve();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
socket.destroy();
|
|
258
|
+
throw new GatewayClientExpectedError(`Could not connect to gateway at ${host}:${port}: ${errorMessage(error)}`);
|
|
259
|
+
}
|
|
125
260
|
let buffer = "";
|
|
261
|
+
let closing = false;
|
|
262
|
+
let inputEnded = false;
|
|
263
|
+
let connectionFailure;
|
|
264
|
+
let rl;
|
|
265
|
+
let pendingWaiter;
|
|
266
|
+
let pipedInputFailed = false;
|
|
267
|
+
let completedNormally = false;
|
|
268
|
+
const pendingRequests = new Map();
|
|
269
|
+
const terminalOutcomes = new Map();
|
|
126
270
|
const renderState = {
|
|
127
271
|
visibleIncomingEventIds: new Set(),
|
|
128
272
|
sawAssistantDelta: false,
|
|
129
273
|
showThinking: false,
|
|
130
274
|
sawThinkingDelta: false,
|
|
131
275
|
};
|
|
276
|
+
const pendingCount = (kind) => {
|
|
277
|
+
let count = 0;
|
|
278
|
+
for (const request of pendingRequests.values()) {
|
|
279
|
+
if (kind === "acknowledgement" ? !request.acknowledged : request.acknowledged && request.terminalEventId !== undefined) {
|
|
280
|
+
count += 1;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return count;
|
|
284
|
+
};
|
|
285
|
+
const finishPendingWaiter = (error) => {
|
|
286
|
+
const waiter = pendingWaiter;
|
|
287
|
+
if (!waiter)
|
|
288
|
+
return;
|
|
289
|
+
pendingWaiter = undefined;
|
|
290
|
+
clearTimeout(waiter.timeout);
|
|
291
|
+
if (error)
|
|
292
|
+
waiter.reject(error);
|
|
293
|
+
else
|
|
294
|
+
waiter.resolve();
|
|
295
|
+
};
|
|
296
|
+
const notifyPendingWaiter = () => {
|
|
297
|
+
if (pendingWaiter && pendingCount(pendingWaiter.kind) === 0)
|
|
298
|
+
finishPendingWaiter();
|
|
299
|
+
};
|
|
300
|
+
const waitForPending = async (kind, timeoutMs) => {
|
|
301
|
+
if (connectionFailure)
|
|
302
|
+
throw connectionFailure;
|
|
303
|
+
const count = pendingCount(kind);
|
|
304
|
+
if (count === 0)
|
|
305
|
+
return;
|
|
306
|
+
await new Promise((resolve, reject) => {
|
|
307
|
+
const label = kind === "acknowledgement" ? "gateway acknowledgement" : "terminal message completion";
|
|
308
|
+
pendingWaiter = {
|
|
309
|
+
kind,
|
|
310
|
+
resolve,
|
|
311
|
+
reject,
|
|
312
|
+
timeout: setTimeout(() => {
|
|
313
|
+
finishPendingWaiter(new GatewayClientExpectedError(`Timed out waiting for ${pendingCount(kind)} ${label}(s)`));
|
|
314
|
+
}, timeoutMs),
|
|
315
|
+
};
|
|
316
|
+
});
|
|
317
|
+
};
|
|
318
|
+
const finishRequestTerminal = (request) => {
|
|
319
|
+
if (!request.terminalEventId)
|
|
320
|
+
return;
|
|
321
|
+
const outcome = terminalOutcomes.get(request.terminalEventId);
|
|
322
|
+
if (!outcome)
|
|
323
|
+
return;
|
|
324
|
+
if (!outcome.ok && !interactive)
|
|
325
|
+
pipedInputFailed = true;
|
|
326
|
+
pendingRequests.delete(request.frameId);
|
|
327
|
+
notifyPendingWaiter();
|
|
328
|
+
};
|
|
329
|
+
const recordTerminalOutcome = (event) => {
|
|
330
|
+
if (interactive || (event.type !== "message_finished" && event.type !== "session_error") || !event.eventId)
|
|
331
|
+
return;
|
|
332
|
+
const outcome = event.type === "message_finished"
|
|
333
|
+
? { ok: true }
|
|
334
|
+
: { ok: false, error: event.error };
|
|
335
|
+
terminalOutcomes.set(event.eventId, outcome);
|
|
336
|
+
for (const request of [...pendingRequests.values()]) {
|
|
337
|
+
if (request.terminalEventId === event.eventId)
|
|
338
|
+
finishRequestTerminal(request);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
const sendRequest = (event) => {
|
|
342
|
+
const frame = createGatewayClientRequestFrame(event);
|
|
343
|
+
pendingRequests.set(frame.id, {
|
|
344
|
+
frameId: frame.id,
|
|
345
|
+
event: frame.event,
|
|
346
|
+
acknowledged: false,
|
|
347
|
+
terminalEventId: !interactive && frame.event.type === "message" && frame.event.delivery !== "steer"
|
|
348
|
+
? frame.event.id
|
|
349
|
+
: undefined,
|
|
350
|
+
});
|
|
351
|
+
writeFrame(socket, frame);
|
|
352
|
+
};
|
|
353
|
+
const subscriptionFrame = createGatewayClientSubscriptionFrame(piboSessionId);
|
|
354
|
+
let subscribed = false;
|
|
355
|
+
let settleSubscription;
|
|
356
|
+
let subscriptionTimeout;
|
|
357
|
+
const subscription = new Promise((resolve, reject) => {
|
|
358
|
+
settleSubscription = (error) => error ? reject(error) : resolve();
|
|
359
|
+
});
|
|
360
|
+
const finishSubscription = (error) => {
|
|
361
|
+
const settle = settleSubscription;
|
|
362
|
+
if (!settle)
|
|
363
|
+
return;
|
|
364
|
+
settleSubscription = undefined;
|
|
365
|
+
if (subscriptionTimeout)
|
|
366
|
+
clearTimeout(subscriptionTimeout);
|
|
367
|
+
settle(error);
|
|
368
|
+
};
|
|
369
|
+
subscriptionTimeout = setTimeout(() => {
|
|
370
|
+
finishSubscription(new GatewayClientExpectedError(`Timed out subscribing to Pibo Session "${piboSessionId}"`));
|
|
371
|
+
}, acknowledgementTimeoutMs);
|
|
372
|
+
const failConnection = (error) => {
|
|
373
|
+
if (!subscribed) {
|
|
374
|
+
finishSubscription(error);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (closing || connectionFailure || (!interactive && inputEnded && pendingRequests.size === 0))
|
|
378
|
+
return;
|
|
379
|
+
connectionFailure = error;
|
|
380
|
+
finishPendingWaiter(error);
|
|
381
|
+
rl?.close();
|
|
382
|
+
};
|
|
383
|
+
socket.on("error", (error) => {
|
|
384
|
+
failConnection(new GatewayClientExpectedError(`Gateway connection error: ${errorMessage(error)}`));
|
|
385
|
+
});
|
|
386
|
+
socket.on("close", () => {
|
|
387
|
+
const acknowledgements = pendingCount("acknowledgement");
|
|
388
|
+
const terminals = pendingCount("terminal");
|
|
389
|
+
const suffix = acknowledgements > 0 || terminals > 0
|
|
390
|
+
? ` before ${acknowledgements} acknowledgement(s) and ${terminals} terminal completion(s)`
|
|
391
|
+
: "";
|
|
392
|
+
failConnection(new GatewayClientExpectedError(`Gateway connection closed${suffix}`));
|
|
393
|
+
});
|
|
132
394
|
socket.on("data", (chunk) => {
|
|
133
395
|
buffer += chunk;
|
|
134
396
|
let newlineIndex = buffer.indexOf("\n");
|
|
@@ -136,73 +398,114 @@ export async function runGatewayClient(options = {}) {
|
|
|
136
398
|
const line = buffer.slice(0, newlineIndex).trim();
|
|
137
399
|
buffer = buffer.slice(newlineIndex + 1);
|
|
138
400
|
const frame = line ? parseJsonLine(line) : undefined;
|
|
139
|
-
if (frame?.type === "res") {
|
|
401
|
+
if (frame?.type === "res" && frame.id === subscriptionFrame.id) {
|
|
402
|
+
if (!frame.ok) {
|
|
403
|
+
finishSubscription(new GatewayClientExpectedError(frame.error?.message ?? "Gateway rejected the session subscription"));
|
|
404
|
+
}
|
|
405
|
+
else if (!isExpectedSubscriptionResponse(frame, piboSessionId)) {
|
|
406
|
+
finishSubscription(new GatewayClientExpectedError("Gateway returned a mismatched session subscription"));
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
subscribed = true;
|
|
410
|
+
finishSubscription();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
else if (subscribed && frame?.type === "res") {
|
|
140
414
|
printResponse(frame);
|
|
415
|
+
const request = pendingRequests.get(frame.id);
|
|
416
|
+
if (request) {
|
|
417
|
+
request.acknowledged = true;
|
|
418
|
+
if (!frame.ok) {
|
|
419
|
+
if (!interactive)
|
|
420
|
+
pipedInputFailed = true;
|
|
421
|
+
pendingRequests.delete(frame.id);
|
|
422
|
+
}
|
|
423
|
+
else if (request.event.type === "message") {
|
|
424
|
+
if (request.event.delivery === "steer") {
|
|
425
|
+
const activeEventId = steeringTargetEventId(frame);
|
|
426
|
+
if (!activeEventId) {
|
|
427
|
+
console.error("\nerror: gateway steering acknowledgement did not identify the active turn");
|
|
428
|
+
if (!interactive)
|
|
429
|
+
pipedInputFailed = true;
|
|
430
|
+
pendingRequests.delete(frame.id);
|
|
431
|
+
}
|
|
432
|
+
else if (interactive) {
|
|
433
|
+
pendingRequests.delete(frame.id);
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
request.terminalEventId = activeEventId;
|
|
437
|
+
finishRequestTerminal(request);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else if (interactive) {
|
|
441
|
+
pendingRequests.delete(frame.id);
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
finishRequestTerminal(request);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
pendingRequests.delete(frame.id);
|
|
449
|
+
}
|
|
450
|
+
notifyPendingWaiter();
|
|
451
|
+
}
|
|
141
452
|
}
|
|
142
|
-
else if (frame?.type === "event") {
|
|
453
|
+
else if (subscribed && frame?.type === "event") {
|
|
143
454
|
printEvent(frame, piboSessionId, renderState);
|
|
455
|
+
recordTerminalOutcome(frame.payload);
|
|
144
456
|
}
|
|
145
457
|
newlineIndex = buffer.indexOf("\n");
|
|
146
458
|
}
|
|
147
459
|
});
|
|
148
|
-
|
|
460
|
+
writeFrame(socket, subscriptionFrame);
|
|
149
461
|
try {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
462
|
+
await subscription;
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
closing = true;
|
|
466
|
+
socket.destroy();
|
|
467
|
+
throw error;
|
|
468
|
+
}
|
|
469
|
+
if (connectionFailure || socket.destroyed) {
|
|
470
|
+
closing = true;
|
|
471
|
+
socket.destroy();
|
|
472
|
+
throw connectionFailure ?? new GatewayClientExpectedError("Gateway connection closed");
|
|
473
|
+
}
|
|
474
|
+
console.error(`connected to pibo gateway at ${host}:${port}`);
|
|
475
|
+
console.error(`session: ${piboSessionId}`);
|
|
476
|
+
printGatewayClientHelp();
|
|
477
|
+
rl = readline.createInterface({ input, output, terminal: interactive });
|
|
478
|
+
if (interactive) {
|
|
479
|
+
rl.setPrompt("you> ");
|
|
480
|
+
rl.prompt();
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
for await (const line of rl) {
|
|
484
|
+
const result = handleGatewayClientLine(line, sendRequest, piboSessionId, renderState);
|
|
485
|
+
if (result === "error" && !interactive)
|
|
486
|
+
pipedInputFailed = true;
|
|
487
|
+
if (result === "quit")
|
|
155
488
|
break;
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
let params;
|
|
164
|
-
try {
|
|
165
|
-
params = level ? { level: parsePiboThinkingLevel(level) } : undefined;
|
|
166
|
-
}
|
|
167
|
-
catch (error) {
|
|
168
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
writeFrame(socket, {
|
|
172
|
-
type: "req",
|
|
173
|
-
id: randomUUID(),
|
|
174
|
-
event: params === undefined
|
|
175
|
-
? { type: "execution", piboSessionId, action: "thinking" }
|
|
176
|
-
: { type: "execution", piboSessionId, action: "thinking", params },
|
|
177
|
-
});
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
if (text === "/status" || text === "/clear" || text === "/abort") {
|
|
181
|
-
const action = text === "/status" ? "status" : text === "/clear" ? "clear_queue" : "abort";
|
|
182
|
-
writeFrame(socket, {
|
|
183
|
-
type: "req",
|
|
184
|
-
id: randomUUID(),
|
|
185
|
-
event: { type: "execution", piboSessionId, action },
|
|
186
|
-
});
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
if (text === "/goal" || text.startsWith("/goal ")) {
|
|
190
|
-
writeFrame(socket, {
|
|
191
|
-
type: "req",
|
|
192
|
-
id: randomUUID(),
|
|
193
|
-
event: { type: "execution", piboSessionId, action: "goal", params: { command: text.slice("/goal".length).trim() } },
|
|
194
|
-
});
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
writeFrame(socket, {
|
|
198
|
-
type: "req",
|
|
199
|
-
id: randomUUID(),
|
|
200
|
-
event: { type: "message", piboSessionId, text, source: "user" },
|
|
201
|
-
});
|
|
489
|
+
if (interactive && !socket.destroyed)
|
|
490
|
+
rl.prompt();
|
|
491
|
+
}
|
|
492
|
+
inputEnded = true;
|
|
493
|
+
if (!interactive) {
|
|
494
|
+
await waitForPending("acknowledgement", acknowledgementTimeoutMs);
|
|
495
|
+
await waitForPending("terminal", terminalTimeoutMs);
|
|
202
496
|
}
|
|
497
|
+
completedNormally = true;
|
|
203
498
|
}
|
|
204
499
|
finally {
|
|
500
|
+
closing = true;
|
|
205
501
|
rl.close();
|
|
206
|
-
|
|
502
|
+
if (completedNormally)
|
|
503
|
+
socket.end();
|
|
504
|
+
else
|
|
505
|
+
socket.destroy();
|
|
207
506
|
}
|
|
507
|
+
if (connectionFailure)
|
|
508
|
+
throw connectionFailure;
|
|
509
|
+
if (!interactive && pipedInputFailed)
|
|
510
|
+
process.exitCode = 1;
|
|
208
511
|
}
|
package/dist/gateway/protocol.js
CHANGED
|
@@ -16,6 +16,8 @@ function isJsonValue(value) {
|
|
|
16
16
|
return false;
|
|
17
17
|
}
|
|
18
18
|
export function isGatewayRequestFrame(value) {
|
|
19
|
+
// Keep this unversioned wire validator compatible with existing clients. Stricter
|
|
20
|
+
// message text, delivery, and identifier validation belongs at client boundaries.
|
|
19
21
|
if (!value || typeof value !== "object")
|
|
20
22
|
return false;
|
|
21
23
|
const frame = value;
|