portable-agent-layer 0.91.8 → 0.93.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/assets/schema/pal-settings.schema.json +8 -0
- package/assets/templates/pal-settings.json +2 -0
- package/package.json +4 -3
- package/src/cli/infer.ts +142 -0
- package/src/cli/knowledge.ts +97 -0
- package/src/cli/tree.ts +2 -0
- package/src/hooks/handlers/entity-extraction.ts +85 -0
- package/src/hooks/handlers/inject-retrieval.ts +4 -0
- package/src/hooks/lib/daily-nudge.ts +29 -6
- package/src/hooks/lib/entity-cards.ts +112 -0
- package/src/hooks/lib/entity-extraction.ts +334 -0
- package/src/hooks/lib/entity-names.ts +242 -0
- package/src/hooks/lib/entity-review.ts +88 -0
- package/src/hooks/lib/inference.ts +38 -4
- package/src/hooks/lib/models.ts +2 -1
- package/src/hooks/lib/paths.ts +6 -2
- package/src/hooks/lib/stop.ts +19 -0
- package/src/hooks/lib/token-usage.ts +1 -0
- package/src/tools/knowledge/ingest.ts +25 -8
|
@@ -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") {
|
|
@@ -552,8 +580,10 @@ async function inferenceViaAntigravitySpawn(
|
|
|
552
580
|
* --disable-builtin-mcps → no MCP servers in the child (extra safety)
|
|
553
581
|
* --no-auto-update → prevent CLI self-update from delaying the spawn
|
|
554
582
|
* --no-color → clean stdout for capture
|
|
555
|
-
* --
|
|
556
|
-
*
|
|
583
|
+
* --available-tools=none → Copilot has no "no tools" flag; an allowlist
|
|
584
|
+
* naming an unknown tool hides every tool
|
|
585
|
+
* --deny-tool=… → backstop: deny rules win over any allow rule
|
|
586
|
+
* --silent → keeps the tool-filter notices out of stdout
|
|
557
587
|
*
|
|
558
588
|
* Copilot has no --system-prompt flag, so system + user + JSON-schema are
|
|
559
589
|
* concatenated into one prompt delivered on stdin. `-p/--prompt` is deliberately
|
|
@@ -567,7 +597,11 @@ export function buildCopilotArgs(_opts: InferenceOptions): string[] {
|
|
|
567
597
|
"--disable-builtin-mcps",
|
|
568
598
|
"--no-auto-update",
|
|
569
599
|
"--no-color",
|
|
570
|
-
"--
|
|
600
|
+
"--silent",
|
|
601
|
+
"--available-tools=none",
|
|
602
|
+
"--deny-tool=shell",
|
|
603
|
+
"--deny-tool=write",
|
|
604
|
+
"--deny-tool=url",
|
|
571
605
|
];
|
|
572
606
|
}
|
|
573
607
|
|
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
|
|
package/src/hooks/lib/stop.ts
CHANGED
|
@@ -168,6 +168,7 @@ export async function runStopHandlers(
|
|
|
168
168
|
// any in-hook budget. These spawn detached bun subprocesses that run the
|
|
169
169
|
// inference and write results to disk; they don't block this hook.
|
|
170
170
|
await detachSessionIntelligence(transcript, options.sessionId);
|
|
171
|
+
await detachEntityExtraction(transcript, options.sessionId);
|
|
171
172
|
await detachFailurePrinciple(transcript);
|
|
172
173
|
detachAgenda(options.sessionId);
|
|
173
174
|
// Failure auto-graduation is intentionally NOT wired here: every pattern it
|
|
@@ -325,6 +326,24 @@ async function detachSessionIntelligence(
|
|
|
325
326
|
}
|
|
326
327
|
}
|
|
327
328
|
|
|
329
|
+
async function detachEntityExtraction(
|
|
330
|
+
transcript: string,
|
|
331
|
+
sessionId?: string
|
|
332
|
+
): Promise<void> {
|
|
333
|
+
if (!sessionId) return;
|
|
334
|
+
try {
|
|
335
|
+
const transcriptPath = await writeTranscriptTmp(transcript);
|
|
336
|
+
const scriptPath = resolve(assets.hooks(), "handlers", "entity-extraction.ts");
|
|
337
|
+
spawnDetachedInference(
|
|
338
|
+
scriptPath,
|
|
339
|
+
["--run", sessionId, transcriptPath],
|
|
340
|
+
"entity-extraction"
|
|
341
|
+
);
|
|
342
|
+
} catch (err) {
|
|
343
|
+
logError("detachEntityExtraction", err);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
328
347
|
/**
|
|
329
348
|
* If a pending-failure exists, rename it to a unique path (race-free claim),
|
|
330
349
|
* write the transcript to tmp, spawn the failure-principle handler detached.
|
|
@@ -29,7 +29,14 @@ import {
|
|
|
29
29
|
|
|
30
30
|
// --- Public input shape -----------------------------------------------------
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
interface Addressing {
|
|
33
|
+
aliases?: string[] | null;
|
|
34
|
+
relation?: string | null;
|
|
35
|
+
/** The stored file to update, when its slug is not derived from the name or domain. */
|
|
36
|
+
slug?: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PersonInput extends Addressing {
|
|
33
40
|
name: string;
|
|
34
41
|
role?: string | null;
|
|
35
42
|
title?: string | null;
|
|
@@ -40,7 +47,7 @@ export interface PersonInput {
|
|
|
40
47
|
[extra: string]: unknown;
|
|
41
48
|
}
|
|
42
49
|
|
|
43
|
-
export interface CompanyInput {
|
|
50
|
+
export interface CompanyInput extends Addressing {
|
|
44
51
|
name: string;
|
|
45
52
|
domain?: string | null;
|
|
46
53
|
industry?: string | null;
|
|
@@ -201,7 +208,17 @@ function newPersonEntity(input: PersonInput, slug: string): Entity {
|
|
|
201
208
|
if (Object.keys(socials).length > 0) {
|
|
202
209
|
fm.socials = Object.entries(socials).map(([k, v]) => `${k}:${v}`);
|
|
203
210
|
}
|
|
204
|
-
return { domain: "People", slug, frontmatter: fm, body: "" };
|
|
211
|
+
return { domain: "People", slug, frontmatter: withAddressing(fm, input), body: "" };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** How the user refers to an entity and stands towards it — both only ever grow. */
|
|
215
|
+
function withAddressing(fm: EntityFrontmatter, input: Addressing): EntityFrontmatter {
|
|
216
|
+
const prior = Array.isArray(fm.aliases) ? (fm.aliases as string[]) : undefined;
|
|
217
|
+
const aliases = mergeStringArray(prior, input.aliases ?? undefined);
|
|
218
|
+
if (aliases.length > 0) fm.aliases = aliases;
|
|
219
|
+
const relation = mergeScalar(fm.relation, input.relation);
|
|
220
|
+
if (relation) fm.relation = relation;
|
|
221
|
+
return fm;
|
|
205
222
|
}
|
|
206
223
|
|
|
207
224
|
function newCompanyEntity(input: CompanyInput, slug: string): Entity {
|
|
@@ -220,7 +237,7 @@ function newCompanyEntity(input: CompanyInput, slug: string): Entity {
|
|
|
220
237
|
if (input.industry) fm.industry = input.industry;
|
|
221
238
|
if (input.mentioned_as) fm.mentioned_as = input.mentioned_as;
|
|
222
239
|
if (input.sentiment) fm.sentiment = input.sentiment;
|
|
223
|
-
return { domain: "Companies", slug, frontmatter: fm, body: "" };
|
|
240
|
+
return { domain: "Companies", slug, frontmatter: withAddressing(fm, input), body: "" };
|
|
224
241
|
}
|
|
225
242
|
|
|
226
243
|
function mergePerson(prior: Entity, input: PersonInput): Entity {
|
|
@@ -248,7 +265,7 @@ function mergePerson(prior: Entity, input: PersonInput): Entity {
|
|
|
248
265
|
if (Object.keys(socials).length > 0) {
|
|
249
266
|
fm.socials = Object.entries(socials).map(([k, v]) => `${k}:${v}`);
|
|
250
267
|
}
|
|
251
|
-
return { ...prior, frontmatter: fm };
|
|
268
|
+
return { ...prior, frontmatter: withAddressing(fm, input) };
|
|
252
269
|
}
|
|
253
270
|
|
|
254
271
|
function mergeCompany(prior: Entity, input: CompanyInput): Entity {
|
|
@@ -260,7 +277,7 @@ function mergeCompany(prior: Entity, input: CompanyInput): Entity {
|
|
|
260
277
|
if (input.industry) {
|
|
261
278
|
fm.tags = mergeStringArray(fm.tags, industryToTopicTags(input.industry));
|
|
262
279
|
}
|
|
263
|
-
return { ...prior, frontmatter: fm };
|
|
280
|
+
return { ...prior, frontmatter: withAddressing(fm, input) };
|
|
264
281
|
}
|
|
265
282
|
|
|
266
283
|
function upsertPerson(
|
|
@@ -268,7 +285,7 @@ function upsertPerson(
|
|
|
268
285
|
sourceId: string,
|
|
269
286
|
rootDir?: string
|
|
270
287
|
): UpsertResult {
|
|
271
|
-
const slug = slugify(input.name);
|
|
288
|
+
const slug = input.slug || slugify(input.name);
|
|
272
289
|
if (!slug) throw new Error(`ingest: cannot slugify person name "${input.name}"`);
|
|
273
290
|
const prior = load("People", slug, rootDir);
|
|
274
291
|
const created = prior === null;
|
|
@@ -294,7 +311,7 @@ function upsertCompany(
|
|
|
294
311
|
rootDir?: string
|
|
295
312
|
): UpsertResult {
|
|
296
313
|
const baseKey = input.domain?.trim() ? input.domain : input.name;
|
|
297
|
-
const slug = slugify(baseKey);
|
|
314
|
+
const slug = input.slug || slugify(baseKey);
|
|
298
315
|
if (!slug) throw new Error(`ingest: cannot slugify company "${input.name}"`);
|
|
299
316
|
const prior = load("Companies", slug, rootDir);
|
|
300
317
|
const created = prior === null;
|