portable-agent-layer 0.91.7 → 0.92.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/README.md +1 -0
- package/package.json +5 -4
- package/src/cli/infer.ts +142 -0
- package/src/cli/tree.ts +2 -0
- package/src/hooks/lib/inference.ts +29 -1
- package/src/hooks/lib/models.ts +2 -1
- package/src/hooks/lib/paths.ts +6 -2
package/README.md
CHANGED
|
@@ -85,6 +85,7 @@ pal cli status # check your setup
|
|
|
85
85
|
| `pal cli migrate` | Run pending data migrations (non-destructive) |
|
|
86
86
|
| `pal cli analyze [--actionable]` | Learning analysis: rating trends, failure patterns, graduation candidates |
|
|
87
87
|
| `pal cli usage` | Summarize token usage and estimated cost |
|
|
88
|
+
| `pal cli infer` | One model call for scripts and bots, on the active agent's route: prompt on stdin, reply on stdout. `--tier small\|medium`, `--system <file>`, `--schema <file>` for a JSON reply checked against it, `--timeout <seconds>`. The call loads no PAL hooks or context; exits 1 with the reason when no route answers |
|
|
88
89
|
| `pal cli actor [label <name>]` | Show or rename the actor — who caused a record. Travels with an export, so a shared memory can tell two people apart |
|
|
89
90
|
| `pal cli machine [label <name>]` | Show or rename this install — where a record was written. Never leaves the machine |
|
|
90
91
|
| `pal cli telos` | Which TELOS topics are answered, in interview order, and which one comes next |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portable-agent-layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.92.0",
|
|
4
4
|
"description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"type-check": "tsc --noEmit",
|
|
40
|
-
"test": "bun test --randomize",
|
|
40
|
+
"test": "bun test --randomize --isolate",
|
|
41
41
|
"test:mutate": "stryker run",
|
|
42
42
|
"test:mutate:diff": "bun .agents/scripts/mutate-diff.ts",
|
|
43
43
|
"format": "biome format",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"check": "biome check",
|
|
48
48
|
"check-write": "bun .agents/scripts/check-write.ts",
|
|
49
49
|
"knip": "knip-bun",
|
|
50
|
-
"klint": "klint",
|
|
50
|
+
"klint": "klint && klint --config test",
|
|
51
51
|
"jscpd": "jscpd --noTips",
|
|
52
52
|
"madge": "madge --circular --extensions ts --ts-config tsconfig.json src",
|
|
53
53
|
"lf": "bun .agents/scripts/check-lf.ts",
|
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
"marked": "18.0.4",
|
|
108
108
|
"pdf-lib": "1.17.1",
|
|
109
109
|
"playwright": "^1.60.0",
|
|
110
|
-
"unpdf": "^0.12.0"
|
|
110
|
+
"unpdf": "^0.12.0",
|
|
111
|
+
"zod": "^4.4.3"
|
|
111
112
|
}
|
|
112
113
|
}
|
package/src/cli/infer.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pal cli infer — one inference call through PAL's own route, for programs
|
|
3
|
+
* outside an agent session. The route, its model for each tier, and the
|
|
4
|
+
* isolation from PAL's hooks are the same ones PAL's background calls use.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { inference } from "../hooks/lib/inference";
|
|
10
|
+
import { INFERENCE_TIERS, type InferenceTier } from "../hooks/lib/models";
|
|
11
|
+
import { leaf } from "../tools/lib/command";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_TIMEOUT_SECONDS = 60;
|
|
14
|
+
|
|
15
|
+
interface InferValues {
|
|
16
|
+
tier?: string;
|
|
17
|
+
system?: string;
|
|
18
|
+
schema?: string;
|
|
19
|
+
timeout?: string;
|
|
20
|
+
caller?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Infer = typeof inference;
|
|
24
|
+
|
|
25
|
+
class InferInputError extends Error {}
|
|
26
|
+
|
|
27
|
+
function tierOf(name = "small"): InferenceTier {
|
|
28
|
+
const tier = INFERENCE_TIERS.find((known) => known === name);
|
|
29
|
+
if (!tier) {
|
|
30
|
+
throw new InferInputError(
|
|
31
|
+
`Unknown tier "${name}". Use ${INFERENCE_TIERS.join(", ")}.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return tier;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function timeoutMs(seconds = String(DEFAULT_TIMEOUT_SECONDS)): number {
|
|
38
|
+
const value = Number(seconds);
|
|
39
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
40
|
+
throw new InferInputError(`--timeout takes a number of seconds, not "${seconds}".`);
|
|
41
|
+
}
|
|
42
|
+
return value * 1000;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function promptOf(stdin: string): string {
|
|
46
|
+
const prompt = stdin.trim();
|
|
47
|
+
if (!prompt) throw new InferInputError("No prompt: pipe it in on stdin.");
|
|
48
|
+
return prompt;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readSchema(path: string): Record<string, unknown> {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
54
|
+
} catch {
|
|
55
|
+
throw new InferInputError(`--schema ${path} is not a readable JSON file.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function assertCheckable(path: string, schema: Record<string, unknown>): void {
|
|
60
|
+
try {
|
|
61
|
+
z.fromJSONSchema(schema);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
throw new InferInputError(
|
|
64
|
+
`--schema ${path} cannot be checked: ${err instanceof Error ? err.message : String(err)}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function schemaFrom(path: string | undefined): Record<string, unknown> | undefined {
|
|
70
|
+
if (!path) return undefined;
|
|
71
|
+
const schema = readSchema(path);
|
|
72
|
+
assertCheckable(path, schema);
|
|
73
|
+
return schema;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function requestFrom(values: InferValues, stdin: string): Parameters<Infer>[0] {
|
|
77
|
+
return {
|
|
78
|
+
user: promptOf(stdin),
|
|
79
|
+
system: values.system ? readFileSync(values.system, "utf-8") : undefined,
|
|
80
|
+
jsonSchema: schemaFrom(values.schema),
|
|
81
|
+
tier: tierOf(values.tier),
|
|
82
|
+
timeout: timeoutMs(values.timeout),
|
|
83
|
+
caller: values.caller ?? "cli-infer",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function failureReason(result: Awaited<ReturnType<Infer>>, wantedJson: boolean): string {
|
|
88
|
+
if (result.error) return result.error;
|
|
89
|
+
if (wantedJson && result.output) return "the reply was not JSON";
|
|
90
|
+
return "no inference route answered (run `pal cli doctor`)";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function inferFromCli(
|
|
94
|
+
values: InferValues,
|
|
95
|
+
stdin: string,
|
|
96
|
+
infer: Infer = inference
|
|
97
|
+
): Promise<number> {
|
|
98
|
+
let request: Parameters<Infer>[0];
|
|
99
|
+
try {
|
|
100
|
+
request = requestFrom(values, stdin);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
const result = await infer(request);
|
|
106
|
+
if (!result.success || !result.output) {
|
|
107
|
+
console.error(
|
|
108
|
+
`Inference failed: ${failureReason(result, Boolean(request.jsonSchema))}`
|
|
109
|
+
);
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
console.log(result.output);
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const inferCommand = leaf({
|
|
117
|
+
summary: "Run one inference call on the active agent's model; prompt on stdin",
|
|
118
|
+
options: {
|
|
119
|
+
tier: {
|
|
120
|
+
type: "string",
|
|
121
|
+
value: "<tier>",
|
|
122
|
+
description: `Model size: ${INFERENCE_TIERS.join(" or ")} (default small)`,
|
|
123
|
+
},
|
|
124
|
+
system: { type: "string", value: "<file>", description: "System prompt file" },
|
|
125
|
+
schema: {
|
|
126
|
+
type: "string",
|
|
127
|
+
value: "<file>",
|
|
128
|
+
description: "JSON schema file; a reply that doesn't match it fails",
|
|
129
|
+
},
|
|
130
|
+
timeout: {
|
|
131
|
+
type: "string",
|
|
132
|
+
value: "<seconds>",
|
|
133
|
+
description: `Give up after this long (default ${DEFAULT_TIMEOUT_SECONDS})`,
|
|
134
|
+
},
|
|
135
|
+
caller: {
|
|
136
|
+
type: "string",
|
|
137
|
+
value: "<label>",
|
|
138
|
+
description: "Label for PAL's debug log",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
run: async ({ values }) => inferFromCli(values, await Bun.stdin.text()),
|
|
142
|
+
});
|
package/src/cli/tree.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "../tools/lib/command";
|
|
16
16
|
import { builtinTools, builtinToolVerbs } from "./builtin-tools";
|
|
17
17
|
import { identityCommand } from "./identity";
|
|
18
|
+
import { inferCommand } from "./infer";
|
|
18
19
|
import { knowledgeCommand } from "./knowledge";
|
|
19
20
|
import { ledgerCommand } from "./ledger-command";
|
|
20
21
|
import { runMigrate } from "./migrate";
|
|
@@ -150,6 +151,7 @@ export function cliTree(admin: AdminHandlers): Group {
|
|
|
150
151
|
commands: {
|
|
151
152
|
...adminCommands(admin),
|
|
152
153
|
usage: usageCommand,
|
|
154
|
+
infer: inferCommand,
|
|
153
155
|
actor: identityCommand("actor"),
|
|
154
156
|
machine: identityCommand("machine"),
|
|
155
157
|
telos: telosCommand,
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
24
24
|
import { tmpdir } from "node:os";
|
|
25
25
|
import { basename, join } from "node:path";
|
|
26
|
+
import { z } from "zod";
|
|
26
27
|
import {
|
|
27
28
|
getActiveAgent,
|
|
28
29
|
isAntigravity,
|
|
@@ -151,7 +152,7 @@ interface InferenceOptions {
|
|
|
151
152
|
tier?: InferenceTier;
|
|
152
153
|
maxTokens?: number;
|
|
153
154
|
timeout?: number;
|
|
154
|
-
/** JSON schema
|
|
155
|
+
/** JSON schema the reply must match; a reply that doesn't fails with the reason */
|
|
155
156
|
jsonSchema?: Record<string, unknown>;
|
|
156
157
|
/** Opaque label identifying the calling handler — appears in debug logs as caller=X */
|
|
157
158
|
caller?: string;
|
|
@@ -167,7 +168,34 @@ interface InferenceResult {
|
|
|
167
168
|
usage?: { inputTokens: number; outputTokens: number };
|
|
168
169
|
}
|
|
169
170
|
|
|
171
|
+
function schemaProblem(
|
|
172
|
+
output: string,
|
|
173
|
+
schema: Record<string, unknown>
|
|
174
|
+
): string | undefined {
|
|
175
|
+
const checked = z.fromJSONSchema(schema).safeParse(parseJsonFromOutput(output));
|
|
176
|
+
if (checked.success) return undefined;
|
|
177
|
+
return checked.error.issues
|
|
178
|
+
.map((issue) => `${issue.path.join(".") || "reply"}: ${issue.message}`)
|
|
179
|
+
.join("; ");
|
|
180
|
+
}
|
|
181
|
+
|
|
170
182
|
export async function inference(opts: InferenceOptions): Promise<InferenceResult> {
|
|
183
|
+
const result = await routeInference(opts);
|
|
184
|
+
if (!opts.jsonSchema || !result.success || !result.output) return result;
|
|
185
|
+
const problem = schemaProblem(result.output, opts.jsonSchema);
|
|
186
|
+
if (!problem) return result;
|
|
187
|
+
logError(
|
|
188
|
+
"inference",
|
|
189
|
+
`caller=${opts.caller ?? "anonymous"} schema mismatch: ${problem}`
|
|
190
|
+
);
|
|
191
|
+
return {
|
|
192
|
+
...result,
|
|
193
|
+
success: false,
|
|
194
|
+
error: `the reply does not match the schema: ${problem}`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function routeInference(opts: InferenceOptions): Promise<InferenceResult> {
|
|
171
199
|
// Hard kill-switch — set by the test suite to guarantee no real inference
|
|
172
200
|
// ever fires from tests (no spawn, no API call). Production code never sets it.
|
|
173
201
|
if (process.env.PAL_INFERENCE_DISABLED === "1") {
|
package/src/hooks/lib/models.ts
CHANGED
|
@@ -9,7 +9,8 @@ export const HAIKU_5_5_MODEL = "claude-haiku-5-5";
|
|
|
9
9
|
export const SONNET_MODEL = "claude-sonnet-5";
|
|
10
10
|
export const FABLE_MODEL = "claude-fable-5";
|
|
11
11
|
|
|
12
|
-
export
|
|
12
|
+
export const INFERENCE_TIERS = ["small", "medium"] as const;
|
|
13
|
+
export type InferenceTier = (typeof INFERENCE_TIERS)[number];
|
|
13
14
|
|
|
14
15
|
export type FixedModelRoute =
|
|
15
16
|
| "claude-spawn"
|
package/src/hooks/lib/paths.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
-
import { homedir
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
3
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -62,8 +62,12 @@ export function palPkg(): string {
|
|
|
62
62
|
* Always resolves to ~/.pal/ regardless of where the package lives.
|
|
63
63
|
* Power users who want memory/telos versioned in a repo can override via PAL_HOME.
|
|
64
64
|
*/
|
|
65
|
+
function sandboxPalHome(): string {
|
|
66
|
+
return resolve(import.meta.dir, "..", "..", "..", ".test", "pal-home");
|
|
67
|
+
}
|
|
68
|
+
|
|
65
69
|
function defaultPalHome(): string {
|
|
66
|
-
if (process.env.PAL_TEST_SANDBOX) return
|
|
70
|
+
if (process.env.PAL_TEST_SANDBOX) return sandboxPalHome();
|
|
67
71
|
return resolve(homedir(), ".pal");
|
|
68
72
|
}
|
|
69
73
|
|