@tangle-network/agent-interface 0.43.1 → 0.45.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.
@@ -1,9 +1,12 @@
1
1
  /**
2
- * The 29 canonical AgentProfile leaves that can affect one execution.
2
+ * The 30 canonical AgentProfile leaves that can affect one execution.
3
3
  *
4
4
  * Compound parents such as `model`, `prompt`, and `resources` are deliberately
5
5
  * absent. A producer must report the exact requested leaf instead of claiming
6
- * a parent while silently dropping one of its children.
6
+ * a parent while silently dropping one of its children. `systemPrompt` and
7
+ * `appendSystemPrompt` are separate leaves for the same reason: a backend that
8
+ * can only add text must report the requested replacement as unsatisfied rather
9
+ * than acknowledge it.
7
10
  */
8
11
  export const AGENT_PROFILE_MATERIALIZATION_AXES = [
9
12
  "name",
@@ -11,6 +14,7 @@ export const AGENT_PROFILE_MATERIALIZATION_AXES = [
11
14
  "version",
12
15
  "tags",
13
16
  "systemPrompt",
17
+ "appendSystemPrompt",
14
18
  "instructions",
15
19
  "modelDefault",
16
20
  "modelSmall",
@@ -68,6 +72,11 @@ const AXIS_DESCRIPTORS = [
68
72
  rootPath: "/prompt/systemPrompt",
69
73
  value: (profile) => profile.prompt?.systemPrompt,
70
74
  },
75
+ {
76
+ axis: "appendSystemPrompt",
77
+ rootPath: "/prompt/appendSystemPrompt",
78
+ value: (profile) => profile.prompt?.appendSystemPrompt,
79
+ },
71
80
  {
72
81
  axis: "instructions",
73
82
  rootPath: "/prompt/instructions",
@@ -131,14 +131,58 @@ export interface AgentProfileModelHints {
131
131
  }
132
132
  /**
133
133
  * Prompt shaping for an agent.
134
+ *
135
+ * Replacement and addition are two different intents against the same channel
136
+ * and are never interchangeable. A backend that can only do one of them must
137
+ * refuse the other rather than substituting it, which is why
138
+ * {@link AgentProfileCapabilities.systemPrompt} carries a separate bit for each.
139
+ *
140
+ * Setting `systemPrompt` and `appendSystemPrompt` together is legal and ordered:
141
+ * the replacement is installed first and the addition composes on top of it, so
142
+ * the effective prompt is `systemPrompt` then `appendSystemPrompt`. The pair is
143
+ * deliberately allowed because {@link mergeAgentProfiles} composes the two
144
+ * fields independently — refusing it would let two individually valid profiles
145
+ * merge into an invalid one.
134
146
  */
135
147
  export interface AgentProfilePrompt {
136
148
  /**
137
- * Full system prompt replacement, when supported.
149
+ * REPLACE the harness's own system prompt with this text.
150
+ *
151
+ * The harness's built-in prompt is DELETED, not extended: the model stops
152
+ * receiving the tool descriptions, output conventions, refusal rules, and
153
+ * workflow scaffolding it was tuned against, so behavior can move far beyond
154
+ * the words written here. Supply a prompt that stands on its own.
155
+ *
156
+ * Honored only where {@link AgentProfileSystemPromptCapability.replace} is
157
+ * true — a harness that exposes a real replacement control (`pi
158
+ * --system-prompt <file>` with context files, skills, and prompt templates
159
+ * off; gemini `.gemini/system.md` with `GEMINI_SYSTEM_MD=1`). A backend that
160
+ * can only add text must reject this field. Folding it into an addition is a
161
+ * silent semantic downgrade: the instructions the caller asked to delete stay
162
+ * in force, and nothing in the result says so.
138
163
  */
139
164
  systemPrompt?: string;
140
165
  /**
141
- * Additional instruction lines appended to the active prompt.
166
+ * ADD this text to the harness's own system prompt, which stays intact.
167
+ *
168
+ * The model keeps everything it was tuned against and receives this on top,
169
+ * in the same privileged position as the system prompt. Maps to claude-code
170
+ * `--append-system-prompt`, and to a leading `role: "system"` message on
171
+ * harnesses that take a message list.
172
+ *
173
+ * Distinct from {@link AgentProfilePrompt.instructions}, which harnesses
174
+ * materialize into their lower-privilege project-instruction surface.
175
+ *
176
+ * Honored only where {@link AgentProfileSystemPromptCapability.append} is
177
+ * true.
178
+ */
179
+ appendSystemPrompt?: string;
180
+ /**
181
+ * Additional instruction lines composed into the agent's project-instruction
182
+ * surface — the harness's `AGENTS.md` / `CLAUDE.md`-style files or its own
183
+ * caller-instruction preamble. Lower privilege than
184
+ * {@link AgentProfilePrompt.appendSystemPrompt} and placed wherever the
185
+ * harness keeps caller instructions rather than in the system prompt.
142
186
  */
143
187
  instructions?: string[];
144
188
  }
@@ -326,12 +370,36 @@ export interface AgentProfile {
326
370
  * Helper for declaring typed profiles in application code.
327
371
  */
328
372
  export declare function defineAgentProfile<T extends AgentProfile>(profile: T): T;
373
+ /**
374
+ * What a backend can do to the harness's system prompt.
375
+ *
376
+ * Two independent bits, because most harnesses can do exactly one of them. A
377
+ * single boolean cannot separate "I delete the built-in prompt and install
378
+ * yours" from "I keep the built-in prompt and add yours to it", so a caller
379
+ * reading it has no way to tell whether a requested replacement will actually
380
+ * happen. Neither bit implies the other: declare each from what the backend's
381
+ * materialization really does, not from whether it accepts the field.
382
+ */
383
+ export interface AgentProfileSystemPromptCapability {
384
+ /**
385
+ * The backend honors {@link AgentProfilePrompt.systemPrompt} by deleting the
386
+ * harness's own system prompt and installing the caller's. `false` means a
387
+ * profile carrying `systemPrompt` must be REFUSED — never quietly added to
388
+ * the built-in prompt instead.
389
+ */
390
+ replace: boolean;
391
+ /**
392
+ * The backend honors {@link AgentProfilePrompt.appendSystemPrompt} by keeping
393
+ * the harness's own system prompt and adding the caller's text to it.
394
+ */
395
+ append: boolean;
396
+ }
329
397
  /**
330
398
  * Capabilities describing how a backend interprets AgentProfile.
331
399
  */
332
400
  export interface AgentProfileCapabilities {
333
401
  namedProfiles: boolean;
334
- systemPrompt: boolean;
402
+ systemPrompt: AgentProfileSystemPromptCapability;
335
403
  instructions: boolean;
336
404
  tools: boolean;
337
405
  permissions: boolean;
@@ -374,7 +442,9 @@ export interface AgentProfileValidationResult {
374
442
  /**
375
443
  * Merge two public AgentProfile values.
376
444
  *
377
- * Overlay fields win on conflicts. Array-like instruction sets are appended.
445
+ * Overlay fields win on conflicts. Additive fields compose instead: array-like
446
+ * instruction sets are concatenated, and `prompt.appendSystemPrompt` values are
447
+ * joined base-first with a blank line between them.
378
448
  */
379
449
  export declare function mergeAgentProfiles(base: AgentProfile | undefined, overlay: AgentProfile | undefined): AgentProfile | undefined;
380
450
  export {};
@@ -72,6 +72,19 @@ function mergeStringArrays(base, overlay) {
72
72
  return undefined;
73
73
  return [...(base ?? []), ...(overlay ?? [])];
74
74
  }
75
+ /**
76
+ * Additive prompt text composes instead of overwriting: an overlay that adds
77
+ * one line must not delete what the base added. `systemPrompt` keeps
78
+ * overlay-wins semantics because two replacements cannot both apply, while two
79
+ * additions always can. An explicitly empty addition contributes no separator.
80
+ */
81
+ function mergeAppendedSystemPrompts(base, overlay) {
82
+ if (base === undefined || base === "")
83
+ return overlay ?? base;
84
+ if (overlay === undefined || overlay === "")
85
+ return base;
86
+ return `${base}\n\n${overlay}`;
87
+ }
75
88
  function mergeRecord(base, overlay) {
76
89
  if (!base && !overlay)
77
90
  return undefined;
@@ -88,7 +101,9 @@ function mergeOptionalArrays(base, overlay) {
88
101
  /**
89
102
  * Merge two public AgentProfile values.
90
103
  *
91
- * Overlay fields win on conflicts. Array-like instruction sets are appended.
104
+ * Overlay fields win on conflicts. Additive fields compose instead: array-like
105
+ * instruction sets are concatenated, and `prompt.appendSystemPrompt` values are
106
+ * joined base-first with a blank line between them.
92
107
  */
93
108
  export function mergeAgentProfiles(base, overlay) {
94
109
  if (!base && !overlay)
@@ -97,6 +112,7 @@ export function mergeAgentProfiles(base, overlay) {
97
112
  ? {
98
113
  ...(base?.prompt ?? {}),
99
114
  ...(overlay?.prompt ?? {}),
115
+ appendSystemPrompt: mergeAppendedSystemPrompts(base?.prompt?.appendSystemPrompt, overlay?.prompt?.appendSystemPrompt),
100
116
  instructions: mergeStringArrays(base?.prompt?.instructions, overlay?.prompt?.instructions),
101
117
  }
102
118
  : undefined;
@@ -617,7 +617,10 @@ export interface AgentEnvironmentCapabilities {
617
617
  export declare const AgentEnvironmentCapabilitiesSchema: z.ZodObject<{
618
618
  profile: z.ZodObject<{
619
619
  namedProfiles: z.ZodBoolean;
620
- systemPrompt: z.ZodBoolean;
620
+ systemPrompt: z.ZodObject<{
621
+ replace: z.ZodBoolean;
622
+ append: z.ZodBoolean;
623
+ }, z.core.$strict>;
621
624
  instructions: z.ZodBoolean;
622
625
  tools: z.ZodBoolean;
623
626
  permissions: z.ZodBoolean;
@@ -7,7 +7,15 @@ import { InteractionCapabilitiesSchema, } from "./interaction.js";
7
7
  */
8
8
  const AgentProfileCapabilitiesSchema = z.strictObject({
9
9
  namedProfiles: z.boolean(),
10
- systemPrompt: z.boolean(),
10
+ /*
11
+ * Both bits are required with no default: a document that omits either one,
12
+ * or sends a bare boolean, fails validation instead of being read as
13
+ * replacement-supported, which for every append-only backend is false.
14
+ */
15
+ systemPrompt: z.strictObject({
16
+ replace: z.boolean(),
17
+ append: z.boolean(),
18
+ }),
11
19
  instructions: z.boolean(),
12
20
  tools: z.boolean(),
13
21
  permissions: z.boolean(),
@@ -1,11 +1,12 @@
1
- import { type ReasoningEffort } from "./agent-profile.js";
1
+ import { type AgentProfileSystemPromptCapability, type ReasoningEffort } from "./agent-profile.js";
2
2
  import type { HarnessType } from "./harness.js";
3
3
  /**
4
4
  * The unified harness capability layer — the single source of truth for:
5
- * 1. harness ↔ model compatibility (which models a harness can run), and
6
- * 2. reasoning-effort support (which thinking levels a harness/model expresses).
5
+ * 1. harness ↔ model compatibility (which models a harness can run),
6
+ * 2. reasoning-effort support (which thinking levels a harness/model expresses), and
7
+ * 3. system-prompt intents (whether a harness can replace its own prompt, add to it, or neither).
7
8
  *
8
- * Both are facets of the same question — "what can this (harness, model) pair actually do" — and
9
+ * All are facets of the same question — "what can this (harness, model) pair actually do" — and
9
10
  * apply to BOTH harness-backed systems (vendor-locked CLIs like claude-code/codex/kimi) AND
10
11
  * router-backed systems (opencode, cli-base: any model the router serves). Lifted here so the
11
12
  * cli-bridge backends, the sandbox UI pickers, and the router all read one truth instead of each
@@ -66,3 +67,31 @@ export declare function harnessHonorsModel(harness: HarnessType): boolean;
66
67
  export declare function harnessHonorsEffort(harness: HarnessType): boolean;
67
68
  /** Whether the harness honors BOTH chat selectors — i.e. the model and effort pickers are live. */
68
69
  export declare function harnessHonorsSelectors(harness: HarnessType): boolean;
70
+ /**
71
+ * Which system-prompt intents a harness honors THROUGH A WORKSPACE — the value an adapter that
72
+ * lowers a profile to files, env vars, and CLI flags and then hands the result to a launcher it
73
+ * does not own should declare as {@link AgentProfileCapabilities.systemPrompt}. That is the shape
74
+ * of every caller today (the cli-bridge and tangle providers both forward a plan), which is why
75
+ * this answer depends on the harness alone.
76
+ *
77
+ * It is NOT the whole truth for an adapter that starts the harness itself, because one control in
78
+ * the table above lives outside any workspace: opencode's `agent.<name>.prompt` really does replace
79
+ * its built-in prompt, but it binds to the single agent whoever starts the server selects. A plan
80
+ * cannot name that agent, so `opencode` reads `replace: false` here — while an adapter that writes
81
+ * opencode's server config AND picks the primary agent (`sdk-provider-opencode`) does honor
82
+ * replacement, and declares `replace: true` for itself. The capability is a property of the
83
+ * (harness, executor) pair; this function answers it for the plan-forwarding executor.
84
+ *
85
+ * Do not widen the table to close that gap: a harness-keyed `true` would promise the intent to
86
+ * every plan-forwarding caller, and those callers cannot deliver it. An executor that owns a
87
+ * launcher control states so where it binds it — `materializeProfile`'s `binds` option in
88
+ * `@tangle-network/agent-profile-materialize`, which turns the plan's refusal into a binding that
89
+ * executor must then apply.
90
+ *
91
+ * Pass `undefined` when the harness is not known at declaration time: the answer is then
92
+ * `{ replace: false, append: false }`, because an adapter that cannot name its harness cannot
93
+ * promise either intent, and `false` means "refuse" rather than "silently substitute the other".
94
+ * An adapter that forwards a profile to some other layer must still declare what that layer's
95
+ * harness really does — being able to put the field on the wire is not the same as honoring it.
96
+ */
97
+ export declare function harnessSystemPromptIntents(harness: HarnessType | undefined): AgentProfileSystemPromptCapability;
@@ -1,10 +1,11 @@
1
1
  import { REASONING_EFFORTS, } from "./agent-profile.js";
2
2
  /**
3
3
  * The unified harness capability layer — the single source of truth for:
4
- * 1. harness ↔ model compatibility (which models a harness can run), and
5
- * 2. reasoning-effort support (which thinking levels a harness/model expresses).
4
+ * 1. harness ↔ model compatibility (which models a harness can run),
5
+ * 2. reasoning-effort support (which thinking levels a harness/model expresses), and
6
+ * 3. system-prompt intents (whether a harness can replace its own prompt, add to it, or neither).
6
7
  *
7
- * Both are facets of the same question — "what can this (harness, model) pair actually do" — and
8
+ * All are facets of the same question — "what can this (harness, model) pair actually do" — and
8
9
  * apply to BOTH harness-backed systems (vendor-locked CLIs like claude-code/codex/kimi) AND
9
10
  * router-backed systems (opencode, cli-base: any model the router serves). Lifted here so the
10
11
  * cli-bridge backends, the sandbox UI pickers, and the router all read one truth instead of each
@@ -124,6 +125,9 @@ export function snapHarnessToModel(harness, modelId) {
124
125
  * and silently replaced with the default rather than rejected — so the set must not overstate.
125
126
  * - pi: `--thinking` accepts `off|minimal|low|medium|high|xhigh|max`; canonical `none` maps to
126
127
  * `off` and `ultracode` to `max`.
128
+ * - prime: the prime fork of the pi line accepts the same `--thinking` set
129
+ * (`off|minimal|low|medium|high|xhigh|max`); canonical `none` maps to `off` and `ultracode` to
130
+ * `max`.
127
131
  * - openclaw: `--thinking` accepts `off|minimal|low|medium|high|xhigh|max` (and `adaptive`, which
128
132
  * defers the choice rather than naming a rung); canonical `none` maps to `off` and `ultracode`
129
133
  * to `max`.
@@ -134,6 +138,7 @@ const harnessReasoningEffortsOverride = {
134
138
  codex: ["none", "minimal", "low", "medium", "high", "xhigh", "ultracode"],
135
139
  "claude-code": ["low", "medium", "high", "xhigh", "ultracode"],
136
140
  pi: ["none", "minimal", "low", "medium", "high", "xhigh", "ultracode"],
141
+ prime: ["none", "minimal", "low", "medium", "high", "xhigh", "ultracode"],
137
142
  openclaw: [
138
143
  "none",
139
144
  "minimal",
@@ -219,3 +224,73 @@ export function harnessHonorsEffort(harness) {
219
224
  export function harnessHonorsSelectors(harness) {
220
225
  return harnessHonorsModel(harness) && harnessHonorsEffort(harness);
221
226
  }
227
+ // ── System-prompt intents (which prompt channel the harness actually owns) ────
228
+ /**
229
+ * The system-prompt intents a harness's own controls can execute, measured by reading the request
230
+ * each installed CLI sends — NOT taken from its help text:
231
+ *
232
+ * - claude-code 2.1.222 and pi 0.83.0 own both. `--system-prompt` drops the built-in prompt from
233
+ * the request (27,673 B → the caller's bytes on claude-code, 2,582 B → the caller's on pi);
234
+ * `--append-system-prompt` leaves it in place and adds the caller's text after it.
235
+ * - codex 0.146.0 owns replacement only: the `model_instructions_file` config key becomes the
236
+ * request's entire instructions text. It has no additive control — its AGENTS.md lands in a
237
+ * developer/user message, not the system channel.
238
+ * - gemini 0.26.0 owns replacement only: `.gemini/system.md` under `GEMINI_SYSTEM_MD=1` replaces
239
+ * the base prompt. Its one additive path is GEMINI.md memory, which IS the `instructions`
240
+ * surface, so an addition lowered there would be byte-indistinguishable from `instructions`.
241
+ * - opencode 1.17.18 owns addition only THROUGH A WORKSPACE: config-declared `instructions[]`
242
+ * files compose into the same single `role: "system"` message as its built-in prompt, which
243
+ * stays in place. Its replacement control (`agent.<name>.prompt`) binds to one agent chosen at
244
+ * launch, which a workspace plan cannot guarantee — but an executor that selects that agent
245
+ * can, so `replace: false` here is the plan-forwarding answer, not opencode's ceiling.
246
+ *
247
+ * Every other harness owns NEITHER, including the ones whose prompt path is a `role: "system"` chat
248
+ * message: that message is flattened into the user turn before the CLI sees it, so it is not a
249
+ * system-prompt channel at all — honoring an intent through it would put the caller's text in
250
+ * ordinary user content while the harness's own prompt ran unchanged. A harness with no entry
251
+ * refuses both, so one added later cannot inherit a capability by omission.
252
+ */
253
+ const harnessSystemPromptControls = {
254
+ "claude-code": { replace: true, append: true },
255
+ pi: { replace: true, append: true },
256
+ prime: { replace: true, append: true },
257
+ codex: { replace: true, append: false },
258
+ gemini: { replace: true, append: false },
259
+ opencode: { replace: false, append: true },
260
+ };
261
+ const noSystemPromptControls = {
262
+ replace: false,
263
+ append: false,
264
+ };
265
+ /**
266
+ * Which system-prompt intents a harness honors THROUGH A WORKSPACE — the value an adapter that
267
+ * lowers a profile to files, env vars, and CLI flags and then hands the result to a launcher it
268
+ * does not own should declare as {@link AgentProfileCapabilities.systemPrompt}. That is the shape
269
+ * of every caller today (the cli-bridge and tangle providers both forward a plan), which is why
270
+ * this answer depends on the harness alone.
271
+ *
272
+ * It is NOT the whole truth for an adapter that starts the harness itself, because one control in
273
+ * the table above lives outside any workspace: opencode's `agent.<name>.prompt` really does replace
274
+ * its built-in prompt, but it binds to the single agent whoever starts the server selects. A plan
275
+ * cannot name that agent, so `opencode` reads `replace: false` here — while an adapter that writes
276
+ * opencode's server config AND picks the primary agent (`sdk-provider-opencode`) does honor
277
+ * replacement, and declares `replace: true` for itself. The capability is a property of the
278
+ * (harness, executor) pair; this function answers it for the plan-forwarding executor.
279
+ *
280
+ * Do not widen the table to close that gap: a harness-keyed `true` would promise the intent to
281
+ * every plan-forwarding caller, and those callers cannot deliver it. An executor that owns a
282
+ * launcher control states so where it binds it — `materializeProfile`'s `binds` option in
283
+ * `@tangle-network/agent-profile-materialize`, which turns the plan's refusal into a binding that
284
+ * executor must then apply.
285
+ *
286
+ * Pass `undefined` when the harness is not known at declaration time: the answer is then
287
+ * `{ replace: false, append: false }`, because an adapter that cannot name its harness cannot
288
+ * promise either intent, and `false` means "refuse" rather than "silently substitute the other".
289
+ * An adapter that forwards a profile to some other layer must still declare what that layer's
290
+ * harness really does — being able to put the field on the wire is not the same as honoring it.
291
+ */
292
+ export function harnessSystemPromptIntents(harness) {
293
+ if (!harness)
294
+ return noSystemPromptControls;
295
+ return harnessSystemPromptControls[harness] ?? noSystemPromptControls;
296
+ }
package/dist/harness.d.ts CHANGED
@@ -16,8 +16,12 @@ import { z } from "zod";
16
16
  *
17
17
  * `forge` (tailcallhq/forgecode) and `cursor` (cursor-agent) are multi-provider CLI harnesses with
18
18
  * no vendor lock, so they carry no entry in the capability tables and resolve as router-backed.
19
+ *
20
+ * `prime` (PrimeIntellect-ai/prime-agent) is the prime fork of the pi line — the RLM
21
+ * IPython-kernel harness. It is a distinct harness from `pi`: the fork's wire protocol has
22
+ * diverged (its daemon rejects pi-line clients), so the two are not interchangeable at run time.
19
23
  */
20
- export type HarnessType = "claude-code" | "nanoclaw" | "codex" | "opencode" | "kimi-code" | "pi" | "gemini" | "hermes" | "openclaw" | "amp" | "factory-droids" | "forge" | "cursor" | "acp" | "cli-base";
24
+ export type HarnessType = "claude-code" | "nanoclaw" | "codex" | "opencode" | "kimi-code" | "pi" | "prime" | "gemini" | "hermes" | "openclaw" | "amp" | "factory-droids" | "forge" | "cursor" | "acp" | "cli-base";
21
25
  /** Runtime validator for {@link HarnessType}. Kept in lockstep with the type by the drift guard below. */
22
26
  export declare const harnessTypeSchema: z.ZodEnum<{
23
27
  "claude-code": "claude-code";
@@ -26,6 +30,7 @@ export declare const harnessTypeSchema: z.ZodEnum<{
26
30
  opencode: "opencode";
27
31
  "kimi-code": "kimi-code";
28
32
  pi: "pi";
33
+ prime: "prime";
29
34
  gemini: "gemini";
30
35
  hermes: "hermes";
31
36
  openclaw: "openclaw";
package/dist/harness.js CHANGED
@@ -7,6 +7,7 @@ export const harnessTypeSchema = z.enum([
7
7
  "opencode",
8
8
  "kimi-code",
9
9
  "pi",
10
+ "prime",
10
11
  "gemini",
11
12
  "hermes",
12
13
  "openclaw",
@@ -4,6 +4,7 @@ export type AgentProfileDiffAxis = "identity" | (typeof agentProfileDiffProperty
4
4
  export type AgentProfileRemoveList = true | readonly string[];
5
5
  export interface AgentProfilePromptRemoval {
6
6
  systemPrompt?: true;
7
+ appendSystemPrompt?: true;
7
8
  instructions?: AgentProfileRemoveList;
8
9
  }
9
10
  export interface AgentProfileResourceRemoval {
@@ -21,6 +21,13 @@ void _agentProfileDiffPropertyAxesAreExhaustive;
21
21
  export function defineAgentProfileDiff(diff) {
22
22
  return diff;
23
23
  }
24
+ const agentProfilePromptDiffPropertyAxes = [
25
+ "systemPrompt",
26
+ "appendSystemPrompt",
27
+ "instructions",
28
+ ];
29
+ const _agentProfilePromptDiffPropertyAxesAreExhaustive = true;
30
+ void _agentProfilePromptDiffPropertyAxesAreExhaustive;
24
31
  const agentProfileResourceDiffPropertyAxes = [
25
32
  "files",
26
33
  "tools",
@@ -223,6 +230,8 @@ function applyRemoval(profile, remove) {
223
230
  const prompt = { ...next.prompt };
224
231
  if (remove.prompt.systemPrompt)
225
232
  prompt.systemPrompt = undefined;
233
+ if (remove.prompt.appendSystemPrompt)
234
+ prompt.appendSystemPrompt = undefined;
226
235
  prompt.instructions = removeValues(prompt.instructions, remove.prompt.instructions);
227
236
  next.prompt = Object.values(prompt).some((value) => value !== undefined)
228
237
  ? prompt
@@ -135,8 +135,15 @@ export declare const agentProfileModelHintsSchema: z.ZodObject<{
135
135
  }>>;
136
136
  metadata: z.ZodOptional<z.ZodType<Record<string, unknown>, unknown, z.core.$ZodTypeInternals<Record<string, unknown>, unknown>>>;
137
137
  }, z.core.$strict>;
138
+ /**
139
+ * Replacement and addition are separate, independently optional fields, and the
140
+ * pair is admitted on purpose: the effective prompt is `systemPrompt` followed
141
+ * by `appendSystemPrompt`. No cross-field refinement rejects the combination,
142
+ * because {@link mergeAgentProfiles} can produce it from two valid profiles.
143
+ */
138
144
  export declare const agentProfilePromptSchema: z.ZodObject<{
139
145
  systemPrompt: z.ZodOptional<z.ZodString>;
146
+ appendSystemPrompt: z.ZodOptional<z.ZodString>;
140
147
  instructions: z.ZodOptional<z.ZodArray<z.ZodString>>;
141
148
  }, z.core.$strict>;
142
149
  export declare const agentProfilePublicConfigValueSchema: z.ZodObject<{
@@ -244,6 +251,7 @@ export declare const agentProfileConnectionSchema: z.ZodObject<{
244
251
  }, z.core.$strict>;
245
252
  export declare const agentProfilePromptRemovalSchema: z.ZodObject<{
246
253
  systemPrompt: z.ZodOptional<z.ZodLiteral<true>>;
254
+ appendSystemPrompt: z.ZodOptional<z.ZodLiteral<true>>;
247
255
  instructions: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodArray<z.ZodString>]>>;
248
256
  }, z.core.$strict>;
249
257
  export declare const agentProfileResourceRemovalSchema: z.ZodObject<{
@@ -260,6 +268,7 @@ export declare const agentProfileDiffRemovalSchema: z.ZodObject<{
260
268
  tags: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodArray<z.ZodString>]>>;
261
269
  prompt: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodObject<{
262
270
  systemPrompt: z.ZodOptional<z.ZodLiteral<true>>;
271
+ appendSystemPrompt: z.ZodOptional<z.ZodLiteral<true>>;
263
272
  instructions: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodArray<z.ZodString>]>>;
264
273
  }, z.core.$strict>]>>;
265
274
  model: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<true>, z.ZodArray<z.ZodString>]>>;
@@ -296,6 +305,7 @@ export declare const agentProfileSchema: z.ZodObject<{
296
305
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
297
306
  prompt: z.ZodOptional<z.ZodObject<{
298
307
  systemPrompt: z.ZodOptional<z.ZodString>;
308
+ appendSystemPrompt: z.ZodOptional<z.ZodString>;
299
309
  instructions: z.ZodOptional<z.ZodArray<z.ZodString>>;
300
310
  }, z.core.$strict>>;
301
311
  model: z.ZodOptional<z.ZodObject<{
@@ -320,6 +330,7 @@ export declare const agentProfileSchema: z.ZodObject<{
320
330
  opencode: "opencode";
321
331
  "kimi-code": "kimi-code";
322
332
  pi: "pi";
333
+ prime: "prime";
323
334
  gemini: "gemini";
324
335
  hermes: "hermes";
325
336
  openclaw: "openclaw";
@@ -98,8 +98,15 @@ export const agentProfileModelHintsSchema = z.strictObject({
98
98
  reasoningEffort: reasoningEffortSchema.optional(),
99
99
  metadata: ownPropertyRecordSchema(z.unknown()).optional(),
100
100
  });
101
+ /**
102
+ * Replacement and addition are separate, independently optional fields, and the
103
+ * pair is admitted on purpose: the effective prompt is `systemPrompt` followed
104
+ * by `appendSystemPrompt`. No cross-field refinement rejects the combination,
105
+ * because {@link mergeAgentProfiles} can produce it from two valid profiles.
106
+ */
101
107
  export const agentProfilePromptSchema = z.strictObject({
102
108
  systemPrompt: z.string().optional(),
109
+ appendSystemPrompt: z.string().optional(),
103
110
  instructions: z.array(z.string()).optional(),
104
111
  });
105
112
  const controlCharacterPattern = /[\u0000-\u001f\u007f]/;
@@ -245,6 +252,7 @@ export const agentProfileConnectionSchema = z.strictObject({
245
252
  const removeListSchema = z.union([z.literal(true), z.array(z.string().min(1))]);
246
253
  export const agentProfilePromptRemovalSchema = z.strictObject({
247
254
  systemPrompt: z.literal(true).optional(),
255
+ appendSystemPrompt: z.literal(true).optional(),
248
256
  instructions: removeListSchema.optional(),
249
257
  });
250
258
  export const agentProfileResourceRemovalSchema = z.strictObject({
@@ -347,6 +355,17 @@ export const agentProfileDiffSchema = z.strictObject({
347
355
  });
348
356
  const _agentProfileSchemaMatchesInterface = true;
349
357
  void _agentProfileSchemaMatchesInterface;
358
+ // Plain assignability cannot see a missing OPTIONAL field: an object type
359
+ // without `x?` is assignable to one with it, in both directions. Since nearly
360
+ // every profile field is optional, comparing the same shapes with optionality
361
+ // removed is what actually catches a field added to one side only — and a field
362
+ // missing from this strict schema means valid profiles get rejected at runtime.
363
+ // Applied at the top level and again inside the prompt, whose two intents are
364
+ // distinct enough that losing one is a silent semantic change, not a parse error.
365
+ const _agentProfileSchemaFieldsMatchInterface = true;
366
+ void _agentProfileSchemaFieldsMatchInterface;
367
+ const _agentProfilePromptSchemaFieldsMatchInterface = true;
368
+ void _agentProfilePromptSchemaFieldsMatchInterface;
350
369
  export const capabilitySchema = z.strictObject({
351
370
  id: z.string().min(1),
352
371
  definition: agentProfileSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-interface",
3
- "version": "0.43.1",
3
+ "version": "0.45.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "license": "MIT",