@pasko70/pibo 2.0.0 → 2.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/dist/agent-runtimes/omp/adapter.js +581 -0
- package/dist/agent-runtimes/omp/auth.js +109 -0
- package/dist/agent-runtimes/omp/client.js +460 -0
- package/dist/agent-runtimes/omp/config.js +188 -0
- package/dist/agent-runtimes/omp/history.js +108 -0
- package/dist/agent-runtimes/omp/host-tools.js +201 -0
- package/dist/agent-runtimes/omp/models.js +86 -0
- package/dist/agent-runtimes/omp/process.js +283 -0
- package/dist/agent-runtimes/omp/protocol-types.js +17 -0
- package/dist/agent-runtimes/omp/resource-delivery.js +162 -0
- package/dist/agent-runtimes/omp/thread.js +141 -0
- package/dist/agent-runtimes/omp/turn.js +325 -0
- package/dist/index.js +1 -1
- package/dist/plugins/builtin.js +4 -2
- package/dist/plugins/omp.js +57 -0
- package/package.json +1 -1
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-2.0.0.vsix +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OMP RPC protocol wire types (read-only mirror of the OMP `--mode rpc`
|
|
3
|
+
* JSON-lines-over-stdio protocol from `@oh-my-pi/pi-coding-agent`).
|
|
4
|
+
*
|
|
5
|
+
* These are Pibo-owned, structurally-typed representations of the wire frames
|
|
6
|
+
* OMP emits/receives. They intentionally do NOT import from `@oh-my-pi/*`
|
|
7
|
+
* (which is Bun-only and cannot be imported under Node). Field names and shapes
|
|
8
|
+
* mirror OMP's `packages/coding-agent/src/modes/rpc/rpc-types.ts` and the
|
|
9
|
+
* session/agent event unions so the Pibo OMP adapter can speak the protocol
|
|
10
|
+
* without depending on the Bun runtime.
|
|
11
|
+
*/
|
|
12
|
+
export const OMP_RPC_PROTOCOL_NAME = "omp-rpc";
|
|
13
|
+
export const OMP_RPC_PROTOCOL_VERSION = 2;
|
|
14
|
+
export const OMP_RPC_SUPPORTED_PROTOCOL_VERSIONS = [1, 2];
|
|
15
|
+
export const OMP_RPC_MAX_FRAME_BYTES = 1024 * 1024;
|
|
16
|
+
export const OMP_RPC_MAX_REASSEMBLED_BYTES = 64 * 1024 * 1024;
|
|
17
|
+
export const OMP_RPC_CHUNK_PAYLOAD_BYTES = 256 * 1024;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
const MAX_CONTEXT_CONTRIBUTIONS = 128;
|
|
4
|
+
const MAX_CONTEXT_BYTES = 1024 * 1024;
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function boundedString(value, label) {
|
|
9
|
+
return typeof value === "string" ? value.slice(0, 4096) : label;
|
|
10
|
+
}
|
|
11
|
+
/** Longest common ancestor (dir) of a set of absolute file/dir paths. */
|
|
12
|
+
function commonParent(paths) {
|
|
13
|
+
if (paths.length === 0)
|
|
14
|
+
return undefined;
|
|
15
|
+
const segs = paths.map((p) => resolve(p).split(/[\\/]/));
|
|
16
|
+
let i = 0;
|
|
17
|
+
while (segs[0][i] !== undefined && segs.every((s) => s[i] === segs[0][i]))
|
|
18
|
+
i++;
|
|
19
|
+
return segs[0].slice(0, i).join("/");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Materializes Pibo-selected skills into the isolated OMP agent dir
|
|
23
|
+
* (`PI_CODING_AGENT_DIR`) and writes the OMP `config.yml` including
|
|
24
|
+
* `skills.customDirectories` (pointing at the directory the resource-service
|
|
25
|
+
* populated with `<skillName>/SKILL.md` entries OMP actually discovers) plus
|
|
26
|
+
* provider/model defaults. Project context is copied under the isolating
|
|
27
|
+
* context dir for diagnostics only and reported `unsupported` — OMP loads its
|
|
28
|
+
* own project context via AGENTS.md discovery and Pibo has no injection seam.
|
|
29
|
+
* The real project's `.omp/skills`, `AGENTS.md` are untouched — OMP keeps
|
|
30
|
+
* discovering them natively.
|
|
31
|
+
*
|
|
32
|
+
* Called BEFORE spawning OMP: OMP reads config.yml at startup, and
|
|
33
|
+
* `skills.customDirectories` dirs are scanned for `<skill>/SKILL.md` subdirs on
|
|
34
|
+
* every session start. The resource-service materializes shared content before
|
|
35
|
+
* `openSession` returns, so `getSkillPaths("materialized")` are already valid.
|
|
36
|
+
*/
|
|
37
|
+
export class OmpResourceDelivery {
|
|
38
|
+
config;
|
|
39
|
+
paths;
|
|
40
|
+
resources;
|
|
41
|
+
constructor(config, paths, resources) {
|
|
42
|
+
this.config = config;
|
|
43
|
+
this.paths = paths;
|
|
44
|
+
this.resources = resources;
|
|
45
|
+
}
|
|
46
|
+
get configYamlPath() {
|
|
47
|
+
return this.paths.config;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Skill directories OMP should scan, derived from the materialized paths.
|
|
51
|
+
* `copyAgentRuntimeSkillDirectory` yields `<skillsRoot>/<skillName>/SKILL.md`;
|
|
52
|
+
* OMP's `scanSkillsFromDir` scans a dir containing `<skill>/SKILL.md`
|
|
53
|
+
* subdirs, which is the common parent of every materialized SKILL.md.
|
|
54
|
+
*/
|
|
55
|
+
get customSkillDirectories() {
|
|
56
|
+
if (!this.resources)
|
|
57
|
+
return [];
|
|
58
|
+
const materialized = this.resources.getSkillPaths("materialized");
|
|
59
|
+
if (materialized.length === 0)
|
|
60
|
+
return [];
|
|
61
|
+
const dirs = materialized.map((p) => dirname(resolve(p)));
|
|
62
|
+
const shared = commonParent(dirs);
|
|
63
|
+
return shared ? [shared] : dirs;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Materialize selected skills/context and write the OMP config. MUST be
|
|
67
|
+
* called BEFORE spawning the OMP process (OMP reads config.yml at startup).
|
|
68
|
+
* Returns a delivery report per contribution.
|
|
69
|
+
*/
|
|
70
|
+
async prepare() {
|
|
71
|
+
const reports = [];
|
|
72
|
+
const diagnostics = [];
|
|
73
|
+
// Context contributions are surfaced for debug traceability only. OMP
|
|
74
|
+
// does not read a `projectContextFiles` config key or any Pibo-injected
|
|
75
|
+
// file — project context arrives via its own AGENTS.md/rules discovery in
|
|
76
|
+
// the session cwd (the user's workspace, which we must not mutate). We
|
|
77
|
+
// write copies under the isolated context dir for diagnostics, but report
|
|
78
|
+
// them unsupported so callers never mistake them for engine-delivered.
|
|
79
|
+
const contributions = this.resources?.getContextContributions() ?? [];
|
|
80
|
+
const totalBytes = contributions.reduce((sum, c) => sum + (c.byteSize ?? c.content?.length ?? 0), 0);
|
|
81
|
+
if (contributions.length > MAX_CONTEXT_CONTRIBUTIONS || totalBytes > MAX_CONTEXT_BYTES) {
|
|
82
|
+
diagnostics.push({
|
|
83
|
+
severity: "warning",
|
|
84
|
+
code: "orp_context_exceeds_limit",
|
|
85
|
+
message: "Pibo context contributions exceed the OMP materialization limit; excess is omitted.",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
await mkdir(this.paths.context, { recursive: true });
|
|
89
|
+
for (const contribution of contributions.slice(0, MAX_CONTEXT_CONTRIBUTIONS)) {
|
|
90
|
+
const safeName = (contribution.label || `contribution-${reports.length}`)
|
|
91
|
+
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
92
|
+
.replace(/^-+|-+$/g, "")
|
|
93
|
+
.slice(0, 64) || "contribution.md";
|
|
94
|
+
const target = join(this.paths.context, `${reports.length}-${safeName}.md`);
|
|
95
|
+
try {
|
|
96
|
+
await writeFile(target, contribution.content ?? "", "utf8");
|
|
97
|
+
reports.push({
|
|
98
|
+
contributionId: contribution.id,
|
|
99
|
+
status: "unsupported",
|
|
100
|
+
mode: "omp-native-agents-md-discovery",
|
|
101
|
+
fidelity: "none",
|
|
102
|
+
target,
|
|
103
|
+
diagnostic: "OMP loads project context via its own AGENTS.md discovery; Pibo context is copied here for diagnostics only and not consumed by OMP.",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
108
|
+
diagnostics.push({
|
|
109
|
+
severity: "error",
|
|
110
|
+
code: "orp_context_materialization_failed",
|
|
111
|
+
message: `Failed to materialize context contribution "${contribution.id}": ${message}`,
|
|
112
|
+
contributionId: contribution.id,
|
|
113
|
+
});
|
|
114
|
+
reports.push({
|
|
115
|
+
contributionId: contribution.id,
|
|
116
|
+
status: "failed",
|
|
117
|
+
mode: "materialized",
|
|
118
|
+
fidelity: "none",
|
|
119
|
+
diagnostic: message,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Skills: point OMP at directories populated with <skillName>/SKILL.md.
|
|
124
|
+
const customDirectories = this.customSkillDirectories;
|
|
125
|
+
await this.writeConfig({ customDirectories });
|
|
126
|
+
return { reports, diagnostics };
|
|
127
|
+
}
|
|
128
|
+
async writeConfig(opts) {
|
|
129
|
+
const lines = [];
|
|
130
|
+
lines.push("setupVersion: 1");
|
|
131
|
+
if (this.config.defaultProvider && this.config.defaultModel) {
|
|
132
|
+
lines.push(`modelRoles:`);
|
|
133
|
+
lines.push(` default: ${this.config.defaultProvider}/${this.config.defaultModel}:max`);
|
|
134
|
+
}
|
|
135
|
+
if (opts.customDirectories.length > 0) {
|
|
136
|
+
lines.push(`skills:`);
|
|
137
|
+
lines.push(` customDirectories:`);
|
|
138
|
+
for (const dir of opts.customDirectories) {
|
|
139
|
+
lines.push(` - ${JSON.stringify(dir)}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
await mkdir(dirname(this.paths.config), { recursive: true });
|
|
143
|
+
await writeFile(this.paths.config, `${lines.join("\n")}\n`, "utf8");
|
|
144
|
+
}
|
|
145
|
+
async readConfig() {
|
|
146
|
+
try {
|
|
147
|
+
return await readFile(this.paths.config, "utf8");
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return "";
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export function contextContributionToString(c) {
|
|
155
|
+
return c.content ?? "";
|
|
156
|
+
}
|
|
157
|
+
export function isRecordValue(value) {
|
|
158
|
+
return isRecord(value);
|
|
159
|
+
}
|
|
160
|
+
export function boundedContextValue(value, label) {
|
|
161
|
+
return boundedString(value, label);
|
|
162
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { OMP_RPC_PROTOCOL_NAME, OMP_RPC_PROTOCOL_VERSION } from "./protocol-types.js";
|
|
2
|
+
export const OMP_ADAPTER_ID = "orp";
|
|
3
|
+
export const OMP_ADAPTER_VERSION = "1.0.0";
|
|
4
|
+
/**
|
|
5
|
+
* OMP session-file lifecycle controller. OMP owns its session files (JSONL
|
|
6
|
+
* session transcripts) under the isolated `PI_CODING_AGENT_DIR` sessions dir.
|
|
7
|
+
* Pibo records only an opaque binding locator pointing at OMP's native session
|
|
8
|
+
* id/file, so resume/re-attach target the native session through `switch_session`.
|
|
9
|
+
*/
|
|
10
|
+
export class OmpThreadController {
|
|
11
|
+
client;
|
|
12
|
+
cwd;
|
|
13
|
+
snapshot;
|
|
14
|
+
forkCandidatesCache = [];
|
|
15
|
+
constructor(client, cwd, initial) {
|
|
16
|
+
this.client = client;
|
|
17
|
+
this.cwd = cwd;
|
|
18
|
+
this.snapshot = { sessionId: initial.sessionId, messageCount: 0, cwd };
|
|
19
|
+
}
|
|
20
|
+
get current() {
|
|
21
|
+
return this.snapshot;
|
|
22
|
+
}
|
|
23
|
+
getSessionSnapshot(runtimeInstanceId) {
|
|
24
|
+
return {
|
|
25
|
+
adapterId: OMP_ADAPTER_ID,
|
|
26
|
+
runtimeInstanceId,
|
|
27
|
+
nativeSessionId: this.snapshot.sessionId,
|
|
28
|
+
locator: {
|
|
29
|
+
kind: "adapter-resolved",
|
|
30
|
+
value: this.snapshot.sessionFile ?? this.snapshot.sessionId,
|
|
31
|
+
},
|
|
32
|
+
cwd: this.cwd,
|
|
33
|
+
name: this.snapshot.sessionName,
|
|
34
|
+
metadata: {
|
|
35
|
+
protocol: OMP_RPC_PROTOCOL_NAME,
|
|
36
|
+
protocolVersion: OMP_RPC_PROTOCOL_VERSION,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async refresh() {
|
|
41
|
+
const state = await this.client.request({ type: "get_state" }, "get_state");
|
|
42
|
+
const data = state["data"];
|
|
43
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
44
|
+
const record = data;
|
|
45
|
+
this.snapshot = {
|
|
46
|
+
sessionId: typeof record.sessionId === "string" ? record.sessionId : this.snapshot.sessionId,
|
|
47
|
+
sessionName: typeof record.sessionName === "string" ? record.sessionName : undefined,
|
|
48
|
+
sessionFile: typeof record.sessionFile === "string" ? record.sessionFile : undefined,
|
|
49
|
+
messageCount: typeof record.messageCount === "number" ? record.messageCount : 0,
|
|
50
|
+
cwd: this.cwd,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async listSessions(runtimeInstanceId) {
|
|
55
|
+
await this.refresh();
|
|
56
|
+
const snapshot = this.getSessionSnapshot(runtimeInstanceId);
|
|
57
|
+
return [{ ...snapshot, messageCount: this.snapshot.messageCount, name: this.snapshot.sessionName }];
|
|
58
|
+
}
|
|
59
|
+
/** Fetch branch candidates from OMP and cache them for the sync SPI. */
|
|
60
|
+
async loadForkCandidates(runtimeInstanceId) {
|
|
61
|
+
try {
|
|
62
|
+
const result = await this.client.request({ type: "get_branch_messages" }, "get_branch_messages");
|
|
63
|
+
const data = result["data"];
|
|
64
|
+
if (data && typeof data === "object" && !Array.isArray(data) && "messages" in data) {
|
|
65
|
+
const messages = data.messages;
|
|
66
|
+
if (Array.isArray(messages)) {
|
|
67
|
+
const candidates = [];
|
|
68
|
+
for (const entry of messages) {
|
|
69
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
70
|
+
const rec = entry;
|
|
71
|
+
if (typeof rec.entryId === "string") {
|
|
72
|
+
candidates.push({
|
|
73
|
+
entryId: rec.entryId,
|
|
74
|
+
text: typeof rec.text === "string" ? rec.text : "",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
this.forkCandidatesCache = candidates;
|
|
80
|
+
return candidates;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Branch candidates are optional; fall through to empty.
|
|
86
|
+
}
|
|
87
|
+
this.forkCandidatesCache = [];
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
/** Sync accessor for the SPI (get_available_commands via a warm-up load). */
|
|
91
|
+
cachedForkCandidates() {
|
|
92
|
+
return this.forkCandidatesCache;
|
|
93
|
+
}
|
|
94
|
+
async forkSession(runtimeInstanceId, entryId) {
|
|
95
|
+
const previous = structuredClone(this.getSessionSnapshot(runtimeInstanceId));
|
|
96
|
+
const result = await this.client.request({ type: "branch", entryId }, "branch");
|
|
97
|
+
const data = result["data"];
|
|
98
|
+
const cancelled = Boolean(data && typeof data === "object" && !Array.isArray(data) && data.cancelled === true);
|
|
99
|
+
await this.refresh();
|
|
100
|
+
const current = this.getSessionSnapshot(runtimeInstanceId);
|
|
101
|
+
return { previous, current, cancelled };
|
|
102
|
+
}
|
|
103
|
+
async switchSession(runtimeInstanceId, sessionPath) {
|
|
104
|
+
const previous = structuredClone(this.getSessionSnapshot(runtimeInstanceId));
|
|
105
|
+
const result = await this.client.request({ type: "switch_session", sessionPath }, "switch_session");
|
|
106
|
+
const data = result["data"];
|
|
107
|
+
const cancelled = Boolean(data && typeof data === "object" && !Array.isArray(data) && data.cancelled === true);
|
|
108
|
+
await this.refresh();
|
|
109
|
+
const current = this.getSessionSnapshot(runtimeInstanceId);
|
|
110
|
+
return { previous, current, cancelled };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export async function readOmpAvailableCommands(client) {
|
|
114
|
+
try {
|
|
115
|
+
const result = await client.request({ type: "get_available_commands" }, "get_available_commands");
|
|
116
|
+
const data = result["data"];
|
|
117
|
+
if (data && typeof data === "object" && !Array.isArray(data) && "commands" in data) {
|
|
118
|
+
const commands = data.commands;
|
|
119
|
+
if (Array.isArray(commands)) {
|
|
120
|
+
return commands
|
|
121
|
+
.filter((c) => Boolean(c) && typeof c === "object")
|
|
122
|
+
.filter(isOmpCommand)
|
|
123
|
+
.map((c) => ({
|
|
124
|
+
name: c.name,
|
|
125
|
+
description: c.description,
|
|
126
|
+
source: String(c.source ?? "unknown"),
|
|
127
|
+
aliases: c.aliases,
|
|
128
|
+
inputHint: c.input?.hint,
|
|
129
|
+
subcommands: c.subcommands,
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// available_commands is best-effort; return empty if unavailable.
|
|
136
|
+
}
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
function isOmpCommand(c) {
|
|
140
|
+
return typeof c.name === "string" && c.name.length > 0;
|
|
141
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/** Hard cap on how long a streaming turn may run before we resolve it. */
|
|
2
|
+
const DEFAULT_TURN_STREAM_TIMEOUT_MS = 10 * 60 * 1_000;
|
|
3
|
+
// Test hook: allows the deadline to be set tiny (e.g. 250ms) so unit tests can
|
|
4
|
+
// exercise the timeout path without waiting ten minutes.
|
|
5
|
+
function turnStreamTimeoutMs() {
|
|
6
|
+
if (typeof process !== "undefined" && process.env?.PIBO_OMP_TURN_TIMEOUT_MS) {
|
|
7
|
+
const parsed = Number(process.env.PIBO_OMP_TURN_TIMEOUT_MS);
|
|
8
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
9
|
+
return parsed;
|
|
10
|
+
}
|
|
11
|
+
return DEFAULT_TURN_STREAM_TIMEOUT_MS;
|
|
12
|
+
}
|
|
13
|
+
function deferred() {
|
|
14
|
+
const d = {};
|
|
15
|
+
d.promise = new Promise((resolve, reject) => {
|
|
16
|
+
d.resolve = resolve;
|
|
17
|
+
d.reject = reject;
|
|
18
|
+
});
|
|
19
|
+
return d;
|
|
20
|
+
}
|
|
21
|
+
export class OmpRpcClientProtocolError extends Error {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "OmpRpcClientProtocolError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function isOmpRpcAgentEvent(frame) {
|
|
28
|
+
return frame.type === "agent_start"
|
|
29
|
+
|| frame.type === "agent_end"
|
|
30
|
+
|| frame.type === "turn_start"
|
|
31
|
+
|| frame.type === "turn_end"
|
|
32
|
+
|| frame.type === "message_start"
|
|
33
|
+
|| frame.type === "message_end"
|
|
34
|
+
|| frame.type === "auto_compaction_start"
|
|
35
|
+
|| frame.type === "auto_compaction_end"
|
|
36
|
+
|| frame.type === "auto_retry_start"
|
|
37
|
+
|| frame.type === "auto_retry_end"
|
|
38
|
+
|| frame.type === "retry_fallback_applied"
|
|
39
|
+
|| frame.type === "retry_fallback_succeeded"
|
|
40
|
+
|| frame.type === "model_changed"
|
|
41
|
+
|| frame.type === "ttsr_triggered"
|
|
42
|
+
|| frame.type === "todo_reminder"
|
|
43
|
+
|| frame.type === "todo_auto_clear"
|
|
44
|
+
|| frame.type === "irc_message"
|
|
45
|
+
|| frame.type === "notice"
|
|
46
|
+
|| frame.type === "thinking_level_changed"
|
|
47
|
+
|| frame.type === "goal_updated";
|
|
48
|
+
}
|
|
49
|
+
function isRecord(value) {
|
|
50
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
51
|
+
}
|
|
52
|
+
function assistantEventText(event) {
|
|
53
|
+
if (event.type === "text_delta")
|
|
54
|
+
return { text: event.delta };
|
|
55
|
+
if (event.type === "text_end")
|
|
56
|
+
return { text: event.content };
|
|
57
|
+
if (event.type === "thinking_delta")
|
|
58
|
+
return { thinking: event.delta };
|
|
59
|
+
if (event.type === "thinking_end")
|
|
60
|
+
return { thinking: event.content };
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
function toolCallFromEvent(event) {
|
|
64
|
+
if (event.type !== "toolcall_end")
|
|
65
|
+
return undefined;
|
|
66
|
+
const toolCall = event.toolCall;
|
|
67
|
+
if (!toolCall)
|
|
68
|
+
return undefined;
|
|
69
|
+
return { id: toolCall.id, name: toolCall.name, args: toolCall.arguments };
|
|
70
|
+
}
|
|
71
|
+
export class OmpRpcTurnController {
|
|
72
|
+
client;
|
|
73
|
+
emit;
|
|
74
|
+
pending;
|
|
75
|
+
disposed = false;
|
|
76
|
+
unsubscribeFrames;
|
|
77
|
+
constructor(client, emit) {
|
|
78
|
+
this.client = client;
|
|
79
|
+
this.emit = emit;
|
|
80
|
+
this.unsubscribeFrames = client.subscribeFrames((frame) => {
|
|
81
|
+
if (this.disposed)
|
|
82
|
+
return;
|
|
83
|
+
try {
|
|
84
|
+
this.handleFrame(frame);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
this.handleProtocolFailure(error);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
this.client.subscribeDiagnostics((message) => {
|
|
91
|
+
if (this.pending && message)
|
|
92
|
+
this.emitWarning(message);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
get streaming() {
|
|
96
|
+
return this.pending !== undefined;
|
|
97
|
+
}
|
|
98
|
+
emitWarning(message) {
|
|
99
|
+
this.emit({ type: "warning", message });
|
|
100
|
+
}
|
|
101
|
+
handleProtocolFailure(error) {
|
|
102
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
103
|
+
this.emit({ type: "error", message });
|
|
104
|
+
if (this.pending && !this.pending.interrupted) {
|
|
105
|
+
this.pending.turnSettled.reject(new OmpRpcClientProtocolError(`OMP RPC protocol failure while streaming a turn: ${message}`));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Start a prompt turn. Strategy per MUST-FIX #4:
|
|
110
|
+
* - Send `prompt`, wait for the correlated `response`.
|
|
111
|
+
* - If the response reports agentInvoked:false (a local-only slash command or
|
|
112
|
+
* skill that never invokes the model), resolve the turn IMMEDIATELY —
|
|
113
|
+
* there is no agent_start/agent_end stream.
|
|
114
|
+
* - If agentInvoked:true, await the terminal agent_end with isTerminal:true.
|
|
115
|
+
*/
|
|
116
|
+
async prompt(text) {
|
|
117
|
+
this.assertActive();
|
|
118
|
+
if (this.pending)
|
|
119
|
+
throw new Error("An OMP turn is already in progress.");
|
|
120
|
+
// Set pending BEFORE sending so frames emitted in the same stdout chunk
|
|
121
|
+
// (or before the response resolves) are attributed to this turn.
|
|
122
|
+
const turn = {
|
|
123
|
+
agentInvoked: true,
|
|
124
|
+
responseSettled: deferred(),
|
|
125
|
+
turnSettled: deferred(),
|
|
126
|
+
interrupted: false,
|
|
127
|
+
};
|
|
128
|
+
this.pending = turn;
|
|
129
|
+
try {
|
|
130
|
+
await this.executePrompt(turn, text);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (this.pending === turn)
|
|
134
|
+
this.pending = undefined;
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async executePrompt(turn, text) {
|
|
139
|
+
let response;
|
|
140
|
+
try {
|
|
141
|
+
response = await this.client.request({ type: "prompt", message: text }, "prompt");
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
turn.responseSettled.resolve();
|
|
145
|
+
}
|
|
146
|
+
const data = response["data"];
|
|
147
|
+
const invoked = !isRecord(data) || !("agentInvoked" in data) || data.agentInvoked !== false;
|
|
148
|
+
turn.agentInvoked = invoked;
|
|
149
|
+
if (!invoked) {
|
|
150
|
+
// Local-only slash/skill command: no agent stream will come (MUST-FIX #4).
|
|
151
|
+
if (this.pending === turn)
|
|
152
|
+
this.pending = undefined;
|
|
153
|
+
turn.turnSettled.resolve();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// A running agent turn normally ends with a terminal agent_end. Guard
|
|
157
|
+
// against OMP never emitting it: resolve after a hard deadline so
|
|
158
|
+
// prompt() cannot hang forever.
|
|
159
|
+
const deadline = setTimeout(() => {
|
|
160
|
+
if (this.pending === turn)
|
|
161
|
+
this.pending = undefined;
|
|
162
|
+
turn.turnSettled.resolve();
|
|
163
|
+
}, turnStreamTimeoutMs());
|
|
164
|
+
try {
|
|
165
|
+
await turn.turnSettled.promise;
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
clearTimeout(deadline);
|
|
169
|
+
if (this.pending === turn)
|
|
170
|
+
this.pending = undefined;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async steer(text) {
|
|
174
|
+
this.assertActive();
|
|
175
|
+
await this.client.request({ type: "steer", message: text }, "steer");
|
|
176
|
+
}
|
|
177
|
+
async interrupt() {
|
|
178
|
+
this.assertActive();
|
|
179
|
+
if (this.pending)
|
|
180
|
+
this.pending.interrupted = true;
|
|
181
|
+
try {
|
|
182
|
+
await this.client.request({ type: "abort" }, "abort");
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
if (this.pending && !this.pending.interrupted)
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
dispose() {
|
|
190
|
+
this.disposed = true;
|
|
191
|
+
this.unsubscribeFrames();
|
|
192
|
+
if (this.pending) {
|
|
193
|
+
this.pending.turnSettled.reject(new Error("OMP turn disposed."));
|
|
194
|
+
this.pending = undefined;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
assertActive() {
|
|
198
|
+
if (this.disposed)
|
|
199
|
+
throw new Error("OMP turn controller is disposed.");
|
|
200
|
+
}
|
|
201
|
+
handleFrame(frame) {
|
|
202
|
+
if (this.pending === undefined)
|
|
203
|
+
return;
|
|
204
|
+
if (frame.type === "agent_end") {
|
|
205
|
+
if (frame.isTerminal !== false) {
|
|
206
|
+
const turn = this.pending;
|
|
207
|
+
this.pending = undefined;
|
|
208
|
+
turn.turnSettled.resolve();
|
|
209
|
+
turn.responseSettled.resolve();
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (frame.type === "message_update") {
|
|
214
|
+
const event = frame.assistantMessageEvent;
|
|
215
|
+
if (!event)
|
|
216
|
+
return;
|
|
217
|
+
if (event.type === "text_delta" || event.type === "text_end") {
|
|
218
|
+
this.emit({ type: "assistant_delta", text: event.type === "text_end" ? event.content : event.delta, contentIndex: event.contentIndex });
|
|
219
|
+
}
|
|
220
|
+
else if (event.type === "thinking_delta" || event.type === "thinking_end") {
|
|
221
|
+
this.emit({ type: "reasoning_delta", text: event.type === "thinking_end" ? event.content : event.delta, contentIndex: event.contentIndex });
|
|
222
|
+
}
|
|
223
|
+
else if (event.type === "thinking_start") {
|
|
224
|
+
this.emit({ type: "reasoning_started", contentIndex: event.contentIndex });
|
|
225
|
+
}
|
|
226
|
+
else if (event.type === "toolcall_end") {
|
|
227
|
+
this.emit({
|
|
228
|
+
type: "tool_call",
|
|
229
|
+
toolCallId: event.toolCall.id,
|
|
230
|
+
toolName: event.toolCall.name,
|
|
231
|
+
args: event.toolCall.arguments,
|
|
232
|
+
argsComplete: true,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
else if (event.type === "done") {
|
|
236
|
+
// message done; terminal is signalled by agent_end
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (frame.type === "tool_execution_start") {
|
|
241
|
+
this.emit({
|
|
242
|
+
type: "tool_execution_started",
|
|
243
|
+
toolCallId: frame.toolCallId,
|
|
244
|
+
toolName: frame.toolName,
|
|
245
|
+
args: frame.args,
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (frame.type === "tool_execution_update") {
|
|
250
|
+
this.emit({
|
|
251
|
+
type: "tool_execution_updated",
|
|
252
|
+
toolCallId: frame.toolCallId,
|
|
253
|
+
toolName: frame.toolName,
|
|
254
|
+
args: frame.args,
|
|
255
|
+
partialResult: frame.partialResult,
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (frame.type === "tool_execution_end") {
|
|
260
|
+
this.emit({
|
|
261
|
+
type: "tool_execution_finished",
|
|
262
|
+
toolCallId: frame.toolCallId,
|
|
263
|
+
toolName: frame.toolName,
|
|
264
|
+
result: frame.result,
|
|
265
|
+
isError: frame.isError === true,
|
|
266
|
+
});
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
this.handleSessionEvent(frame);
|
|
270
|
+
}
|
|
271
|
+
handleSessionEvent(frame) {
|
|
272
|
+
if (!isOmpRpcAgentEvent(frame))
|
|
273
|
+
return;
|
|
274
|
+
switch (frame.type) {
|
|
275
|
+
case "turn_start":
|
|
276
|
+
this.emit({ type: "turn_started" });
|
|
277
|
+
break;
|
|
278
|
+
case "turn_end":
|
|
279
|
+
this.emit({ type: "turn_completed", status: "completed" });
|
|
280
|
+
break;
|
|
281
|
+
case "auto_compaction_start":
|
|
282
|
+
this.emit({ type: "compaction_start", reason: frame.reason });
|
|
283
|
+
break;
|
|
284
|
+
case "auto_compaction_end":
|
|
285
|
+
this.emit({
|
|
286
|
+
type: "compaction_end",
|
|
287
|
+
reason: frame.action,
|
|
288
|
+
aborted: frame.aborted,
|
|
289
|
+
errorMessage: frame.errorMessage,
|
|
290
|
+
result: frame.result,
|
|
291
|
+
});
|
|
292
|
+
break;
|
|
293
|
+
case "notice":
|
|
294
|
+
if (frame.level === "error") {
|
|
295
|
+
this.emit({ type: "error", message: frame.message });
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
this.emit({ type: "warning", message: frame.message });
|
|
299
|
+
}
|
|
300
|
+
break;
|
|
301
|
+
default:
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/** Normalize an OMP AgentMessage usage into Pibo usage, if present. */
|
|
306
|
+
static usageFromMessage(message) {
|
|
307
|
+
if (!isRecord(message))
|
|
308
|
+
return undefined;
|
|
309
|
+
const usage = message.usage;
|
|
310
|
+
if (!isRecord(usage))
|
|
311
|
+
return undefined;
|
|
312
|
+
const record = usage;
|
|
313
|
+
const input = typeof record.inputTokens === "number" ? record.inputTokens : 0;
|
|
314
|
+
const output = typeof record.outputTokens === "number" ? record.outputTokens : 0;
|
|
315
|
+
const total = typeof record.totalTokens === "number" ? record.totalTokens : input + output;
|
|
316
|
+
return {
|
|
317
|
+
inputTokens: input,
|
|
318
|
+
outputTokens: output,
|
|
319
|
+
cacheReadTokens: typeof record.cachedInputTokens === "number" ? record.cachedInputTokens : 0,
|
|
320
|
+
cacheWriteTokens: 0,
|
|
321
|
+
reasoningTokens: typeof record.reasoningTokens === "number" ? record.reasoningTokens : 0,
|
|
322
|
+
totalTokens: total,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createDefaultPiboProfile, createDefaultPiboPluginRegistry, createDefaultPiboPlugins, createGatewayProducerPiboPluginRegistry, createGatewayProducerPiboProfile, CODEX_NATIVE_PROFILE_NAME, CODEX_NATIVE_RUNTIME_INSTANCE_ID, piboCodexNativePlugin, piboCorePlugin, piboGatewayProducerPlugin, } from "./plugins/builtin.js";
|
|
1
|
+
export { createDefaultPiboProfile, createDefaultPiboPluginRegistry, createDefaultPiboPlugins, createGatewayProducerPiboPluginRegistry, createGatewayProducerPiboProfile, CODEX_NATIVE_PROFILE_NAME, CODEX_NATIVE_RUNTIME_INSTANCE_ID, piboCodexNativePlugin, OMP_PROFILE_NAME, OMP_RUNTIME_INSTANCE_ID, piboOmpPlugin, piboCorePlugin, piboGatewayProducerPlugin, } from "./plugins/builtin.js";
|
|
2
2
|
export { createPiboBetterAuthPlugin } from "./plugins/better-auth.js";
|
|
3
3
|
export { createPiboChatWebPlugin } from "./plugins/chat-web.js";
|
|
4
4
|
export { createPiboContextFilesPlugin } from "./plugins/context-files.js";
|
package/dist/plugins/builtin.js
CHANGED
|
@@ -12,10 +12,12 @@ import { piboCodexCompatPlugin } from "./codex-compat.js";
|
|
|
12
12
|
import { piboCodexNativePlugin } from "./codex-native.js";
|
|
13
13
|
import { addPiboNativeToolingContext, registerPiboNativeTooling } from "./native-tooling.js";
|
|
14
14
|
import { piboWebAnnotationsPlugin } from "./web-annotations.js";
|
|
15
|
+
import { piboOmpPlugin } from "./omp.js";
|
|
15
16
|
import { definePiboPlugin, PiboPluginRegistry } from "./registry.js";
|
|
16
17
|
import { PI_AGENT_RUNTIME_DRIVER } from "../agent-runtimes/pi/adapter.js";
|
|
17
18
|
export { createDefaultPiboProfile, DEFAULT_PIBO_PROFILE_NAME } from "../core/default-profile.js";
|
|
18
19
|
export { CODEX_NATIVE_PROFILE_NAME, CODEX_NATIVE_RUNTIME_INSTANCE_ID, piboCodexNativePlugin, } from "./codex-native.js";
|
|
20
|
+
export { OMP_PROFILE_NAME, OMP_RUNTIME_INSTANCE_ID, piboOmpPlugin, } from "./omp.js";
|
|
19
21
|
const GATEWAY_PROFILE_TOOLS = ["pibo_gateway_send"];
|
|
20
22
|
const PIBO_PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
21
23
|
function builtinSkillPath(name) {
|
|
@@ -587,11 +589,11 @@ export const piboGatewayProducerPlugin = definePiboPlugin({
|
|
|
587
589
|
},
|
|
588
590
|
});
|
|
589
591
|
export function createDefaultPiboPlugins() {
|
|
590
|
-
return [piboCorePlugin, piboCodexNativePlugin, piboCodexCompatPlugin, piboWebAnnotationsPlugin];
|
|
592
|
+
return [piboCorePlugin, piboCodexNativePlugin, piboCodexCompatPlugin, piboWebAnnotationsPlugin, piboOmpPlugin];
|
|
591
593
|
}
|
|
592
594
|
export function createGatewayProducerPiboPluginRegistry() {
|
|
593
595
|
return PiboPluginRegistry.create({
|
|
594
|
-
plugins: [piboCorePlugin, piboCodexNativePlugin, piboGatewayProducerPlugin, piboCodexCompatPlugin, piboWebAnnotationsPlugin],
|
|
596
|
+
plugins: [piboCorePlugin, piboCodexNativePlugin, piboGatewayProducerPlugin, piboCodexCompatPlugin, piboWebAnnotationsPlugin, piboOmpPlugin],
|
|
595
597
|
});
|
|
596
598
|
}
|
|
597
599
|
export function createDefaultPiboPluginRegistry() {
|