portable-agent-layer 0.84.5 → 0.85.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/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
import { inference, previewInferenceRoute } from "../hooks/lib/inference";
|
|
50
50
|
import { logDebug, recentHookErrors } from "../hooks/lib/log";
|
|
51
51
|
import { ensureRegistered, writeRegistryEntry } from "../hooks/lib/machine";
|
|
52
|
+
import { opencodeBackgroundModel } from "../hooks/lib/opencode-config";
|
|
52
53
|
import { palHome, palPkg, paths, platform, toPath } from "../hooks/lib/paths";
|
|
53
54
|
import { auditBindings, describeBindingIssue } from "../hooks/lib/projects";
|
|
54
55
|
import { telosStatus } from "../hooks/lib/telos-topics";
|
|
@@ -1051,6 +1052,14 @@ function doctor(silent = false): DoctorResult {
|
|
|
1051
1052
|
ok(`Would route to: ${preview.route} (${preview.reason})`);
|
|
1052
1053
|
}
|
|
1053
1054
|
}
|
|
1055
|
+
if (opencode.available) {
|
|
1056
|
+
const model = opencodeBackgroundModel();
|
|
1057
|
+
model
|
|
1058
|
+
? ok(`opencode background model: ${model}`)
|
|
1059
|
+
: warn(
|
|
1060
|
+
`opencode background model: not pinned — background inference uses the model last picked in the TUI. Pin one with "model" in ${resolve(platform.opencodeDir(), "config.json")}`
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1054
1063
|
if (process.env.PAL_INFERENCE_DISABLED === "1") {
|
|
1055
1064
|
warn(
|
|
1056
1065
|
"PAL_INFERENCE_DISABLED=1 — test kill-switch leaked into prod env; every inference call will return failure"
|
|
@@ -31,8 +31,9 @@ import {
|
|
|
31
31
|
isCursor,
|
|
32
32
|
isOpencode,
|
|
33
33
|
} from "./agent";
|
|
34
|
-
import { logDebug } from "./log";
|
|
34
|
+
import { logDebug, logError } from "./log";
|
|
35
35
|
import { HAIKU_MODEL } from "./models";
|
|
36
|
+
import { writeInstructionFreeConfig } from "./opencode-config";
|
|
36
37
|
import { buildSpawnGuardEnv, getInferenceDepth, SPAWN_GUARD_ENV } from "./spawn-guard";
|
|
37
38
|
import { findBinaryOnPath } from "./which";
|
|
38
39
|
|
|
@@ -171,13 +172,7 @@ export async function inference(opts: InferenceOptions): Promise<InferenceResult
|
|
|
171
172
|
const bin = getOpencodeBinary();
|
|
172
173
|
if (bin) {
|
|
173
174
|
logDebug("inference", `${tag} route=opencode-spawn agent=${agent}`);
|
|
174
|
-
return
|
|
175
|
-
bin,
|
|
176
|
-
buildOpencodeArgs(opts),
|
|
177
|
-
buildCliPrompt(opts),
|
|
178
|
-
opts,
|
|
179
|
-
extractOpencodeText
|
|
180
|
-
);
|
|
175
|
+
return inferenceViaOpencodeSpawn(bin, opts);
|
|
181
176
|
}
|
|
182
177
|
}
|
|
183
178
|
if (isCopilot()) {
|
|
@@ -366,6 +361,34 @@ export function buildOpencodeArgs(_opts: InferenceOptions): string[] {
|
|
|
366
361
|
return ["run", "--pure", "--format", "json"];
|
|
367
362
|
}
|
|
368
363
|
|
|
364
|
+
// Without its own AGENTS.md, opencode falls back to ~/.claude/CLAUDE.md, and it
|
|
365
|
+
// lists every Claude and external skill in its tool description.
|
|
366
|
+
const OPENCODE_WITHOUT_EXTERNAL_CONTEXT = {
|
|
367
|
+
OPENCODE_DISABLE_CLAUDE_CODE: "1",
|
|
368
|
+
OPENCODE_DISABLE_EXTERNAL_SKILLS: "1",
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
/** Runs from an empty directory so no project AGENTS.md is in reach either. */
|
|
372
|
+
async function inferenceViaOpencodeSpawn(
|
|
373
|
+
bin: string,
|
|
374
|
+
opts: InferenceOptions
|
|
375
|
+
): Promise<InferenceResult> {
|
|
376
|
+
const dir = await mkdtemp(join(tmpdir(), "pal-opencode-"));
|
|
377
|
+
try {
|
|
378
|
+
writeInstructionFreeConfig(dir);
|
|
379
|
+
return await inferenceViaCliSpawn(
|
|
380
|
+
bin,
|
|
381
|
+
buildOpencodeArgs(opts),
|
|
382
|
+
buildCliPrompt(opts),
|
|
383
|
+
opts,
|
|
384
|
+
extractOpencodeText,
|
|
385
|
+
{ cwd: dir, env: { XDG_CONFIG_HOME: dir, ...OPENCODE_WITHOUT_EXTERNAL_CONTEXT } }
|
|
386
|
+
);
|
|
387
|
+
} finally {
|
|
388
|
+
await rm(dir, { recursive: true, force: true });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
369
392
|
/**
|
|
370
393
|
* Build the argv for `cursor-agent -p …` from inference options. Pure.
|
|
371
394
|
*
|
|
@@ -513,7 +536,8 @@ async function singleCliAttempt(
|
|
|
513
536
|
args: string[],
|
|
514
537
|
stdinInput: string,
|
|
515
538
|
env: NodeJS.ProcessEnv,
|
|
516
|
-
timeout: number
|
|
539
|
+
timeout: number,
|
|
540
|
+
cwd?: string
|
|
517
541
|
): Promise<RawSpawnResult> {
|
|
518
542
|
return new Promise<RawSpawnResult>((resolve) => {
|
|
519
543
|
let stdout = "";
|
|
@@ -530,13 +554,14 @@ async function singleCliAttempt(
|
|
|
530
554
|
try {
|
|
531
555
|
proc = Bun.spawn([binary, ...args], {
|
|
532
556
|
env,
|
|
557
|
+
cwd,
|
|
533
558
|
stdin: "pipe",
|
|
534
559
|
stdout: "pipe",
|
|
535
560
|
stderr: "pipe",
|
|
536
561
|
windowsHide: true,
|
|
537
562
|
});
|
|
538
563
|
} catch (err) {
|
|
539
|
-
|
|
564
|
+
logError("inference:spawn", err);
|
|
540
565
|
finish({ code: null, stdout: "", stderr: "", timedOut: false });
|
|
541
566
|
return;
|
|
542
567
|
}
|
|
@@ -563,7 +588,7 @@ async function singleCliAttempt(
|
|
|
563
588
|
if (stdinInput) stdinWriter.write(stdinInput);
|
|
564
589
|
stdinWriter.end();
|
|
565
590
|
} catch (err) {
|
|
566
|
-
|
|
591
|
+
logError("inference:stdin", err);
|
|
567
592
|
}
|
|
568
593
|
}
|
|
569
594
|
|
|
@@ -593,6 +618,11 @@ async function singleCliAttempt(
|
|
|
593
618
|
});
|
|
594
619
|
}
|
|
595
620
|
|
|
621
|
+
interface SpawnPlace {
|
|
622
|
+
cwd?: string;
|
|
623
|
+
env?: Record<string, string>;
|
|
624
|
+
}
|
|
625
|
+
|
|
596
626
|
/**
|
|
597
627
|
* Generic CLI dispatcher: spawn `binary args`, write stdinInput to stdin (may be
|
|
598
628
|
* empty for argv-only CLIs like codex), capture stdout, retry once on empty-abort.
|
|
@@ -603,10 +633,11 @@ async function inferenceViaCliSpawn(
|
|
|
603
633
|
args: string[],
|
|
604
634
|
stdinInput: string,
|
|
605
635
|
opts: InferenceOptions,
|
|
606
|
-
extractText?: (rawStdout: string) => string
|
|
636
|
+
extractText?: (rawStdout: string) => string,
|
|
637
|
+
place: SpawnPlace = {}
|
|
607
638
|
): Promise<InferenceResult> {
|
|
608
639
|
const timeout = opts.timeout ?? 15000;
|
|
609
|
-
const env = buildSpawnGuardEnv(process.env);
|
|
640
|
+
const env = { ...buildSpawnGuardEnv(process.env), ...place.env };
|
|
610
641
|
const started = Date.now();
|
|
611
642
|
const caller = opts.caller ?? "anonymous";
|
|
612
643
|
const session = opts.sessionId ?? "-";
|
|
@@ -616,7 +647,7 @@ async function inferenceViaCliSpawn(
|
|
|
616
647
|
const binaryName = basename(binary).replace(/\.(cmd|bat|exe|com)$/i, "");
|
|
617
648
|
|
|
618
649
|
// Attempt 1
|
|
619
|
-
let attempt = await singleCliAttempt(binary, args, stdinInput, env, timeout);
|
|
650
|
+
let attempt = await singleCliAttempt(binary, args, stdinInput, env, timeout, place.cwd);
|
|
620
651
|
|
|
621
652
|
// Universal retry on empty-output exit≠0 (correlates strongly with burst-
|
|
622
653
|
// concurrency races — the binary silently aborts without writing to either
|
|
@@ -633,7 +664,7 @@ async function inferenceViaCliSpawn(
|
|
|
633
664
|
`${tag} retry: empty-abort binary=${binaryName} exit=${attempt.code} after ${Date.now() - started}ms, jitter=${jitterMs}ms`
|
|
634
665
|
);
|
|
635
666
|
await new Promise((r) => setTimeout(r, jitterMs));
|
|
636
|
-
attempt = await singleCliAttempt(binary, args, stdinInput, env, timeout);
|
|
667
|
+
attempt = await singleCliAttempt(binary, args, stdinInput, env, timeout, place.cwd);
|
|
637
668
|
}
|
|
638
669
|
|
|
639
670
|
const elapsedMs = Date.now() - started;
|
|
@@ -646,14 +677,11 @@ async function inferenceViaCliSpawn(
|
|
|
646
677
|
};
|
|
647
678
|
|
|
648
679
|
if (attempt.timedOut) {
|
|
649
|
-
|
|
650
|
-
"inference:spawn",
|
|
651
|
-
`${tag} timeout binary=${binaryName} after ${timeout}ms`
|
|
652
|
-
);
|
|
680
|
+
logError("inference:spawn", `${tag} timeout binary=${binaryName} after ${timeout}ms`);
|
|
653
681
|
return finish({ success: false });
|
|
654
682
|
}
|
|
655
683
|
if (attempt.code !== 0) {
|
|
656
|
-
|
|
684
|
+
logError(
|
|
657
685
|
"inference:spawn",
|
|
658
686
|
`${tag} exited=${attempt.code} binary=${binaryName} argv=${JSON.stringify(args)} stderr(${attempt.stderr.length})=${attempt.stderr.slice(0, 300)} stdout(${attempt.stdout.length})=${attempt.stdout.slice(0, 300)}`
|
|
659
687
|
);
|
|
@@ -665,7 +693,7 @@ async function inferenceViaCliSpawn(
|
|
|
665
693
|
if (!text) {
|
|
666
694
|
// Extraction returned empty — the binary succeeded but our extractor found
|
|
667
695
|
// no usable text. Log the raw stdout so we can see what was actually emitted.
|
|
668
|
-
|
|
696
|
+
logError(
|
|
669
697
|
"inference:spawn",
|
|
670
698
|
`${tag} extract-empty binary=${binaryName} rawStdout(${rawText.length})=${rawText.slice(0, 500)}`
|
|
671
699
|
);
|
|
@@ -679,11 +707,6 @@ async function inferenceViaCliSpawn(
|
|
|
679
707
|
return finish({ success: true, output: text });
|
|
680
708
|
}
|
|
681
709
|
|
|
682
|
-
async function logError(scope: string, err: unknown): Promise<void> {
|
|
683
|
-
const { logError: log } = await import("./log");
|
|
684
|
-
log(scope, err);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
710
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
688
711
|
// Anthropic API path — used when no claude binary is available, or when the
|
|
689
712
|
// active agent is not claude. Preserves the original PAL inference behavior.
|
|
@@ -733,7 +756,7 @@ async function inferenceViaApi(opts: InferenceOptions): Promise<InferenceResult>
|
|
|
733
756
|
|
|
734
757
|
if (!response.ok) {
|
|
735
758
|
const errBody = await response.text().catch(() => "");
|
|
736
|
-
|
|
759
|
+
logError("inference", `HTTP ${response.status}: ${errBody.slice(0, 200)}`);
|
|
737
760
|
return { success: false };
|
|
738
761
|
}
|
|
739
762
|
|
|
@@ -752,7 +775,7 @@ async function inferenceViaApi(opts: InferenceOptions): Promise<InferenceResult>
|
|
|
752
775
|
|
|
753
776
|
return { success: true, output: text, usage };
|
|
754
777
|
} catch (err) {
|
|
755
|
-
|
|
778
|
+
logError("inference", err);
|
|
756
779
|
return { success: false };
|
|
757
780
|
}
|
|
758
781
|
}
|
|
@@ -813,10 +836,7 @@ async function inferenceViaOpenAiApi(opts: InferenceOptions): Promise<InferenceR
|
|
|
813
836
|
|
|
814
837
|
if (!response.ok) {
|
|
815
838
|
const errBody = await response.text().catch(() => "");
|
|
816
|
-
|
|
817
|
-
"inference:openai",
|
|
818
|
-
`HTTP ${response.status}: ${errBody.slice(0, 200)}`
|
|
819
|
-
);
|
|
839
|
+
logError("inference:openai", `HTTP ${response.status}: ${errBody.slice(0, 200)}`);
|
|
820
840
|
return { success: false };
|
|
821
841
|
}
|
|
822
842
|
|
|
@@ -840,7 +860,7 @@ async function inferenceViaOpenAiApi(opts: InferenceOptions): Promise<InferenceR
|
|
|
840
860
|
|
|
841
861
|
return { success: true, output: text, usage };
|
|
842
862
|
} catch (err) {
|
|
843
|
-
|
|
863
|
+
logError("inference:openai", err);
|
|
844
864
|
return { success: false };
|
|
845
865
|
}
|
|
846
866
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { platform } from "./paths";
|
|
4
|
+
|
|
5
|
+
const GLOBAL_CONFIG_FILES = ["config.json", "opencode.json", "opencode.jsonc"];
|
|
6
|
+
|
|
7
|
+
type OpencodeConfig = Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
function readConfig(name: string): OpencodeConfig | null {
|
|
10
|
+
try {
|
|
11
|
+
const config = Bun.JSONC.parse(
|
|
12
|
+
readFileSync(resolve(platform.opencodeDir(), name), "utf-8")
|
|
13
|
+
);
|
|
14
|
+
return config && typeof config === "object" ? (config as OpencodeConfig) : null;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function pinnedModel(config: OpencodeConfig | null): string | null {
|
|
21
|
+
return typeof config?.model === "string" && config.model ? config.model : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Without a pinned model, `opencode run` falls back to whatever was last picked in the TUI. */
|
|
25
|
+
export function opencodeBackgroundModel(): string | null {
|
|
26
|
+
for (const name of GLOBAL_CONFIG_FILES) {
|
|
27
|
+
const model = pinnedModel(readConfig(name));
|
|
28
|
+
if (model) return model;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function withoutInstructions(config: OpencodeConfig): OpencodeConfig {
|
|
34
|
+
const { instructions: _, ...rest } = config;
|
|
35
|
+
return rest;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* opencode loads the global AGENTS.md and every `instructions` file into each run,
|
|
40
|
+
* so PAL's identity and output format would reach background inference too.
|
|
41
|
+
*/
|
|
42
|
+
export function writeInstructionFreeConfig(configHome: string): void {
|
|
43
|
+
const dir = resolve(configHome, "opencode");
|
|
44
|
+
mkdirSync(dir, { recursive: true });
|
|
45
|
+
for (const name of GLOBAL_CONFIG_FILES) {
|
|
46
|
+
const config = readConfig(name);
|
|
47
|
+
if (config)
|
|
48
|
+
writeFileSync(resolve(dir, name), JSON.stringify(withoutInstructions(config)));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -25,6 +25,10 @@ async function lib<T>(mod: string): Promise<T> {
|
|
|
25
25
|
|
|
26
26
|
type TranscriptMessage = { role: string; content: string };
|
|
27
27
|
|
|
28
|
+
function isSessionEvent(type: string): boolean {
|
|
29
|
+
return type.startsWith("session.");
|
|
30
|
+
}
|
|
31
|
+
|
|
28
32
|
const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
29
33
|
// Pre-load shared modules
|
|
30
34
|
const { buildSystemReminder } =
|
|
@@ -125,7 +129,7 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
|
125
129
|
// --- Session events: start and stop handling ---
|
|
126
130
|
event: async ({ event }) => {
|
|
127
131
|
if (isPalSpawnedInference()) return;
|
|
128
|
-
logDebug("opencode:event", `Event: ${event.type}`);
|
|
132
|
+
if (isSessionEvent(event.type)) logDebug("opencode:event", `Event: ${event.type}`);
|
|
129
133
|
|
|
130
134
|
if (event.type === "server.instance.disposed") {
|
|
131
135
|
const { autoUpdateOnClose } =
|
|
@@ -139,7 +143,7 @@ const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
|
139
143
|
regenerateIfNeeded();
|
|
140
144
|
}
|
|
141
145
|
|
|
142
|
-
if (event.type === "session.idle"
|
|
146
|
+
if (event.type === "session.idle") {
|
|
143
147
|
logDebug("opencode:event", "Running stop handlers...");
|
|
144
148
|
try {
|
|
145
149
|
const sessionID = (event as { properties?: { sessionID?: string } })?.properties
|