@tangle-network/agent-profile-materialize 0.12.0 → 0.13.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/dist/index.d.ts +87 -12
- package/dist/index.js +277 -105
- package/dist/prime-fork.d.ts +192 -0
- package/dist/prime-fork.js +303 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* cli-bridge and agent-dev-container — this is the single source those re-export.
|
|
23
23
|
*/
|
|
24
24
|
export { modelIdsMatch, qualifyModelId } from "./model-id.js";
|
|
25
|
+
export { matchPrimeKernelDeadMarker, PRIME_AGENT_DIR_ENV_VAR, PRIME_AGENT_DIR_ENV_VARS, PRIME_FORK_INSTALL, PRIME_FORK_PIN, PRIME_HARNESS_STATE_ENV_DENYLIST, PRIME_KERNEL_DEAD_MARKERS, PRIME_KERNEL_ENV_PASSTHROUGH, PRIME_MCP_UNSUPPORTED_REASON, PRIME_THINKING_LEVELS, PrimeHarnessStateError, PrimeProfileError, type PrimeProfileErrorKind, type PrimeThinkingLevel, parsePrimeModelsJson, primeAgentDirEnv, primeApiKeyEnvNames, primeThinkingLevel, stripPrimeJsonComments, } from "./prime-fork.js";
|
|
25
26
|
import type { AgentCandidateConfigValue, AgentCandidateProfile, AgentCandidateProfileActivation, AgentCandidateProfileApplication, AgentCandidateProfilePlanEvidence, AgentCandidateProfilePlanMaterial, AgentProfile, AgentProfileConfigValue, AgentProfileMcpServer, AgentProfileSecretProvider, AgentProfileSecretRef, Sha256Digest } from "@tangle-network/agent-interface";
|
|
26
27
|
export declare const UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER: AgentProfileSecretProvider;
|
|
27
28
|
/**
|
|
@@ -503,31 +504,105 @@ export interface PrimeProfileSubagent {
|
|
|
503
504
|
export interface PrimeProfileControls {
|
|
504
505
|
systemPrompt?: string;
|
|
505
506
|
appendSystemPrompt?: string;
|
|
507
|
+
/** Composed caller instructions, lowered to the agent dir's AGENTS.md. */
|
|
508
|
+
instructions?: string;
|
|
506
509
|
skills: PrimeProfileSkill[];
|
|
507
510
|
subagents: PrimeProfileSubagent[];
|
|
508
511
|
}
|
|
512
|
+
/** True when the controls materialize anything at all. */
|
|
513
|
+
export declare function hasPrimeProfileMaterial(controls: PrimeProfileControls): boolean;
|
|
514
|
+
/**
|
|
515
|
+
* True when the controls must WRITE INTO the agent dir itself.
|
|
516
|
+
*
|
|
517
|
+
* Prompt files and skill dirs are named by a flag, so they can live anywhere
|
|
518
|
+
* the agent can read (`--system-prompt` / `--append-system-prompt` resolve a
|
|
519
|
+
* path when it exists, fork resource-loader.ts:41-56; `--skill` takes an
|
|
520
|
+
* arbitrary dir, :437-442). The context file and the continual-harness store
|
|
521
|
+
* are located BY the agent dir and have no flag, so they cannot move.
|
|
522
|
+
*
|
|
523
|
+
* An executor pointed at an agent dir it does not own (an operator's shared
|
|
524
|
+
* dir) uses this to refuse exactly the controls it would have to write there,
|
|
525
|
+
* while still binding the ones it can materialize elsewhere.
|
|
526
|
+
*/
|
|
527
|
+
export declare function primeProfileNeedsAgentDir(controls: PrimeProfileControls): boolean;
|
|
509
528
|
/**
|
|
510
529
|
* Validate and extract the profile controls the prime agent-dir lowering
|
|
511
|
-
* materializes. Throws on any control (or control
|
|
512
|
-
* verified read path for — partial acceptance would
|
|
530
|
+
* materializes. Throws {@link PrimeProfileError} on any control (or control
|
|
531
|
+
* facet) the fork has no verified read path for — partial acceptance would
|
|
532
|
+
* silently drop the rest.
|
|
513
533
|
*/
|
|
514
534
|
export declare function readPrimeProfileControls(profile: AgentProfile | undefined): PrimeProfileControls;
|
|
535
|
+
/**
|
|
536
|
+
* Refuse a run whose selected model disagrees with the profile's own.
|
|
537
|
+
*
|
|
538
|
+
* One authority per run: a profile names the model it was written and scored
|
|
539
|
+
* against, so running it on a different one silently scores a different agent.
|
|
540
|
+
* Both sides go through {@link modelIdsMatch}, so the qualification level a
|
|
541
|
+
* caller happened to use is not a conflict.
|
|
542
|
+
*
|
|
543
|
+
* `provider` is the executor's PROVIDER SELECTION, and is compared only when
|
|
544
|
+
* the executor's provider is part of model identity. An executor whose
|
|
545
|
+
* provider key is merely a namespace inside the agent dir passes it as
|
|
546
|
+
* undefined and gets the model-name comparison alone.
|
|
547
|
+
*/
|
|
548
|
+
export declare function assertPrimeModelAgreement(profile: AgentProfile | undefined, selected: {
|
|
549
|
+
provider?: string;
|
|
550
|
+
model: string;
|
|
551
|
+
}): void;
|
|
515
552
|
/** Deterministic harness-state entry id for a profile subagent name. */
|
|
516
553
|
export declare function primeSubagentEntryId(name: string): string;
|
|
517
554
|
/**
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
555
|
+
* Write one agent-dir file atomically (temp + rename).
|
|
556
|
+
*
|
|
557
|
+
* Every file this lowering writes lands in a directory a LIVE agent may be
|
|
558
|
+
* reading: prime reloads models.json at model-resolution time and re-reads the
|
|
559
|
+
* harness store on every system-prompt build, so a partial write is observable.
|
|
560
|
+
* `mode` is applied to the temp file before the rename, so the final file is
|
|
561
|
+
* never briefly world-readable.
|
|
562
|
+
*/
|
|
563
|
+
export declare function writePrimeAgentFile(path: string, data: string, mode?: number): void;
|
|
564
|
+
export interface PrimeMaterializeOptions {
|
|
565
|
+
/**
|
|
566
|
+
* Where the FLAG-NAMED files go — the prompt files and the skills root.
|
|
567
|
+
* Defaults to `agentDir`. An executor that does not own the agent dir points
|
|
568
|
+
* this at a per-run directory of its own, so binding a profile's prompts and
|
|
569
|
+
* skills writes nothing into a dir it must not modify. The context file and
|
|
570
|
+
* the harness store have no flag and always land in `agentDir`; refuse those
|
|
571
|
+
* with {@link primeProfileNeedsAgentDir} instead.
|
|
572
|
+
*/
|
|
573
|
+
flagFileRoot?: string;
|
|
574
|
+
/**
|
|
575
|
+
* Reports state the executor should see but that does not fail the run
|
|
576
|
+
* (currently: adopting entries a pre-shared executor left behind).
|
|
577
|
+
*/
|
|
578
|
+
warn?: (msg: string, meta?: Record<string, unknown>) => void;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Materialize the validated controls and return the extra spawn args that bind
|
|
582
|
+
* them. Runs before every spawn (same place models.json is materialized), so a
|
|
583
|
+
* continual agent dir is re-pinned to the current profile on each fresh prime
|
|
584
|
+
* process.
|
|
585
|
+
*/
|
|
586
|
+
export declare function materializePrimeProfileControls(agentDir: string, controls: PrimeProfileControls, options?: PrimeMaterializeOptions): string[];
|
|
587
|
+
/**
|
|
588
|
+
* Remove the files a PREVIOUS materialization of this lowering wrote, so a
|
|
589
|
+
* profile that shrank cannot leave a stale instruction file or skill behind for
|
|
590
|
+
* the next run against the same dirs.
|
|
591
|
+
*
|
|
592
|
+
* Only the paths this lowering NAMES are removed. There is deliberately no
|
|
593
|
+
* manifest: a manifest would live where the agent can write it, and reading a
|
|
594
|
+
* delete list out of agent-writable state turns pruning into an arbitrary-path
|
|
595
|
+
* delete.
|
|
522
596
|
*/
|
|
523
|
-
export declare function
|
|
597
|
+
export declare function prunePrimeProfileMaterial(agentDir: string, options?: Pick<PrimeMaterializeOptions, "flagFileRoot">): void;
|
|
524
598
|
/**
|
|
525
599
|
* Upsert profile subagent specs into the fork's global continual-harness
|
|
526
600
|
* store and remove profile-sourced entries the current profile no longer
|
|
527
|
-
* declares. Agent-written entries (`source: "agent"`)
|
|
601
|
+
* declares. Agent-written entries (`source: "agent"`) — and every top-level
|
|
602
|
+
* key this lowering does not manage — are preserved untouched.
|
|
528
603
|
*
|
|
529
|
-
* A
|
|
530
|
-
*
|
|
531
|
-
*
|
|
604
|
+
* A store that exists but no longer parses raises
|
|
605
|
+
* {@link PrimeHarnessStateError}: the file is the agent's own self-modification
|
|
606
|
+
* state, and overwriting it destroys what the run was supposed to observe.
|
|
532
607
|
*/
|
|
533
|
-
export declare function seedPrimeProfileSubagents(agentDir: string, subagents: PrimeProfileSubagent[], warn
|
|
608
|
+
export declare function seedPrimeProfileSubagents(agentDir: string, subagents: PrimeProfileSubagent[], warn?: (msg: string, meta?: Record<string, unknown>) => void): void;
|
package/dist/index.js
CHANGED
|
@@ -22,10 +22,13 @@
|
|
|
22
22
|
* cli-bridge and agent-dev-container — this is the single source those re-export.
|
|
23
23
|
*/
|
|
24
24
|
export { modelIdsMatch, qualifyModelId } from "./model-id.js";
|
|
25
|
+
export { matchPrimeKernelDeadMarker, PRIME_AGENT_DIR_ENV_VAR, PRIME_AGENT_DIR_ENV_VARS, PRIME_FORK_INSTALL, PRIME_FORK_PIN, PRIME_HARNESS_STATE_ENV_DENYLIST, PRIME_KERNEL_DEAD_MARKERS, PRIME_KERNEL_ENV_PASSTHROUGH, PRIME_MCP_UNSUPPORTED_REASON, PRIME_THINKING_LEVELS, PrimeHarnessStateError, PrimeProfileError, parsePrimeModelsJson, primeAgentDirEnv, primeApiKeyEnvNames, primeThinkingLevel, stripPrimeJsonComments, } from "./prime-fork.js";
|
|
25
26
|
import { createHash, randomUUID } from "node:crypto";
|
|
26
|
-
import { closeSync, existsSync, fchmodSync, constants as fsConstants, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
27
|
+
import { closeSync, existsSync, fchmodSync, constants as fsConstants, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
27
28
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep, win32, } from "node:path";
|
|
28
29
|
import { agentCandidateProfileActivationSchema, agentCandidateProfilePlanEvidenceSchema, agentCandidateProfilePlanMaterialSchema, agentCandidateProfileSchema, agentProfileSchema, agentProfileSecretRefSchema, canonicalCandidateBytes, canonicalCandidateDigest, resolveNativeWebTools, } from "@tangle-network/agent-interface";
|
|
30
|
+
import { modelIdsMatch, qualifyModelId } from "./model-id.js";
|
|
31
|
+
import { PRIME_MCP_UNSUPPORTED_REASON, PrimeHarnessStateError, rejectPrimeControl, rejectPrimeDeclaration, } from "./prime-fork.js";
|
|
29
32
|
export const UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER = Object.freeze({
|
|
30
33
|
async get() {
|
|
31
34
|
return undefined;
|
|
@@ -3173,12 +3176,29 @@ function materializeCommand(h, name, body, add, unsupported) {
|
|
|
3173
3176
|
// and the addition appends after it (system-prompt.ts:61,109-110). Values
|
|
3174
3177
|
// are materialized as FILES under the agent dir because the flag value is
|
|
3175
3178
|
// read as a file when the path exists (resource-loader.ts:41-56), which
|
|
3176
|
-
// keeps literal-vs-path resolution deterministic
|
|
3179
|
+
// keeps literal-vs-path resolution deterministic AND removes the argv size
|
|
3180
|
+
// ceiling an inline flag value would run into (Linux MAX_ARG_STRLEN).
|
|
3181
|
+
//
|
|
3182
|
+
// - `prompt.instructions` / `resources.instructions` → `<agentDir>/AGENTS.md`.
|
|
3183
|
+
// The agent dir IS an instruction surface: `loadProjectContextFiles`
|
|
3184
|
+
// (resource-loader.ts:76) reads the agent dir's own AGENTS.md/CLAUDE.md as
|
|
3185
|
+
// the GLOBAL context file (:86 `loadContextFileFromDir(resolvedAgentDir)`,
|
|
3186
|
+
// candidates at :59) BEFORE walking the cwd ancestors (:94-112), and the
|
|
3187
|
+
// result is loaded unless `--no-context-files` (:476). Writing it here
|
|
3188
|
+
// touches no caller workspace content, which is what the agent-dir lowering
|
|
3189
|
+
// exists to guarantee. (`materializeProfile(profile, "prime")` lowers the
|
|
3190
|
+
// same intent to the cwd AGENTS.md for callers that own the workspace
|
|
3191
|
+
// instead of the agent dir.)
|
|
3177
3192
|
//
|
|
3178
3193
|
// - `resources.skills` (inline refs) → `--skill <agentDir>/profile/skills`
|
|
3179
3194
|
// (cli/args.ts:207-209 → main.ts:652 → agent-session-config.ts:67 →
|
|
3180
|
-
// additionalSkillPaths, resource-loader.ts:437-442).
|
|
3181
|
-
//
|
|
3195
|
+
// additionalSkillPaths, resource-loader.ts:437-442). An explicit flag rather
|
|
3196
|
+
// than the agent dir's auto-discovered `skills/` tree (package-manager.ts:884
|
|
3197
|
+
// global base, :2171 skills dir, :2240-2246 discovery), which also loads but
|
|
3198
|
+
// goes through the fork's global-vs-project dedupe (package-manager.ts:880,
|
|
3199
|
+
// "project scope wins over global") — so an unrelated project-scope skill of
|
|
3200
|
+
// the same name silently shadows the profile's. The loader recurses into
|
|
3201
|
+
// `<name>/SKILL.md` subdirs (skills.ts:328-372) and loaded skills are
|
|
3182
3202
|
// rendered into the system prompt (agent-session.ts:4272-4291,
|
|
3183
3203
|
// system-prompt.ts:87-89). The fork silently DROPS a SKILL.md without a
|
|
3184
3204
|
// frontmatter description (skills.ts:416-419), so content is normalized
|
|
@@ -3192,25 +3212,17 @@ function materializeCommand(h, name, body, add, unsupported) {
|
|
|
3192
3212
|
// reaches the same store through the host-set RLM_GLOBAL_HARNESS_STATE_DIR
|
|
3193
3213
|
// (agent-session.ts:8805).
|
|
3194
3214
|
//
|
|
3195
|
-
// Kept REJECTED with the fork evidence
|
|
3196
|
-
//
|
|
3197
|
-
//
|
|
3198
|
-
//
|
|
3199
|
-
// startup, but a user-declared server never becomes model-visible: the host
|
|
3200
|
-
// McpManager only registers OAuth providers and serves mcp.config/refresh
|
|
3201
|
-
// host-requests (core/mcp/mcp-manager.ts:62-124,156-181), skips stdio
|
|
3202
|
-
// servers entirely (mcp-manager.ts:73), and nothing generates a skill or
|
|
3203
|
-
// prompt section for it — model-visible MCP is exactly the built-in
|
|
3204
|
-
// linear/notion catalog skills (packages/coding-agent/skills/…). Wiring
|
|
3205
|
-
// `profile.mcp` into settings.json would materialize config the model can
|
|
3206
|
-
// never discover: a silently dropped control.
|
|
3215
|
+
// Kept REJECTED with the fork evidence — see {@link PRIME_MCP_UNSUPPORTED_REASON}
|
|
3216
|
+
// for `mcp`/`connections`, and the per-control reasons in
|
|
3217
|
+
// {@link readPrimeProfileControls} for permissions, tools, hooks, modes,
|
|
3218
|
+
// confidential, and the resource kinds with no agent-dir read path.
|
|
3207
3219
|
//
|
|
3208
|
-
// -
|
|
3209
|
-
//
|
|
3210
|
-
//
|
|
3211
|
-
//
|
|
3212
|
-
//
|
|
3213
|
-
//
|
|
3220
|
+
// The store is AGENT-OWNED: it is written by the agent mid-session and rides
|
|
3221
|
+
// every later system prompt. A store that exists but no longer parses
|
|
3222
|
+
// therefore fails the turn ({@link PrimeHarnessStateError}) rather than being
|
|
3223
|
+
// reseeded from empty — the fork's own loader degrades to empty in-process
|
|
3224
|
+
// (refinement.ts:290-300), but an executor that OVERWRITES the file makes that
|
|
3225
|
+
// degradation permanent and destroys the evidence of what happened.
|
|
3214
3226
|
/** Skill name rules enforced by the fork's Agent Skills loader (skills.ts:122-147). */
|
|
3215
3227
|
const PRIME_SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
3216
3228
|
const PRIME_MAX_SKILL_NAME_LENGTH = 64;
|
|
@@ -3218,6 +3230,8 @@ const PRIME_PROFILE_DIR_NAME = "profile";
|
|
|
3218
3230
|
const PRIME_SKILLS_DIR_NAME = "skills";
|
|
3219
3231
|
const PRIME_SYSTEM_PROMPT_FILE = "system-prompt.md";
|
|
3220
3232
|
const PRIME_APPEND_SYSTEM_PROMPT_FILE = "append-system-prompt.md";
|
|
3233
|
+
/** Agent-dir global context file the fork reads (resource-loader.ts:59,86). */
|
|
3234
|
+
const PRIME_CONTEXT_FILE = "AGENTS.md";
|
|
3221
3235
|
/** Fork continual-harness store location under the agent dir (refinement.ts:269-279). */
|
|
3222
3236
|
const PRIME_HARNESS_DIR_NAME = "harness";
|
|
3223
3237
|
const PRIME_HARNESS_STATE_FILE = "harness_state.json";
|
|
@@ -3228,22 +3242,101 @@ const PRIME_HARNESS_KINDS = ["prompt", "memory", "skill", "subagent"];
|
|
|
3228
3242
|
* touched.
|
|
3229
3243
|
*/
|
|
3230
3244
|
const PRIME_PROFILE_ENTRY_SOURCE = "profile";
|
|
3231
|
-
|
|
3232
|
-
|
|
3245
|
+
/**
|
|
3246
|
+
* Provenance markers written by executors that predate this shared lowering.
|
|
3247
|
+
*
|
|
3248
|
+
* Pruning keys on the source tag, so an executor that switches to the shared
|
|
3249
|
+
* implementation would otherwise leave its own earlier entries behind forever:
|
|
3250
|
+
* they match no current profile and no current source, and the model's
|
|
3251
|
+
* spawnable roster silently accumulates duplicates of every subagent. They are
|
|
3252
|
+
* adopted (pruned or replaced) on the first shared run instead.
|
|
3253
|
+
*/
|
|
3254
|
+
const PRIME_LEGACY_PROFILE_ENTRY_SOURCES = [
|
|
3255
|
+
"cli-bridge-profile",
|
|
3256
|
+
];
|
|
3257
|
+
/** True when the controls materialize anything at all. */
|
|
3258
|
+
export function hasPrimeProfileMaterial(controls) {
|
|
3259
|
+
return (controls.systemPrompt !== undefined ||
|
|
3260
|
+
controls.appendSystemPrompt !== undefined ||
|
|
3261
|
+
primeProfileNeedsAgentDir(controls) ||
|
|
3262
|
+
controls.skills.length > 0);
|
|
3263
|
+
}
|
|
3264
|
+
/**
|
|
3265
|
+
* True when the controls must WRITE INTO the agent dir itself.
|
|
3266
|
+
*
|
|
3267
|
+
* Prompt files and skill dirs are named by a flag, so they can live anywhere
|
|
3268
|
+
* the agent can read (`--system-prompt` / `--append-system-prompt` resolve a
|
|
3269
|
+
* path when it exists, fork resource-loader.ts:41-56; `--skill` takes an
|
|
3270
|
+
* arbitrary dir, :437-442). The context file and the continual-harness store
|
|
3271
|
+
* are located BY the agent dir and have no flag, so they cannot move.
|
|
3272
|
+
*
|
|
3273
|
+
* An executor pointed at an agent dir it does not own (an operator's shared
|
|
3274
|
+
* dir) uses this to refuse exactly the controls it would have to write there,
|
|
3275
|
+
* while still binding the ones it can materialize elsewhere.
|
|
3276
|
+
*/
|
|
3277
|
+
export function primeProfileNeedsAgentDir(controls) {
|
|
3278
|
+
return controls.instructions !== undefined || controls.subagents.length > 0;
|
|
3279
|
+
}
|
|
3280
|
+
/**
|
|
3281
|
+
* Read one instruction block, which the profile may carry inline or as a
|
|
3282
|
+
* resource ref. Only inline refs are accepted: resolving a GitHub-backed ref
|
|
3283
|
+
* fetches remote content, which is `resolveAgentProfileResources`' job and
|
|
3284
|
+
* must happen before a profile reaches an executor.
|
|
3285
|
+
*/
|
|
3286
|
+
function primeInstructionBlock(value, control) {
|
|
3287
|
+
if (typeof value === "string")
|
|
3288
|
+
return value;
|
|
3289
|
+
if (value.kind === "inline")
|
|
3290
|
+
return value.content;
|
|
3291
|
+
rejectPrimeControl(control, "only inline resource refs are accepted — resolve GitHub-backed refs " +
|
|
3292
|
+
"(resolveAgentProfileResources) before handing the profile to this lowering.");
|
|
3233
3293
|
}
|
|
3234
3294
|
/**
|
|
3235
3295
|
* Validate and extract the profile controls the prime agent-dir lowering
|
|
3236
|
-
* materializes. Throws on any control (or control
|
|
3237
|
-
* verified read path for — partial acceptance would
|
|
3296
|
+
* materializes. Throws {@link PrimeProfileError} on any control (or control
|
|
3297
|
+
* facet) the fork has no verified read path for — partial acceptance would
|
|
3298
|
+
* silently drop the rest.
|
|
3238
3299
|
*/
|
|
3239
3300
|
export function readPrimeProfileControls(profile) {
|
|
3240
3301
|
const controls = { skills: [], subagents: [] };
|
|
3241
3302
|
if (!profile)
|
|
3242
3303
|
return controls;
|
|
3243
3304
|
if (profile.harness !== undefined && profile.harness !== "prime") {
|
|
3244
|
-
|
|
3305
|
+
rejectPrimeDeclaration("harness", `prime cannot run a profile pinned to harness "${profile.harness}" — ` +
|
|
3245
3306
|
'only "prime" (or an unset harness preference) is accepted.');
|
|
3246
3307
|
}
|
|
3308
|
+
// An entry with `enabled: false` is OFF, not requested. A fully disabled map
|
|
3309
|
+
// asks for nothing, so accepting it drops no control — which is the only
|
|
3310
|
+
// thing this refusal exists to prevent.
|
|
3311
|
+
const requestedMcp = Object.keys(activeMcpServers(profile.mcp));
|
|
3312
|
+
if (requestedMcp.length > 0) {
|
|
3313
|
+
rejectPrimeControl(`mcp: ${requestedMcp.join(", ")}`, PRIME_MCP_UNSUPPORTED_REASON);
|
|
3314
|
+
}
|
|
3315
|
+
if (profile.connections !== undefined) {
|
|
3316
|
+
rejectPrimeControl("connections", `a hub connection grant resolves to MCP tools, and ${PRIME_MCP_UNSUPPORTED_REASON}`);
|
|
3317
|
+
}
|
|
3318
|
+
if (profile.permissions !== undefined) {
|
|
3319
|
+
rejectPrimeControl("permissions", "prime-agent exposes no permission control at all (full flag scan of fork " +
|
|
3320
|
+
"cli/args.ts:93-295; native tools run unconditionally), so a permission map " +
|
|
3321
|
+
"would be accepted and never enforced.");
|
|
3322
|
+
}
|
|
3323
|
+
if (profile.tools !== undefined) {
|
|
3324
|
+
rejectPrimeControl("tools", "the only per-run tool control is `--tools` over the single builtin `ipython` " +
|
|
3325
|
+
"(fork cli/args.ts:63-64,156-167); bash/edit and the rest are extension-provided " +
|
|
3326
|
+
"and not addressable per run, so a harness-neutral tool map cannot bind to it.");
|
|
3327
|
+
}
|
|
3328
|
+
if (profile.hooks !== undefined) {
|
|
3329
|
+
rejectPrimeControl("hooks", "prime-agent has no hook mechanism (fork cli/args.ts flag scan; extensions are " +
|
|
3330
|
+
"host-trusted JS modules loaded into the agent process, not caller command hooks).");
|
|
3331
|
+
}
|
|
3332
|
+
if (profile.modes !== undefined) {
|
|
3333
|
+
rejectPrimeControl("modes", "prime-agent has no mode concept (fork cli/args.ts flag scan).");
|
|
3334
|
+
}
|
|
3335
|
+
if (profile.confidential !== undefined) {
|
|
3336
|
+
rejectPrimeControl("confidential", "confidential execution is a property of the executor's isolation boundary, not " +
|
|
3337
|
+
"of the agent dir; this lowering cannot establish it.");
|
|
3338
|
+
}
|
|
3339
|
+
const instructionBlocks = [];
|
|
3247
3340
|
if (profile.prompt) {
|
|
3248
3341
|
const { systemPrompt, appendSystemPrompt, instructions } = profile.prompt;
|
|
3249
3342
|
const knownPromptFields = new Set([
|
|
@@ -3257,15 +3350,9 @@ export function readPrimeProfileControls(profile) {
|
|
|
3257
3350
|
if (unknown.length > 0) {
|
|
3258
3351
|
rejectPrimeControl(`prompt fields: ${unknown.join(", ")}`, "no verified prime-agent read path exists for them.");
|
|
3259
3352
|
}
|
|
3260
|
-
if (instructions !== undefined) {
|
|
3261
|
-
rejectPrimeControl("prompt.instructions", "prime-agent's caller-instruction surface is the workspace " +
|
|
3262
|
-
"AGENTS.md context file (fork resource-loader.ts:58-66), which is " +
|
|
3263
|
-
"caller workspace content this adapter must not write into. Use " +
|
|
3264
|
-
"prompt.appendSystemPrompt instead.");
|
|
3265
|
-
}
|
|
3266
3353
|
if (systemPrompt !== undefined) {
|
|
3267
3354
|
if (typeof systemPrompt !== "string" || systemPrompt.trim() === "") {
|
|
3268
|
-
|
|
3355
|
+
rejectPrimeDeclaration("prompt.systemPrompt", "an empty replacement would delete the built-in system prompt and " +
|
|
3269
3356
|
"install nothing.");
|
|
3270
3357
|
}
|
|
3271
3358
|
controls.systemPrompt = systemPrompt;
|
|
@@ -3273,52 +3360,69 @@ export function readPrimeProfileControls(profile) {
|
|
|
3273
3360
|
if (appendSystemPrompt !== undefined) {
|
|
3274
3361
|
if (typeof appendSystemPrompt !== "string" ||
|
|
3275
3362
|
appendSystemPrompt.trim() === "") {
|
|
3276
|
-
|
|
3363
|
+
rejectPrimeDeclaration("prompt.appendSystemPrompt", "must be non-empty text.");
|
|
3277
3364
|
}
|
|
3278
3365
|
controls.appendSystemPrompt = appendSystemPrompt;
|
|
3279
3366
|
}
|
|
3367
|
+
for (const block of instructions ?? []) {
|
|
3368
|
+
if (typeof block === "string" && block.trim() !== "")
|
|
3369
|
+
instructionBlocks.push(block);
|
|
3370
|
+
}
|
|
3280
3371
|
}
|
|
3281
3372
|
if (profile.resources) {
|
|
3282
|
-
const { skills, failOnError, ...unsupported } = profile.resources;
|
|
3373
|
+
const { skills, instructions, failOnError, ...unsupported } = profile.resources;
|
|
3283
3374
|
const rejectedKeys = Object.entries(unsupported)
|
|
3284
3375
|
.filter(([, value]) => value !== undefined)
|
|
3285
3376
|
.map(([key]) => key);
|
|
3286
3377
|
if (rejectedKeys.length > 0) {
|
|
3287
|
-
rejectPrimeControl(`resources: ${rejectedKeys.join(", ")}`, "only resources.skills
|
|
3288
|
-
"(
|
|
3378
|
+
rejectPrimeControl(`resources: ${rejectedKeys.join(", ")}`, "only resources.skills (--skill dirs, fork resource-loader.ts:437-442) and " +
|
|
3379
|
+
"resources.instructions (agent-dir AGENTS.md, resource-loader.ts:86) have a " +
|
|
3380
|
+
"verified prime-agent read path. Generic workspace files, tool files, " +
|
|
3381
|
+
"subagent-definition files and prompt templates either have no headless read " +
|
|
3382
|
+
"path or would run caller-authored code inside the agent process.");
|
|
3289
3383
|
}
|
|
3290
3384
|
if (failOnError === false) {
|
|
3291
|
-
rejectPrimeControl("resources.failOnError: false", "this
|
|
3385
|
+
rejectPrimeControl("resources.failOnError: false", "this lowering always fails loud on a resource it cannot " +
|
|
3292
3386
|
"materialize; lenient materialization is not implemented.");
|
|
3293
3387
|
}
|
|
3388
|
+
if (instructions !== undefined) {
|
|
3389
|
+
const block = primeInstructionBlock(instructions, "resources.instructions");
|
|
3390
|
+
if (block.trim() !== "")
|
|
3391
|
+
instructionBlocks.push(block);
|
|
3392
|
+
}
|
|
3294
3393
|
const seen = new Set();
|
|
3295
|
-
for (const ref of skills ?? []) {
|
|
3394
|
+
for (const [index, ref] of (skills ?? []).entries()) {
|
|
3296
3395
|
if (ref.kind !== "inline") {
|
|
3297
|
-
rejectPrimeControl(`resources.skills
|
|
3396
|
+
rejectPrimeControl(`resources.skills[${index}]`, "only inline skill refs are accepted — resolve GitHub-backed refs " +
|
|
3298
3397
|
"(resolveAgentProfileResources) before handing the profile to " +
|
|
3299
|
-
"this
|
|
3398
|
+
"this lowering.");
|
|
3300
3399
|
}
|
|
3301
3400
|
const name = ref.name;
|
|
3302
3401
|
if (!PRIME_SKILL_NAME_RE.test(name) ||
|
|
3303
3402
|
name.length > PRIME_MAX_SKILL_NAME_LENGTH) {
|
|
3304
|
-
|
|
3305
|
-
|
|
3403
|
+
rejectPrimeDeclaration(`resources.skills[${index}]`, `name ${JSON.stringify(name)} is not a valid prime skill name — it becomes the ` +
|
|
3404
|
+
"skill directory name, so it must be lowercase a-z0-9 with single hyphens, at " +
|
|
3405
|
+
`most ${PRIME_MAX_SKILL_NAME_LENGTH} chars (fork skills.ts:122-147).`);
|
|
3306
3406
|
}
|
|
3307
3407
|
if (seen.has(name)) {
|
|
3308
|
-
|
|
3408
|
+
rejectPrimeDeclaration("resources.skills", `declares ${JSON.stringify(name)} twice; duplicate skill names collide in the ` +
|
|
3409
|
+
"fork's skill registry, which keeps only the first and warns.");
|
|
3309
3410
|
}
|
|
3310
3411
|
seen.add(name);
|
|
3311
3412
|
if (ref.content.trim() === "") {
|
|
3312
|
-
|
|
3413
|
+
rejectPrimeDeclaration(`resources.skills[${index}]`, `skill ${JSON.stringify(name)} declares no content.`);
|
|
3313
3414
|
}
|
|
3314
3415
|
controls.skills.push({ name, content: ref.content });
|
|
3315
3416
|
}
|
|
3316
3417
|
}
|
|
3418
|
+
if (instructionBlocks.length > 0) {
|
|
3419
|
+
controls.instructions = instructionBlocks.join("\n\n");
|
|
3420
|
+
}
|
|
3317
3421
|
if (profile.subagents) {
|
|
3318
3422
|
const usedIds = new Map();
|
|
3319
3423
|
for (const [name, spec] of Object.entries(profile.subagents)) {
|
|
3320
3424
|
if (name.trim() === "") {
|
|
3321
|
-
|
|
3425
|
+
rejectPrimeDeclaration("subagents", "a subagent name must be non-empty.");
|
|
3322
3426
|
}
|
|
3323
3427
|
const inexpressible = [
|
|
3324
3428
|
["model", spec.model],
|
|
@@ -3329,19 +3433,20 @@ export function readPrimeProfileControls(profile) {
|
|
|
3329
3433
|
.filter(([, value]) => value !== undefined)
|
|
3330
3434
|
.map(([key]) => key);
|
|
3331
3435
|
if (inexpressible.length > 0) {
|
|
3332
|
-
rejectPrimeControl(`subagents[
|
|
3436
|
+
rejectPrimeControl(`subagents[${JSON.stringify(name)}] fields: ${inexpressible.join(", ")}`, "a prime continual-harness subagent spec is prompt-roster " +
|
|
3333
3437
|
"guidance for `await rlm('<task>')` (fork refinement.ts:483-496) " +
|
|
3334
3438
|
"and pins no model, tool set, permission, or step budget on the " +
|
|
3335
3439
|
"spawned child.");
|
|
3336
3440
|
}
|
|
3337
3441
|
if (!spec.description?.trim() && !spec.prompt?.trim()) {
|
|
3338
|
-
|
|
3339
|
-
"the model an empty roster line.");
|
|
3442
|
+
rejectPrimeDeclaration(`subagents[${JSON.stringify(name)}]`, "declares no behavior — a spec needs a description and/or prompt, since an " +
|
|
3443
|
+
"empty entry gives the model an empty roster line.");
|
|
3340
3444
|
}
|
|
3341
3445
|
const id = primeSubagentEntryId(name);
|
|
3342
3446
|
const clash = usedIds.get(id);
|
|
3343
3447
|
if (clash !== undefined) {
|
|
3344
|
-
|
|
3448
|
+
rejectPrimeDeclaration(`subagents[${JSON.stringify(name)}]`, `collides with subagent ${JSON.stringify(clash)} on harness-state id ` +
|
|
3449
|
+
`${JSON.stringify(id)}.`);
|
|
3345
3450
|
}
|
|
3346
3451
|
usedIds.set(id, name);
|
|
3347
3452
|
controls.subagents.push({
|
|
@@ -3354,6 +3459,39 @@ export function readPrimeProfileControls(profile) {
|
|
|
3354
3459
|
}
|
|
3355
3460
|
return controls;
|
|
3356
3461
|
}
|
|
3462
|
+
/**
|
|
3463
|
+
* Refuse a run whose selected model disagrees with the profile's own.
|
|
3464
|
+
*
|
|
3465
|
+
* One authority per run: a profile names the model it was written and scored
|
|
3466
|
+
* against, so running it on a different one silently scores a different agent.
|
|
3467
|
+
* Both sides go through {@link modelIdsMatch}, so the qualification level a
|
|
3468
|
+
* caller happened to use is not a conflict.
|
|
3469
|
+
*
|
|
3470
|
+
* `provider` is the executor's PROVIDER SELECTION, and is compared only when
|
|
3471
|
+
* the executor's provider is part of model identity. An executor whose
|
|
3472
|
+
* provider key is merely a namespace inside the agent dir passes it as
|
|
3473
|
+
* undefined and gets the model-name comparison alone.
|
|
3474
|
+
*/
|
|
3475
|
+
export function assertPrimeModelAgreement(profile, selected) {
|
|
3476
|
+
const declared = profile?.model?.default;
|
|
3477
|
+
const declaredProvider = profile?.model?.provider;
|
|
3478
|
+
if (declared !== undefined) {
|
|
3479
|
+
const selectedId = qualifyModelId(selected.provider, selected.model);
|
|
3480
|
+
const declaredId = qualifyModelId(declaredProvider, declared);
|
|
3481
|
+
if (!modelIdsMatch(selectedId, declaredId, selected.provider)) {
|
|
3482
|
+
rejectPrimeDeclaration("model.default", `the run selects ${JSON.stringify(selectedId)} but the profile names ` +
|
|
3483
|
+
`${JSON.stringify(declaredId)}; a profile must not be scored against a model it ` +
|
|
3484
|
+
"does not name.");
|
|
3485
|
+
}
|
|
3486
|
+
return;
|
|
3487
|
+
}
|
|
3488
|
+
if (selected.provider !== undefined &&
|
|
3489
|
+
declaredProvider !== undefined &&
|
|
3490
|
+
declaredProvider !== selected.provider) {
|
|
3491
|
+
rejectPrimeDeclaration("model.provider", `the run selects provider ${JSON.stringify(selected.provider)} but the profile names ` +
|
|
3492
|
+
`${JSON.stringify(declaredProvider)}.`);
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3357
3495
|
/** Deterministic harness-state entry id for a profile subagent name. */
|
|
3358
3496
|
export function primeSubagentEntryId(name) {
|
|
3359
3497
|
const slug = name
|
|
@@ -3363,96 +3501,127 @@ export function primeSubagentEntryId(name) {
|
|
|
3363
3501
|
return `profile-${slug || "subagent"}`;
|
|
3364
3502
|
}
|
|
3365
3503
|
/**
|
|
3366
|
-
*
|
|
3367
|
-
*
|
|
3368
|
-
*
|
|
3369
|
-
*
|
|
3504
|
+
* Write one agent-dir file atomically (temp + rename).
|
|
3505
|
+
*
|
|
3506
|
+
* Every file this lowering writes lands in a directory a LIVE agent may be
|
|
3507
|
+
* reading: prime reloads models.json at model-resolution time and re-reads the
|
|
3508
|
+
* harness store on every system-prompt build, so a partial write is observable.
|
|
3509
|
+
* `mode` is applied to the temp file before the rename, so the final file is
|
|
3510
|
+
* never briefly world-readable.
|
|
3511
|
+
*/
|
|
3512
|
+
export function writePrimeAgentFile(path, data, mode = 0o600) {
|
|
3513
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
3514
|
+
const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
3515
|
+
writeFileSync(temp, data, { mode });
|
|
3516
|
+
renameSync(temp, path);
|
|
3517
|
+
}
|
|
3518
|
+
/**
|
|
3519
|
+
* Materialize the validated controls and return the extra spawn args that bind
|
|
3520
|
+
* them. Runs before every spawn (same place models.json is materialized), so a
|
|
3521
|
+
* continual agent dir is re-pinned to the current profile on each fresh prime
|
|
3522
|
+
* process.
|
|
3370
3523
|
*/
|
|
3371
|
-
export function materializePrimeProfileControls(agentDir, controls,
|
|
3524
|
+
export function materializePrimeProfileControls(agentDir, controls, options = {}) {
|
|
3525
|
+
const { flagFileRoot = agentDir, warn } = options;
|
|
3372
3526
|
const args = [];
|
|
3373
|
-
const profileDir = join(
|
|
3527
|
+
const profileDir = join(flagFileRoot, PRIME_PROFILE_DIR_NAME);
|
|
3374
3528
|
if (controls.systemPrompt !== undefined) {
|
|
3375
|
-
mkdirSync(profileDir, { recursive: true });
|
|
3376
3529
|
const promptPath = join(profileDir, PRIME_SYSTEM_PROMPT_FILE);
|
|
3377
|
-
|
|
3530
|
+
writePrimeAgentFile(promptPath, controls.systemPrompt);
|
|
3378
3531
|
args.push("--system-prompt", promptPath);
|
|
3379
3532
|
}
|
|
3380
3533
|
if (controls.appendSystemPrompt !== undefined) {
|
|
3381
|
-
mkdirSync(profileDir, { recursive: true });
|
|
3382
3534
|
const appendPath = join(profileDir, PRIME_APPEND_SYSTEM_PROMPT_FILE);
|
|
3383
|
-
|
|
3535
|
+
writePrimeAgentFile(appendPath, controls.appendSystemPrompt);
|
|
3384
3536
|
args.push("--append-system-prompt", appendPath);
|
|
3385
3537
|
}
|
|
3538
|
+
if (controls.instructions !== undefined) {
|
|
3539
|
+
writePrimeAgentFile(join(agentDir, PRIME_CONTEXT_FILE), `${controls.instructions}\n`);
|
|
3540
|
+
}
|
|
3386
3541
|
if (controls.skills.length > 0) {
|
|
3387
3542
|
const skillsRoot = join(profileDir, PRIME_SKILLS_DIR_NAME);
|
|
3388
3543
|
for (const skill of controls.skills) {
|
|
3389
|
-
|
|
3390
|
-
mkdirSync(skillDir, { recursive: true });
|
|
3391
|
-
writeFileSync(join(skillDir, "SKILL.md"), normalizeSkillMd(skill.name, skill.content), "utf8");
|
|
3544
|
+
writePrimeAgentFile(join(skillsRoot, skill.name, "SKILL.md"), normalizeSkillMd(skill.name, skill.content));
|
|
3392
3545
|
}
|
|
3393
3546
|
args.push("--skill", skillsRoot);
|
|
3394
3547
|
}
|
|
3395
3548
|
seedPrimeProfileSubagents(agentDir, controls.subagents, warn);
|
|
3396
3549
|
return args;
|
|
3397
3550
|
}
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3551
|
+
/**
|
|
3552
|
+
* Remove the files a PREVIOUS materialization of this lowering wrote, so a
|
|
3553
|
+
* profile that shrank cannot leave a stale instruction file or skill behind for
|
|
3554
|
+
* the next run against the same dirs.
|
|
3555
|
+
*
|
|
3556
|
+
* Only the paths this lowering NAMES are removed. There is deliberately no
|
|
3557
|
+
* manifest: a manifest would live where the agent can write it, and reading a
|
|
3558
|
+
* delete list out of agent-writable state turns pruning into an arbitrary-path
|
|
3559
|
+
* delete.
|
|
3560
|
+
*/
|
|
3561
|
+
export function prunePrimeProfileMaterial(agentDir, options = {}) {
|
|
3562
|
+
const { flagFileRoot = agentDir } = options;
|
|
3563
|
+
rmSync(join(flagFileRoot, PRIME_PROFILE_DIR_NAME), {
|
|
3564
|
+
recursive: true,
|
|
3565
|
+
force: true,
|
|
3566
|
+
});
|
|
3567
|
+
rmSync(join(agentDir, PRIME_CONTEXT_FILE), { force: true });
|
|
3403
3568
|
}
|
|
3404
3569
|
/**
|
|
3405
3570
|
* Upsert profile subagent specs into the fork's global continual-harness
|
|
3406
3571
|
* store and remove profile-sourced entries the current profile no longer
|
|
3407
|
-
* declares. Agent-written entries (`source: "agent"`)
|
|
3572
|
+
* declares. Agent-written entries (`source: "agent"`) — and every top-level
|
|
3573
|
+
* key this lowering does not manage — are preserved untouched.
|
|
3408
3574
|
*
|
|
3409
|
-
* A
|
|
3410
|
-
*
|
|
3411
|
-
*
|
|
3575
|
+
* A store that exists but no longer parses raises
|
|
3576
|
+
* {@link PrimeHarnessStateError}: the file is the agent's own self-modification
|
|
3577
|
+
* state, and overwriting it destroys what the run was supposed to observe.
|
|
3412
3578
|
*/
|
|
3413
3579
|
export function seedPrimeProfileSubagents(agentDir, subagents, warn) {
|
|
3414
3580
|
const statePath = join(agentDir, PRIME_HARNESS_DIR_NAME, PRIME_HARNESS_STATE_FILE);
|
|
3415
|
-
let state =
|
|
3581
|
+
let state = {};
|
|
3416
3582
|
if (existsSync(statePath)) {
|
|
3583
|
+
let parsed;
|
|
3417
3584
|
try {
|
|
3418
|
-
|
|
3419
|
-
if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
|
|
3420
|
-
const parsed = raw;
|
|
3421
|
-
state.schema =
|
|
3422
|
-
typeof parsed.schema === "number" ? parsed.schema : state.schema;
|
|
3423
|
-
for (const kind of PRIME_HARNESS_KINDS) {
|
|
3424
|
-
const records = parsed.entries?.[kind];
|
|
3425
|
-
if (records && typeof records === "object") {
|
|
3426
|
-
state.entries[kind] = { ...records };
|
|
3427
|
-
}
|
|
3428
|
-
}
|
|
3429
|
-
if (Array.isArray(parsed.refinements)) {
|
|
3430
|
-
state.refinements = parsed.refinements;
|
|
3431
|
-
}
|
|
3432
|
-
}
|
|
3433
|
-
else {
|
|
3434
|
-
warn("prime: existing harness_state.json is not an object — reseeding", {
|
|
3435
|
-
statePath,
|
|
3436
|
-
});
|
|
3437
|
-
}
|
|
3585
|
+
parsed = JSON.parse(readFileSync(statePath, "utf8"));
|
|
3438
3586
|
}
|
|
3439
3587
|
catch (err) {
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
state = emptyPrimeHarnessState();
|
|
3588
|
+
throw new PrimeHarnessStateError(statePath, err instanceof Error ? err.message : String(err));
|
|
3589
|
+
}
|
|
3590
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3591
|
+
throw new PrimeHarnessStateError(statePath, "the store is not a JSON object");
|
|
3445
3592
|
}
|
|
3593
|
+
state = parsed;
|
|
3446
3594
|
}
|
|
3447
3595
|
else if (subagents.length === 0) {
|
|
3448
3596
|
// Nothing to pin and nothing stale to remove.
|
|
3449
3597
|
return;
|
|
3450
3598
|
}
|
|
3451
|
-
const
|
|
3599
|
+
const rawEntries = state.entries;
|
|
3600
|
+
const entries = rawEntries && typeof rawEntries === "object" && !Array.isArray(rawEntries)
|
|
3601
|
+
? rawEntries
|
|
3602
|
+
: {};
|
|
3603
|
+
for (const kind of PRIME_HARNESS_KINDS) {
|
|
3604
|
+
const records = entries[kind];
|
|
3605
|
+
if (!records || typeof records !== "object" || Array.isArray(records)) {
|
|
3606
|
+
entries[kind] = {};
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
const existing = entries.subagent;
|
|
3452
3610
|
const desiredIds = new Set(subagents.map((s) => primeSubagentEntryId(s.name)));
|
|
3453
3611
|
let changed = false;
|
|
3454
3612
|
for (const [id, entry] of Object.entries(existing)) {
|
|
3455
|
-
|
|
3613
|
+
const source = entry?.source;
|
|
3614
|
+
const isLegacy = typeof source === "string" &&
|
|
3615
|
+
PRIME_LEGACY_PROFILE_ENTRY_SOURCES.includes(source);
|
|
3616
|
+
if (isLegacy) {
|
|
3617
|
+
warn?.("prime: adopting a subagent entry left by a pre-shared executor", {
|
|
3618
|
+
statePath,
|
|
3619
|
+
id,
|
|
3620
|
+
source,
|
|
3621
|
+
});
|
|
3622
|
+
}
|
|
3623
|
+
if ((source === PRIME_PROFILE_ENTRY_SOURCE || isLegacy) &&
|
|
3624
|
+
!desiredIds.has(id)) {
|
|
3456
3625
|
delete existing[id];
|
|
3457
3626
|
changed = true;
|
|
3458
3627
|
}
|
|
@@ -3475,7 +3644,7 @@ export function seedPrimeProfileSubagents(agentDir, subagents, warn) {
|
|
|
3475
3644
|
kind: "subagent",
|
|
3476
3645
|
title: spec.name,
|
|
3477
3646
|
content,
|
|
3478
|
-
path:
|
|
3647
|
+
path: PRIME_PROFILE_ENTRY_SOURCE,
|
|
3479
3648
|
scope: "global",
|
|
3480
3649
|
reference: {},
|
|
3481
3650
|
arguments: {},
|
|
@@ -3489,9 +3658,12 @@ export function seedPrimeProfileSubagents(agentDir, subagents, warn) {
|
|
|
3489
3658
|
};
|
|
3490
3659
|
changed = true;
|
|
3491
3660
|
}
|
|
3492
|
-
state.entries.subagent = existing;
|
|
3493
3661
|
if (!changed)
|
|
3494
3662
|
return;
|
|
3495
|
-
|
|
3496
|
-
|
|
3663
|
+
writePrimeAgentFile(statePath, `${JSON.stringify({
|
|
3664
|
+
...state,
|
|
3665
|
+
schema: typeof state.schema === "number" ? state.schema : 1,
|
|
3666
|
+
entries,
|
|
3667
|
+
refinements: Array.isArray(state.refinements) ? state.refinements : [],
|
|
3668
|
+
}, null, 2)}\n`);
|
|
3497
3669
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fork FACTS for prime-agent (PrimeIntellect-ai/prime-agent) — the constants,
|
|
3
|
+
* dialects and refusal reasons every executor that drives the fork needs, with
|
|
4
|
+
* no `node:fs` and no executor assumptions. The agent-dir MATERIALIZATION that
|
|
5
|
+
* consumes them lives in `./index.ts`; both consumers (cli-bridge's host-spawn
|
|
6
|
+
* backend on `--mode rpc`, sdk-provider-prime's in-sandbox adapter on
|
|
7
|
+
* `--mode acp`) read these instead of each keeping a private copy, because a
|
|
8
|
+
* fork bump must move one file, not two repositories.
|
|
9
|
+
*
|
|
10
|
+
* Every value here is verified against the pinned clone named by
|
|
11
|
+
* {@link PRIME_FORK_PIN}; `tests/prime-fork-pin.test.ts` re-asserts them
|
|
12
|
+
* against that clone when it is present, so a fork bump fails loudly in one
|
|
13
|
+
* place.
|
|
14
|
+
*/
|
|
15
|
+
/** The prime-agent clone every constant in this module was read from. */
|
|
16
|
+
export declare const PRIME_FORK_PIN: {
|
|
17
|
+
readonly repo: "PrimeIntellect-ai/prime-agent";
|
|
18
|
+
readonly version: "0.7.0";
|
|
19
|
+
readonly commit: "be9e2fa0";
|
|
20
|
+
/** Local clone used by the fork-pin test when it exists. */
|
|
21
|
+
readonly clonePath: "/home/drew/bench-cache/prime-agent";
|
|
22
|
+
/** Package root inside the clone that the file:line citations are relative to. */
|
|
23
|
+
readonly packageRoot: "packages/coding-agent/src";
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The fork's package identity, and why it cannot be installed from the npm
|
|
27
|
+
* registry.
|
|
28
|
+
*
|
|
29
|
+
* `@earendil-works/pi-coding-agent` on registry.npmjs.org serves the UPSTREAM
|
|
30
|
+
* pi line (0.84.x); the fork's 0.7.x versions were never published there. The
|
|
31
|
+
* two are wire-incompatible — different daemon schema revision, different
|
|
32
|
+
* config dir, different agent-dir env var (see
|
|
33
|
+
* {@link PRIME_AGENT_DIR_ENV_VARS}) — so `npm install -g` under that name
|
|
34
|
+
* yields an agent none of this lowering was verified against. The fork must be
|
|
35
|
+
* built from source and exposed under a `prime-agent` command name; its own
|
|
36
|
+
* `bin` entry is `pi` (fork package.json `bin`), which is exactly how the wrong
|
|
37
|
+
* binary ends up on `PATH` under the right-looking name.
|
|
38
|
+
*/
|
|
39
|
+
export declare const PRIME_FORK_INSTALL: {
|
|
40
|
+
readonly npmName: "@earendil-works/pi-coding-agent";
|
|
41
|
+
/** The fork's own bin name — deliberately NOT the command to install it as. */
|
|
42
|
+
readonly forkBinName: "pi";
|
|
43
|
+
/** Command name an executor's binary path is expected to resolve to. */
|
|
44
|
+
readonly expectedCommandName: "prime-agent";
|
|
45
|
+
readonly sourceRepo: "https://github.com/PrimeIntellect-ai/prime-agent";
|
|
46
|
+
/** Built entrypoint to expose as `prime-agent`. */
|
|
47
|
+
readonly sourceEntrypoint: "packages/coding-agent/dist/bundle/cli.js";
|
|
48
|
+
readonly reason: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Env vars the agent resolves its config dir from, most-specific first.
|
|
52
|
+
*
|
|
53
|
+
* The fork derives the name from its own package identity —
|
|
54
|
+
* `ENV_AGENT_DIR = \`${envPrefix}_CODING_AGENT_DIR\`` where `envPrefix` comes
|
|
55
|
+
* from `package.json` `piConfig.name` (config.ts:485-498) — so the fork reads
|
|
56
|
+
* `PRIME_AGENT_CODING_AGENT_DIR` and upstream pi reads `PI_CODING_AGENT_DIR`.
|
|
57
|
+
* An executor that sets only the fork's name has its isolation SILENTLY
|
|
58
|
+
* disabled when the binary on PATH turns out to be upstream pi: `getAgentDir()`
|
|
59
|
+
* falls back to `homedir()/<CONFIG_DIR_NAME>` (config.ts:520-527), one shared
|
|
60
|
+
* dir — and therefore one shared self-modifying `harness_state.json` — for
|
|
61
|
+
* every session on the box. Setting BOTH names costs nothing and keeps the
|
|
62
|
+
* isolation fail-closed under either binary.
|
|
63
|
+
*/
|
|
64
|
+
export declare const PRIME_AGENT_DIR_ENV_VARS: readonly ["PRIME_AGENT_CODING_AGENT_DIR", "PI_CODING_AGENT_DIR"];
|
|
65
|
+
/** Canonical agent-dir env var for the fork itself (config.ts:498). */
|
|
66
|
+
export declare const PRIME_AGENT_DIR_ENV_VAR: "PRIME_AGENT_CODING_AGENT_DIR";
|
|
67
|
+
/** Pin `agentDir` under every name the agent could resolve its config dir from. */
|
|
68
|
+
export declare function primeAgentDirEnv(agentDir: string): Record<string, string>;
|
|
69
|
+
/**
|
|
70
|
+
* Operator knobs an executor forwards into the agent when they are set.
|
|
71
|
+
*
|
|
72
|
+
* `PRIME_AGENT_KERNEL_VENV` is load-bearing for any executor that also pins
|
|
73
|
+
* HOME: the kernel venv defaults under `homedir()/.prime/agent/kernel-venv`
|
|
74
|
+
* (kernel/bootstrap.ts:331-342), so a fresh HOME re-bootstraps it through uv on
|
|
75
|
+
* every run unless the operator's venv path is forwarded
|
|
76
|
+
* (bootstrap.ts:340 override, :358 branch).
|
|
77
|
+
*/
|
|
78
|
+
export declare const PRIME_KERNEL_ENV_PASSTHROUGH: readonly ["PRIME_AGENT_INSTALL_UV", "PRIME_AGENT_KERNEL_PYTHON", "PRIME_AGENT_KERNEL_VENV"];
|
|
79
|
+
/**
|
|
80
|
+
* Env vars an executor must NEVER forward from its own environment.
|
|
81
|
+
*
|
|
82
|
+
* These re-point the self-modifying continual-harness store OUTSIDE the agent
|
|
83
|
+
* dir (agent-session.ts:8805 hands the kernel its store location), which
|
|
84
|
+
* defeats per-session isolation without changing anything an executor inspects.
|
|
85
|
+
* Isolation is the fail-closed default; sharing state is an explicit opt-in
|
|
86
|
+
* that names a directory, never an inherited env var.
|
|
87
|
+
*/
|
|
88
|
+
export declare const PRIME_HARNESS_STATE_ENV_DENYLIST: readonly ["RLM_GLOBAL_HARNESS_STATE_DIR", "RLM_HARNESS_STATE_DIR"];
|
|
89
|
+
/**
|
|
90
|
+
* A prime-agent IPython kernel that dies is never re-provisioned headlessly —
|
|
91
|
+
* every later `ipython` call fails with one of these markers until the process
|
|
92
|
+
* is replaced. An executor that cannot recognize them either reports a generic
|
|
93
|
+
* turn failure or burns its whole timeout against a permanently dead tool.
|
|
94
|
+
*
|
|
95
|
+
* Verified in `core/kernel/index.ts`: :723 ("forked kernel exited
|
|
96
|
+
* unexpectedly"), :750 ("Kernel exited before resolving ports"), :779 ("Kernel
|
|
97
|
+
* exited during startup"), :820/:837/:1172/:1295 ("Kernel has been shut down").
|
|
98
|
+
*/
|
|
99
|
+
export declare const PRIME_KERNEL_DEAD_MARKERS: readonly string[];
|
|
100
|
+
/** The first kernel-death marker present in `text`, or undefined. */
|
|
101
|
+
export declare function matchPrimeKernelDeadMarker(text: string): string | undefined;
|
|
102
|
+
/**
|
|
103
|
+
* `--thinking` rungs the fork's argument parser accepts
|
|
104
|
+
* (cli/args.ts:62 `VALID_THINKING_LEVELS`, rejected at :175). Read from the
|
|
105
|
+
* fork itself rather than borrowed from upstream pi's measured table: the two
|
|
106
|
+
* agreeing today is a coincidence this module must not depend on.
|
|
107
|
+
*/
|
|
108
|
+
export declare const PRIME_THINKING_LEVELS: readonly ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
109
|
+
export type PrimeThinkingLevel = (typeof PRIME_THINKING_LEVELS)[number];
|
|
110
|
+
/**
|
|
111
|
+
* Map the portable reasoning ladder onto the fork's `--thinking` value.
|
|
112
|
+
*
|
|
113
|
+
* Throws on an effort with no rung instead of returning undefined: omitting the
|
|
114
|
+
* flag runs the agent at its default thinking level while the caller believes
|
|
115
|
+
* the requested effort is in force, which is a silently dropped control.
|
|
116
|
+
*/
|
|
117
|
+
export declare function primeThinkingLevel(effort: string | null | undefined): PrimeThinkingLevel | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* Why `profile.mcp` (and any hub connection that resolves to MCP) is refused.
|
|
120
|
+
*
|
|
121
|
+
* The fork's `mcpServers` settings surface IS read at startup
|
|
122
|
+
* (settings-manager.ts:98-120,219) but a user-declared server never becomes
|
|
123
|
+
* model-visible: the host McpManager registers OAuth providers and serves
|
|
124
|
+
* mcp.config/refresh host-requests only (core/mcp/mcp-manager.ts:62-124),
|
|
125
|
+
* skips stdio servers entirely (mcp-manager.ts:73), and nothing generates a
|
|
126
|
+
* skill or prompt section for one. Model-visible MCP is exactly the built-in
|
|
127
|
+
* catalog skills, which are hand-written Python `McpIntegration` subclasses
|
|
128
|
+
* (prime-agent-runtime/src/rlm/mcp_base.py:112-125; only linear and notion
|
|
129
|
+
* ship). Materializing a per-run entry would run the agent WITHOUT the
|
|
130
|
+
* requested tools while the config on disk says otherwise.
|
|
131
|
+
*/
|
|
132
|
+
export declare const PRIME_MCP_UNSUPPORTED_REASON: string;
|
|
133
|
+
/** Which half of a profile refusal this is: a fork limit, or a bad declaration. */
|
|
134
|
+
export type PrimeProfileErrorKind = "unsupported" | "invalid";
|
|
135
|
+
/**
|
|
136
|
+
* Refusal raised by the shared prime lowering, carrying the control and the
|
|
137
|
+
* reason separately so each executor can render them in its own error contract
|
|
138
|
+
* (an HTTP backend's typed `BackendError`, an SDK adapter's plain `Error`)
|
|
139
|
+
* without the reason text — which is where the fork evidence lives — being
|
|
140
|
+
* rewritten per repository.
|
|
141
|
+
*/
|
|
142
|
+
export declare class PrimeProfileError extends Error {
|
|
143
|
+
readonly kind: PrimeProfileErrorKind;
|
|
144
|
+
readonly control: string;
|
|
145
|
+
readonly reason: string;
|
|
146
|
+
constructor(kind: PrimeProfileErrorKind, control: string, reason: string);
|
|
147
|
+
}
|
|
148
|
+
/** Raise the "the fork cannot honor this control" half of the refusal. */
|
|
149
|
+
export declare function rejectPrimeControl(control: string, reason: string): never;
|
|
150
|
+
/** Raise the "this declaration is malformed" half of the refusal. */
|
|
151
|
+
export declare function rejectPrimeDeclaration(control: string, reason: string): never;
|
|
152
|
+
/**
|
|
153
|
+
* The fork's continual-harness store exists but no longer parses.
|
|
154
|
+
*
|
|
155
|
+
* The store is the agent's own self-modification state — refinements, learned
|
|
156
|
+
* prompts, subagents it created mid-session — so an executor that reseeds it
|
|
157
|
+
* from empty destroys evidence and silently mutates the thing under study. The
|
|
158
|
+
* turn is failed instead; the file is the operator's to repair or remove.
|
|
159
|
+
*/
|
|
160
|
+
export declare class PrimeHarnessStateError extends Error {
|
|
161
|
+
readonly statePath: string;
|
|
162
|
+
constructor(statePath: string, detail: string);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Strip `//` line comments and trailing commas, leaving string literals
|
|
166
|
+
* untouched — byte-for-byte the fork's own `stripJsonComments`
|
|
167
|
+
* (core/model-registry.ts), so a models.json the agent accepts is never refused
|
|
168
|
+
* by an executor and vice versa.
|
|
169
|
+
*/
|
|
170
|
+
export declare function stripPrimeJsonComments(input: string): string;
|
|
171
|
+
/**
|
|
172
|
+
* Parse a models.json with the fork's own laxness and assert the one structural
|
|
173
|
+
* invariant it requires (a `providers` object). Throws naming `sourcePath`.
|
|
174
|
+
*
|
|
175
|
+
* This is the SHAPE and DIALECT only. Producing the file is deliberately not
|
|
176
|
+
* shared: an executor holding real operator credentials copies an
|
|
177
|
+
* operator-authored file and forwards only the env var names its `apiKey`
|
|
178
|
+
* entries reference, while an in-sandbox executor synthesizes a one-provider
|
|
179
|
+
* entry with a literal key that is a proxy dummy. Same schema, opposite threat
|
|
180
|
+
* models.
|
|
181
|
+
*/
|
|
182
|
+
export declare function parsePrimeModelsJson(text: string, sourcePath: string): Record<string, unknown>;
|
|
183
|
+
/**
|
|
184
|
+
* Environment variables a models.json names as `apiKey` sources.
|
|
185
|
+
*
|
|
186
|
+
* prime resolves an `apiKey` value env-var first and LITERAL second
|
|
187
|
+
* (core/resolve-config-value.ts), so an executor that forwards exactly the
|
|
188
|
+
* named variables lets an operator keep secrets out of the file — and an
|
|
189
|
+
* executor that forwards NONE of them silently turns the variable name itself
|
|
190
|
+
* into the credential string.
|
|
191
|
+
*/
|
|
192
|
+
export declare function primeApiKeyEnvNames(modelsConfig: Record<string, unknown>): string[];
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fork FACTS for prime-agent (PrimeIntellect-ai/prime-agent) — the constants,
|
|
3
|
+
* dialects and refusal reasons every executor that drives the fork needs, with
|
|
4
|
+
* no `node:fs` and no executor assumptions. The agent-dir MATERIALIZATION that
|
|
5
|
+
* consumes them lives in `./index.ts`; both consumers (cli-bridge's host-spawn
|
|
6
|
+
* backend on `--mode rpc`, sdk-provider-prime's in-sandbox adapter on
|
|
7
|
+
* `--mode acp`) read these instead of each keeping a private copy, because a
|
|
8
|
+
* fork bump must move one file, not two repositories.
|
|
9
|
+
*
|
|
10
|
+
* Every value here is verified against the pinned clone named by
|
|
11
|
+
* {@link PRIME_FORK_PIN}; `tests/prime-fork-pin.test.ts` re-asserts them
|
|
12
|
+
* against that clone when it is present, so a fork bump fails loudly in one
|
|
13
|
+
* place.
|
|
14
|
+
*/
|
|
15
|
+
/** The prime-agent clone every constant in this module was read from. */
|
|
16
|
+
export const PRIME_FORK_PIN = {
|
|
17
|
+
repo: "PrimeIntellect-ai/prime-agent",
|
|
18
|
+
version: "0.7.0",
|
|
19
|
+
commit: "be9e2fa0",
|
|
20
|
+
/** Local clone used by the fork-pin test when it exists. */
|
|
21
|
+
clonePath: "/home/drew/bench-cache/prime-agent",
|
|
22
|
+
/** Package root inside the clone that the file:line citations are relative to. */
|
|
23
|
+
packageRoot: "packages/coding-agent/src",
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The fork's package identity, and why it cannot be installed from the npm
|
|
27
|
+
* registry.
|
|
28
|
+
*
|
|
29
|
+
* `@earendil-works/pi-coding-agent` on registry.npmjs.org serves the UPSTREAM
|
|
30
|
+
* pi line (0.84.x); the fork's 0.7.x versions were never published there. The
|
|
31
|
+
* two are wire-incompatible — different daemon schema revision, different
|
|
32
|
+
* config dir, different agent-dir env var (see
|
|
33
|
+
* {@link PRIME_AGENT_DIR_ENV_VARS}) — so `npm install -g` under that name
|
|
34
|
+
* yields an agent none of this lowering was verified against. The fork must be
|
|
35
|
+
* built from source and exposed under a `prime-agent` command name; its own
|
|
36
|
+
* `bin` entry is `pi` (fork package.json `bin`), which is exactly how the wrong
|
|
37
|
+
* binary ends up on `PATH` under the right-looking name.
|
|
38
|
+
*/
|
|
39
|
+
export const PRIME_FORK_INSTALL = {
|
|
40
|
+
npmName: "@earendil-works/pi-coding-agent",
|
|
41
|
+
/** The fork's own bin name — deliberately NOT the command to install it as. */
|
|
42
|
+
forkBinName: "pi",
|
|
43
|
+
/** Command name an executor's binary path is expected to resolve to. */
|
|
44
|
+
expectedCommandName: "prime-agent",
|
|
45
|
+
sourceRepo: "https://github.com/PrimeIntellect-ai/prime-agent",
|
|
46
|
+
/** Built entrypoint to expose as `prime-agent`. */
|
|
47
|
+
sourceEntrypoint: "packages/coding-agent/dist/bundle/cli.js",
|
|
48
|
+
reason: "registry.npmjs.org serves the upstream pi line under @earendil-works/pi-coding-agent " +
|
|
49
|
+
"(0.84.x); the prime-agent fork's 0.7.x versions do not exist there. Installing from " +
|
|
50
|
+
"the registry yields a wire-incompatible agent with a different daemon schema, a " +
|
|
51
|
+
"different config dir, and a different agent-dir env var. Build from source and expose " +
|
|
52
|
+
"packages/coding-agent/dist/bundle/cli.js as `prime-agent`.",
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Env vars the agent resolves its config dir from, most-specific first.
|
|
56
|
+
*
|
|
57
|
+
* The fork derives the name from its own package identity —
|
|
58
|
+
* `ENV_AGENT_DIR = \`${envPrefix}_CODING_AGENT_DIR\`` where `envPrefix` comes
|
|
59
|
+
* from `package.json` `piConfig.name` (config.ts:485-498) — so the fork reads
|
|
60
|
+
* `PRIME_AGENT_CODING_AGENT_DIR` and upstream pi reads `PI_CODING_AGENT_DIR`.
|
|
61
|
+
* An executor that sets only the fork's name has its isolation SILENTLY
|
|
62
|
+
* disabled when the binary on PATH turns out to be upstream pi: `getAgentDir()`
|
|
63
|
+
* falls back to `homedir()/<CONFIG_DIR_NAME>` (config.ts:520-527), one shared
|
|
64
|
+
* dir — and therefore one shared self-modifying `harness_state.json` — for
|
|
65
|
+
* every session on the box. Setting BOTH names costs nothing and keeps the
|
|
66
|
+
* isolation fail-closed under either binary.
|
|
67
|
+
*/
|
|
68
|
+
export const PRIME_AGENT_DIR_ENV_VARS = [
|
|
69
|
+
"PRIME_AGENT_CODING_AGENT_DIR",
|
|
70
|
+
"PI_CODING_AGENT_DIR",
|
|
71
|
+
];
|
|
72
|
+
/** Canonical agent-dir env var for the fork itself (config.ts:498). */
|
|
73
|
+
export const PRIME_AGENT_DIR_ENV_VAR = PRIME_AGENT_DIR_ENV_VARS[0];
|
|
74
|
+
/** Pin `agentDir` under every name the agent could resolve its config dir from. */
|
|
75
|
+
export function primeAgentDirEnv(agentDir) {
|
|
76
|
+
const env = {};
|
|
77
|
+
for (const name of PRIME_AGENT_DIR_ENV_VARS)
|
|
78
|
+
env[name] = agentDir;
|
|
79
|
+
return env;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Operator knobs an executor forwards into the agent when they are set.
|
|
83
|
+
*
|
|
84
|
+
* `PRIME_AGENT_KERNEL_VENV` is load-bearing for any executor that also pins
|
|
85
|
+
* HOME: the kernel venv defaults under `homedir()/.prime/agent/kernel-venv`
|
|
86
|
+
* (kernel/bootstrap.ts:331-342), so a fresh HOME re-bootstraps it through uv on
|
|
87
|
+
* every run unless the operator's venv path is forwarded
|
|
88
|
+
* (bootstrap.ts:340 override, :358 branch).
|
|
89
|
+
*/
|
|
90
|
+
export const PRIME_KERNEL_ENV_PASSTHROUGH = [
|
|
91
|
+
/** "1" lets a headless first run install uv itself; "0" forbids it. */
|
|
92
|
+
"PRIME_AGENT_INSTALL_UV",
|
|
93
|
+
/** Offline escape hatch: pin the kernel venv python instead of uv-provisioning. */
|
|
94
|
+
"PRIME_AGENT_KERNEL_PYTHON",
|
|
95
|
+
/** Reuse an operator-provisioned kernel venv instead of re-bootstrapping it. */
|
|
96
|
+
"PRIME_AGENT_KERNEL_VENV",
|
|
97
|
+
];
|
|
98
|
+
/**
|
|
99
|
+
* Env vars an executor must NEVER forward from its own environment.
|
|
100
|
+
*
|
|
101
|
+
* These re-point the self-modifying continual-harness store OUTSIDE the agent
|
|
102
|
+
* dir (agent-session.ts:8805 hands the kernel its store location), which
|
|
103
|
+
* defeats per-session isolation without changing anything an executor inspects.
|
|
104
|
+
* Isolation is the fail-closed default; sharing state is an explicit opt-in
|
|
105
|
+
* that names a directory, never an inherited env var.
|
|
106
|
+
*/
|
|
107
|
+
export const PRIME_HARNESS_STATE_ENV_DENYLIST = [
|
|
108
|
+
"RLM_GLOBAL_HARNESS_STATE_DIR",
|
|
109
|
+
"RLM_HARNESS_STATE_DIR",
|
|
110
|
+
];
|
|
111
|
+
/**
|
|
112
|
+
* A prime-agent IPython kernel that dies is never re-provisioned headlessly —
|
|
113
|
+
* every later `ipython` call fails with one of these markers until the process
|
|
114
|
+
* is replaced. An executor that cannot recognize them either reports a generic
|
|
115
|
+
* turn failure or burns its whole timeout against a permanently dead tool.
|
|
116
|
+
*
|
|
117
|
+
* Verified in `core/kernel/index.ts`: :723 ("forked kernel exited
|
|
118
|
+
* unexpectedly"), :750 ("Kernel exited before resolving ports"), :779 ("Kernel
|
|
119
|
+
* exited during startup"), :820/:837/:1172/:1295 ("Kernel has been shut down").
|
|
120
|
+
*/
|
|
121
|
+
export const PRIME_KERNEL_DEAD_MARKERS = [
|
|
122
|
+
"Kernel has been shut down",
|
|
123
|
+
"Kernel exited before resolving ports",
|
|
124
|
+
"Kernel exited during startup",
|
|
125
|
+
"forked kernel exited unexpectedly",
|
|
126
|
+
];
|
|
127
|
+
/** The first kernel-death marker present in `text`, or undefined. */
|
|
128
|
+
export function matchPrimeKernelDeadMarker(text) {
|
|
129
|
+
return PRIME_KERNEL_DEAD_MARKERS.find((marker) => text.includes(marker));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* `--thinking` rungs the fork's argument parser accepts
|
|
133
|
+
* (cli/args.ts:62 `VALID_THINKING_LEVELS`, rejected at :175). Read from the
|
|
134
|
+
* fork itself rather than borrowed from upstream pi's measured table: the two
|
|
135
|
+
* agreeing today is a coincidence this module must not depend on.
|
|
136
|
+
*/
|
|
137
|
+
export const PRIME_THINKING_LEVELS = [
|
|
138
|
+
"off",
|
|
139
|
+
"minimal",
|
|
140
|
+
"low",
|
|
141
|
+
"medium",
|
|
142
|
+
"high",
|
|
143
|
+
"xhigh",
|
|
144
|
+
"max",
|
|
145
|
+
];
|
|
146
|
+
/** Canonical portable effort → fork `--thinking` rung. */
|
|
147
|
+
const PRIME_THINKING_BY_EFFORT = {
|
|
148
|
+
none: "off",
|
|
149
|
+
minimal: "minimal",
|
|
150
|
+
low: "low",
|
|
151
|
+
medium: "medium",
|
|
152
|
+
high: "high",
|
|
153
|
+
xhigh: "xhigh",
|
|
154
|
+
// The fork carries `max` above `xhigh`, so the ceiling level reaches it.
|
|
155
|
+
ultracode: "max",
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Map the portable reasoning ladder onto the fork's `--thinking` value.
|
|
159
|
+
*
|
|
160
|
+
* Throws on an effort with no rung instead of returning undefined: omitting the
|
|
161
|
+
* flag runs the agent at its default thinking level while the caller believes
|
|
162
|
+
* the requested effort is in force, which is a silently dropped control.
|
|
163
|
+
*/
|
|
164
|
+
export function primeThinkingLevel(effort) {
|
|
165
|
+
if (effort === undefined || effort === null || effort === "")
|
|
166
|
+
return undefined;
|
|
167
|
+
const level = PRIME_THINKING_BY_EFFORT[effort];
|
|
168
|
+
if (level === undefined) {
|
|
169
|
+
throw new PrimeProfileError("invalid", "model.reasoningEffort", `prime-agent has no --thinking rung for ${JSON.stringify(effort)} ` +
|
|
170
|
+
`(fork cli/args.ts:62 accepts ${PRIME_THINKING_LEVELS.join(", ")}).`);
|
|
171
|
+
}
|
|
172
|
+
return level;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Why `profile.mcp` (and any hub connection that resolves to MCP) is refused.
|
|
176
|
+
*
|
|
177
|
+
* The fork's `mcpServers` settings surface IS read at startup
|
|
178
|
+
* (settings-manager.ts:98-120,219) but a user-declared server never becomes
|
|
179
|
+
* model-visible: the host McpManager registers OAuth providers and serves
|
|
180
|
+
* mcp.config/refresh host-requests only (core/mcp/mcp-manager.ts:62-124),
|
|
181
|
+
* skips stdio servers entirely (mcp-manager.ts:73), and nothing generates a
|
|
182
|
+
* skill or prompt section for one. Model-visible MCP is exactly the built-in
|
|
183
|
+
* catalog skills, which are hand-written Python `McpIntegration` subclasses
|
|
184
|
+
* (prime-agent-runtime/src/rlm/mcp_base.py:112-125; only linear and notion
|
|
185
|
+
* ship). Materializing a per-run entry would run the agent WITHOUT the
|
|
186
|
+
* requested tools while the config on disk says otherwise.
|
|
187
|
+
*/
|
|
188
|
+
export const PRIME_MCP_UNSUPPORTED_REASON = "a user-declared mcpServers entry is read from the fork's settings.json at startup but never " +
|
|
189
|
+
"becomes model-visible: the host only registers OAuth/config for it (fork core/mcp/" +
|
|
190
|
+
"mcp-manager.ts:62-124), skips stdio servers entirely (mcp-manager.ts:73), and generates no " +
|
|
191
|
+
"skill or prompt section for it. Model-visible MCP is exactly the built-in Python " +
|
|
192
|
+
"McpIntegration catalog skills (prime-agent-runtime/src/rlm/mcp_base.py:112-125), which no " +
|
|
193
|
+
"per-run configuration creates — so materializing it would be a silently dropped control.";
|
|
194
|
+
/**
|
|
195
|
+
* Refusal raised by the shared prime lowering, carrying the control and the
|
|
196
|
+
* reason separately so each executor can render them in its own error contract
|
|
197
|
+
* (an HTTP backend's typed `BackendError`, an SDK adapter's plain `Error`)
|
|
198
|
+
* without the reason text — which is where the fork evidence lives — being
|
|
199
|
+
* rewritten per repository.
|
|
200
|
+
*/
|
|
201
|
+
export class PrimeProfileError extends Error {
|
|
202
|
+
kind;
|
|
203
|
+
control;
|
|
204
|
+
reason;
|
|
205
|
+
constructor(kind, control, reason) {
|
|
206
|
+
super(kind === "unsupported"
|
|
207
|
+
? `prime does not support AgentProfile ${control}: ${reason}`
|
|
208
|
+
: `prime cannot accept AgentProfile ${control}: ${reason}`);
|
|
209
|
+
this.name = "PrimeProfileError";
|
|
210
|
+
this.kind = kind;
|
|
211
|
+
this.control = control;
|
|
212
|
+
this.reason = reason;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** Raise the "the fork cannot honor this control" half of the refusal. */
|
|
216
|
+
export function rejectPrimeControl(control, reason) {
|
|
217
|
+
throw new PrimeProfileError("unsupported", control, reason);
|
|
218
|
+
}
|
|
219
|
+
/** Raise the "this declaration is malformed" half of the refusal. */
|
|
220
|
+
export function rejectPrimeDeclaration(control, reason) {
|
|
221
|
+
throw new PrimeProfileError("invalid", control, reason);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* The fork's continual-harness store exists but no longer parses.
|
|
225
|
+
*
|
|
226
|
+
* The store is the agent's own self-modification state — refinements, learned
|
|
227
|
+
* prompts, subagents it created mid-session — so an executor that reseeds it
|
|
228
|
+
* from empty destroys evidence and silently mutates the thing under study. The
|
|
229
|
+
* turn is failed instead; the file is the operator's to repair or remove.
|
|
230
|
+
*/
|
|
231
|
+
export class PrimeHarnessStateError extends Error {
|
|
232
|
+
statePath;
|
|
233
|
+
constructor(statePath, detail) {
|
|
234
|
+
super(`prime harness state ${statePath} is not valid JSON and may hold agent self-modification ` +
|
|
235
|
+
`state; refusing to overwrite it — repair or remove the file to continue (${detail})`);
|
|
236
|
+
this.name = "PrimeHarnessStateError";
|
|
237
|
+
this.statePath = statePath;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Strip `//` line comments and trailing commas, leaving string literals
|
|
242
|
+
* untouched — byte-for-byte the fork's own `stripJsonComments`
|
|
243
|
+
* (core/model-registry.ts), so a models.json the agent accepts is never refused
|
|
244
|
+
* by an executor and vice versa.
|
|
245
|
+
*/
|
|
246
|
+
export function stripPrimeJsonComments(input) {
|
|
247
|
+
return input
|
|
248
|
+
.replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*/g, (m) => (m[0] === '"' ? m : ""))
|
|
249
|
+
.replace(/"(?:\\.|[^"\\])*"|,(\s*[}\]])/g, (m, tail) => tail !== undefined ? tail : m[0] === '"' ? m : "");
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Parse a models.json with the fork's own laxness and assert the one structural
|
|
253
|
+
* invariant it requires (a `providers` object). Throws naming `sourcePath`.
|
|
254
|
+
*
|
|
255
|
+
* This is the SHAPE and DIALECT only. Producing the file is deliberately not
|
|
256
|
+
* shared: an executor holding real operator credentials copies an
|
|
257
|
+
* operator-authored file and forwards only the env var names its `apiKey`
|
|
258
|
+
* entries reference, while an in-sandbox executor synthesizes a one-provider
|
|
259
|
+
* entry with a literal key that is a proxy dummy. Same schema, opposite threat
|
|
260
|
+
* models.
|
|
261
|
+
*/
|
|
262
|
+
export function parsePrimeModelsJson(text, sourcePath) {
|
|
263
|
+
let parsed;
|
|
264
|
+
try {
|
|
265
|
+
parsed = JSON.parse(stripPrimeJsonComments(text));
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
throw new Error(`prime models.json ${sourcePath} is not valid JSON: ${err.message}`);
|
|
269
|
+
}
|
|
270
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
271
|
+
throw new Error(`prime models.json ${sourcePath} must be a JSON object`);
|
|
272
|
+
}
|
|
273
|
+
const providers = parsed.providers;
|
|
274
|
+
if (!providers || typeof providers !== "object" || Array.isArray(providers)) {
|
|
275
|
+
throw new Error(`prime models.json ${sourcePath} must carry a "providers" object (prime's models.json schema)`);
|
|
276
|
+
}
|
|
277
|
+
return parsed;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Environment variables a models.json names as `apiKey` sources.
|
|
281
|
+
*
|
|
282
|
+
* prime resolves an `apiKey` value env-var first and LITERAL second
|
|
283
|
+
* (core/resolve-config-value.ts), so an executor that forwards exactly the
|
|
284
|
+
* named variables lets an operator keep secrets out of the file — and an
|
|
285
|
+
* executor that forwards NONE of them silently turns the variable name itself
|
|
286
|
+
* into the credential string.
|
|
287
|
+
*/
|
|
288
|
+
export function primeApiKeyEnvNames(modelsConfig) {
|
|
289
|
+
const names = [];
|
|
290
|
+
const providers = modelsConfig.providers;
|
|
291
|
+
if (!providers || typeof providers !== "object")
|
|
292
|
+
return names;
|
|
293
|
+
for (const provider of Object.values(providers)) {
|
|
294
|
+
if (!provider || typeof provider !== "object")
|
|
295
|
+
continue;
|
|
296
|
+
const key = provider.apiKey;
|
|
297
|
+
if (typeof key !== "string" || !/^[A-Z_][A-Z0-9_]*$/.test(key))
|
|
298
|
+
continue;
|
|
299
|
+
if (!names.includes(key))
|
|
300
|
+
names.push(key);
|
|
301
|
+
}
|
|
302
|
+
return names;
|
|
303
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-profile-materialize",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Materialize an AgentProfile into the native per-harness workspace files (the one shared materializer for cli-bridge, agent-dev-container, and VB).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|