ework-daemon 0.4.27 → 0.4.28
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/package.json +1 -1
- package/src/config.ts +21 -1
- package/src/opencode.ts +10 -5
- package/src/runtime/pi-backend.ts +167 -0
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -28,12 +28,20 @@ export const configSchema = z.object({
|
|
|
28
28
|
}),
|
|
29
29
|
opencode: z.object({
|
|
30
30
|
binary: z.string().default("opencode"),
|
|
31
|
-
baseWorkdir: z.string()
|
|
31
|
+
baseWorkdir: z.string().default(
|
|
32
|
+
`${process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share")}/ework-aio/opencode-workdir`
|
|
33
|
+
),
|
|
32
34
|
dbPath: z.string().default(
|
|
33
35
|
`${process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share")}/opencode/opencode.db`
|
|
34
36
|
),
|
|
35
37
|
defaultModel: z.string().default(""),
|
|
36
38
|
}),
|
|
39
|
+
pi: z.object({
|
|
40
|
+
binary: z.string().default("pi"),
|
|
41
|
+
provider: z.string().default("bailian"),
|
|
42
|
+
defaultModel: z.string().default(""),
|
|
43
|
+
}).optional(),
|
|
44
|
+
runtime: z.enum(["opencode", "pi"]).default("opencode"),
|
|
37
45
|
work: z.object({
|
|
38
46
|
capacity: z.coerce.number().int().positive().default(4),
|
|
39
47
|
maxConcurrent: z.coerce.number().int().positive().default(4),
|
|
@@ -138,6 +146,12 @@ export function loadConfig(): Config {
|
|
|
138
146
|
dbPath: process.env.OPENCODE_DB_PATH ?? `${process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share")}/opencode/opencode.db`,
|
|
139
147
|
defaultModel: process.env.WORK_DEFAULT_MODEL ?? TEST_DEFAULTS.opencode.defaultModel,
|
|
140
148
|
},
|
|
149
|
+
pi: {
|
|
150
|
+
binary: process.env.WORK_PI_BINARY ?? "pi",
|
|
151
|
+
provider: process.env.WORK_PI_PROVIDER ?? "bailian",
|
|
152
|
+
defaultModel: process.env.WORK_PI_DEFAULT_MODEL ?? process.env.WORK_DEFAULT_MODEL ?? "",
|
|
153
|
+
},
|
|
154
|
+
runtime: (process.env.WORK_RUNTIME ?? "opencode").trim().toLowerCase() as "opencode" | "pi",
|
|
141
155
|
work: readWorkSection(),
|
|
142
156
|
db: readDbSection(TEST_DEFAULTS.db.path),
|
|
143
157
|
completionCheck: process.env.COMPLETION_CHECK_API_KEY ? {
|
|
@@ -175,6 +189,12 @@ export function loadConfig(): Config {
|
|
|
175
189
|
dbPath: process.env.OPENCODE_DB_PATH ?? `${process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share")}/opencode/opencode.db`,
|
|
176
190
|
defaultModel: process.env.WORK_DEFAULT_MODEL ?? "",
|
|
177
191
|
},
|
|
192
|
+
pi: {
|
|
193
|
+
binary: process.env.WORK_PI_BINARY ?? "pi",
|
|
194
|
+
provider: process.env.WORK_PI_PROVIDER ?? "bailian",
|
|
195
|
+
defaultModel: process.env.WORK_PI_DEFAULT_MODEL ?? process.env.WORK_DEFAULT_MODEL ?? "",
|
|
196
|
+
},
|
|
197
|
+
runtime: (process.env.WORK_RUNTIME ?? "opencode").trim().toLowerCase() as "opencode" | "pi",
|
|
178
198
|
work: readWorkSection(),
|
|
179
199
|
db: readDbSection(PRODUCTION_DB_DEFAULT),
|
|
180
200
|
completionCheck: process.env.COMPLETION_CHECK_API_KEY ? {
|
package/src/opencode.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { IssueTracker, TrackerRef, TrackerEvent, TrackerComment, Issue, OpS
|
|
|
9
9
|
import { formatKey, parseKey } from "./trackers/types";
|
|
10
10
|
import type { RuntimeBackend, RuntimeHandle } from "./runtime/types";
|
|
11
11
|
import { OpencodeBackend } from "./runtime/opencode-backend";
|
|
12
|
+
import { PiBackend } from "./runtime/pi-backend";
|
|
12
13
|
|
|
13
14
|
// ─── Types ───
|
|
14
15
|
|
|
@@ -279,14 +280,18 @@ export function pickLastActive(sessions: OpSession[]): OpSession | undefined {
|
|
|
279
280
|
// ─── Engine ───
|
|
280
281
|
|
|
281
282
|
export interface EngineOptions {
|
|
282
|
-
/** DB-allocated logical daemon id (from Store.registerDaemon). */
|
|
283
283
|
daemonId: number;
|
|
284
|
-
/** Workdir + session-resume strategy; defaults to RecloneStrategy. */
|
|
285
284
|
takeover?: TakeoverStrategy;
|
|
286
|
-
/** Runtime backend (opencode/pi); defaults to OpencodeBackend. */
|
|
287
285
|
backend?: RuntimeBackend;
|
|
288
286
|
}
|
|
289
287
|
|
|
288
|
+
function createDefaultBackend(cfg: Config): RuntimeBackend {
|
|
289
|
+
if (cfg.runtime === "pi" && cfg.pi) {
|
|
290
|
+
return new PiBackend(cfg.pi.binary, cfg.pi.provider, cfg.pi.defaultModel, cfg.childEnvDeny);
|
|
291
|
+
}
|
|
292
|
+
return new OpencodeBackend(cfg.opencode.binary, cfg.opencode.dbPath, cfg.childEnvDeny);
|
|
293
|
+
}
|
|
294
|
+
|
|
290
295
|
export class Engine {
|
|
291
296
|
private cfg: Config;
|
|
292
297
|
private store: Store;
|
|
@@ -342,7 +347,7 @@ export class Engine {
|
|
|
342
347
|
this.trackers = trackers;
|
|
343
348
|
this.daemonId = opts.daemonId;
|
|
344
349
|
this.takeover = opts.takeover ?? new RecloneStrategy(cfg);
|
|
345
|
-
this.backend = opts.backend ??
|
|
350
|
+
this.backend = opts.backend ?? createDefaultBackend(cfg);
|
|
346
351
|
this.startGlobalObserver();
|
|
347
352
|
void this.recover();
|
|
348
353
|
}
|
|
@@ -978,7 +983,7 @@ export class Engine {
|
|
|
978
983
|
await this.store.updateSession(session.id, { opencodeSessionId: undefined });
|
|
979
984
|
}
|
|
980
985
|
|
|
981
|
-
const model = msg.model || this.cfg.opencode.defaultModel;
|
|
986
|
+
const model = msg.model || (this.cfg.runtime === "pi" && this.cfg.pi ? this.cfg.pi.defaultModel : this.cfg.opencode.defaultModel);
|
|
982
987
|
this.currentModel.set(k, model);
|
|
983
988
|
|
|
984
989
|
if (msg.sourceCommentId) {
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { spawn } from "bun";
|
|
2
|
+
import { readdirSync, existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { log } from "../logger";
|
|
5
|
+
import type {
|
|
6
|
+
RuntimeBackend,
|
|
7
|
+
RuntimeSpawnOpts,
|
|
8
|
+
RuntimeSpawnCallbacks,
|
|
9
|
+
RuntimeHandle,
|
|
10
|
+
SessionOutputResult,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
export class PiBackend implements RuntimeBackend {
|
|
14
|
+
readonly name = "pi";
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private binary: string,
|
|
18
|
+
private provider: string,
|
|
19
|
+
private defaultModel: string | undefined,
|
|
20
|
+
private childEnvDeny: string[] = [],
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
async spawn(opts: RuntimeSpawnOpts, cb: RuntimeSpawnCallbacks): Promise<RuntimeHandle> {
|
|
24
|
+
const args: string[] = [this.binary, "--mode", "json", "--print"];
|
|
25
|
+
|
|
26
|
+
const model = opts.model || this.defaultModel;
|
|
27
|
+
if (model) {
|
|
28
|
+
if (model.includes("/")) {
|
|
29
|
+
const slashIdx = model.indexOf("/");
|
|
30
|
+
const provider = model.slice(0, slashIdx);
|
|
31
|
+
const modelId = model.slice(slashIdx + 1);
|
|
32
|
+
args.push("--provider", provider, "--model", modelId);
|
|
33
|
+
} else {
|
|
34
|
+
args.push("--provider", this.provider, "--model", model);
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
args.push("--provider", this.provider);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (opts.resumeSessionId) {
|
|
41
|
+
args.push("--session-id", opts.resumeSessionId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
args.push(opts.prompt);
|
|
45
|
+
|
|
46
|
+
const childEnv = { ...opts.env };
|
|
47
|
+
for (const key of this.childEnvDeny) delete childEnv[key];
|
|
48
|
+
|
|
49
|
+
const proc = spawn({
|
|
50
|
+
cmd: args,
|
|
51
|
+
cwd: opts.workdir,
|
|
52
|
+
env: childEnv,
|
|
53
|
+
stdout: "pipe",
|
|
54
|
+
stderr: "pipe",
|
|
55
|
+
stdin: "ignore",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const stderrText = new Response(proc.stderr).text();
|
|
59
|
+
|
|
60
|
+
void this.readStdout(proc, cb);
|
|
61
|
+
|
|
62
|
+
return { pid: proc.pid, exited: proc.exited, stderrText };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private async readStdout(
|
|
66
|
+
proc: ReturnType<typeof spawn>,
|
|
67
|
+
cb: RuntimeSpawnCallbacks,
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
let captured = false;
|
|
70
|
+
const stdout = proc.stdout;
|
|
71
|
+
if (!stdout || typeof stdout === "number") return;
|
|
72
|
+
const reader = stdout.getReader();
|
|
73
|
+
const decoder = new TextDecoder();
|
|
74
|
+
let lineBuf = "";
|
|
75
|
+
|
|
76
|
+
while (true) {
|
|
77
|
+
const { done, value } = await reader.read();
|
|
78
|
+
if (done) break;
|
|
79
|
+
|
|
80
|
+
cb.onOutput();
|
|
81
|
+
|
|
82
|
+
if (!captured) {
|
|
83
|
+
lineBuf += decoder.decode(value, { stream: true });
|
|
84
|
+
const lines = lineBuf.split("\n");
|
|
85
|
+
lineBuf = lines.pop()!;
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
if (!line.trim()) continue;
|
|
88
|
+
try {
|
|
89
|
+
const ev = JSON.parse(line);
|
|
90
|
+
if (ev.type === "session" && ev.id) {
|
|
91
|
+
await cb.onSessionId(ev.id as string);
|
|
92
|
+
captured = true;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
} catch { /* not json */ }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async sessionExists(sessionId: string): Promise<boolean> {
|
|
102
|
+
const sessionDir = this.resolveSessionDir();
|
|
103
|
+
if (!existsSync(sessionDir)) return false;
|
|
104
|
+
try {
|
|
105
|
+
const files = readdirSync(sessionDir);
|
|
106
|
+
return files.some((f) => f.includes(sessionId) && f.endsWith(".jsonl"));
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async getSessionOutputTokens(sessionId: string | undefined): Promise<SessionOutputResult> {
|
|
113
|
+
if (!sessionId) return { hasOutput: true, tokenCount: 0 };
|
|
114
|
+
|
|
115
|
+
const filePath = this.findSessionFile(sessionId);
|
|
116
|
+
if (!filePath) return { hasOutput: true, tokenCount: 0 };
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const { readFileSync } = await import("fs");
|
|
120
|
+
const content = readFileSync(filePath, "utf-8");
|
|
121
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
122
|
+
let totalTokens = 0;
|
|
123
|
+
let hasAssistant = false;
|
|
124
|
+
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
try {
|
|
127
|
+
const ev = JSON.parse(line);
|
|
128
|
+
if (ev.type === "message" && ev.message?.role === "assistant") {
|
|
129
|
+
hasAssistant = true;
|
|
130
|
+
const output = ev.message?.usage?.output ?? 0;
|
|
131
|
+
totalTokens += output;
|
|
132
|
+
}
|
|
133
|
+
} catch { /* skip */ }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { hasOutput: hasAssistant && totalTokens > 0, tokenCount: totalTokens };
|
|
137
|
+
} catch {
|
|
138
|
+
return { hasOutput: true, tokenCount: 0 };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private resolveSessionDir(): string {
|
|
143
|
+
const envDir = process.env.PI_CODING_AGENT_SESSION_DIR;
|
|
144
|
+
if (envDir) return envDir;
|
|
145
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR || join(process.env.HOME || "~", ".pi", "agent");
|
|
146
|
+
return join(agentDir, "sessions");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private findSessionFile(sessionId: string): string | null {
|
|
150
|
+
const sessionDir = this.resolveSessionDir();
|
|
151
|
+
if (!existsSync(sessionDir)) return null;
|
|
152
|
+
try {
|
|
153
|
+
const subdirs = readdirSync(sessionDir, { withFileTypes: true });
|
|
154
|
+
for (const dir of subdirs) {
|
|
155
|
+
if (!dir.isDirectory()) continue;
|
|
156
|
+
const dirPath = join(sessionDir, dir.name);
|
|
157
|
+
const files = readdirSync(dirPath);
|
|
158
|
+
for (const f of files) {
|
|
159
|
+
if (f.includes(sessionId) && f.endsWith(".jsonl")) {
|
|
160
|
+
return join(dirPath, f);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} catch { /* not found */ }
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|