@tangle-network/agent-interface 0.43.1 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,6 +48,12 @@ The older `checkpoint()` and `fork()` methods remain source-compatible for provi
48
48
  All new wire values have exported Zod schemas on the package root.
49
49
  Omitting `interactions` and `nativeContinuation`, or leaving the three durable branching flags false, is the compatible declaration for existing providers.
50
50
 
51
+ `profile.systemPrompt` declares two independent bits rather than one flag.
52
+ `replace` means the provider deletes the harness's own system prompt and installs `prompt.systemPrompt`; `append` means it keeps that prompt and adds `prompt.appendSystemPrompt` to it.
53
+ A provider that can only append must declare `replace: false` and refuse a profile carrying `systemPrompt`, because quietly appending a requested replacement leaves the instructions the caller asked to delete in force.
54
+
55
+
56
+
51
57
  ## Install
52
58
 
53
59
  ```bash
@@ -77,7 +83,8 @@ const provider: AgentEnvironmentProvider = {
77
83
  capabilities: () => ({
78
84
  profile: {
79
85
  namedProfiles: false,
80
- systemPrompt: true,
86
+ // Most harnesses can only add to their built-in prompt, not delete it.
87
+ systemPrompt: { replace: false, append: true },
81
88
  instructions: true,
82
89
  tools: true,
83
90
  permissions: true,
@@ -96,6 +96,10 @@ export declare const agentCandidateProfilePlanMaterialSchema: z.ZodObject<{
96
96
  kind: z.ZodLiteral<"public">;
97
97
  value: z.ZodString;
98
98
  }, z.core.$strict>>;
99
+ appendSystemPrompt: z.ZodOptional<z.ZodObject<{
100
+ kind: z.ZodLiteral<"public">;
101
+ value: z.ZodString;
102
+ }, z.core.$strict>>;
99
103
  unsupported: z.ZodArray<z.ZodObject<{
100
104
  dimension: z.ZodString;
101
105
  reason: z.ZodString;
@@ -545,6 +549,10 @@ export declare const agentCandidateProfilePlanEvidenceSchema: z.ZodObject<{
545
549
  kind: "public";
546
550
  value: string;
547
551
  } | undefined;
552
+ appendSystemPrompt?: {
553
+ kind: "public";
554
+ value: string;
555
+ } | undefined;
548
556
  }, unknown, z.core.$ZodTypeInternals<{
549
557
  sourceProfileDigest: `sha256:${string}`;
550
558
  harness: "claude-code" | "nanoclaw" | "codex" | "opencode" | "kimi-code" | "pi" | "gemini" | "hermes" | "openclaw" | "amp" | "factory-droids" | "forge" | "cursor" | "acp" | "cli-base";
@@ -569,6 +577,10 @@ export declare const agentCandidateProfilePlanEvidenceSchema: z.ZodObject<{
569
577
  kind: "public";
570
578
  value: string;
571
579
  } | undefined;
580
+ appendSystemPrompt?: {
581
+ kind: "public";
582
+ value: string;
583
+ } | undefined;
572
584
  }, unknown>>;
573
585
  artifact: z.ZodUnion<readonly [z.ZodObject<{
574
586
  locator: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -618,6 +630,10 @@ export declare const agentCandidateProfileActivationSchema: z.ZodObject<{
618
630
  kind: "public";
619
631
  value: string;
620
632
  } | undefined;
633
+ appendSystemPrompt?: {
634
+ kind: "public";
635
+ value: string;
636
+ } | undefined;
621
637
  };
622
638
  artifact: {
623
639
  locator: {
@@ -665,6 +681,10 @@ export declare const agentCandidateProfileActivationSchema: z.ZodObject<{
665
681
  kind: "public";
666
682
  value: string;
667
683
  } | undefined;
684
+ appendSystemPrompt?: {
685
+ kind: "public";
686
+ value: string;
687
+ } | undefined;
668
688
  };
669
689
  artifact: {
670
690
  locator: {
@@ -117,6 +117,7 @@ export const agentCandidateProfilePlanMaterialSchema = z
117
117
  env: environmentConfigSchema,
118
118
  flags: z.array(agentCandidateConfigValueSchema),
119
119
  systemPrompt: agentCandidateConfigValueSchema.optional(),
120
+ appendSystemPrompt: agentCandidateConfigValueSchema.optional(),
120
121
  unsupported: z
121
122
  .array(z
122
123
  .object({
@@ -22,6 +22,7 @@ export declare const agentCandidateProfileSchema: z.ZodObject<{
22
22
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
23
23
  prompt: z.ZodOptional<z.ZodObject<{
24
24
  systemPrompt: z.ZodOptional<z.ZodString>;
25
+ appendSystemPrompt: z.ZodOptional<z.ZodString>;
25
26
  instructions: z.ZodOptional<z.ZodArray<z.ZodString>>;
26
27
  }, z.core.$strict>>;
27
28
  model: z.ZodOptional<z.ZodObject<{
@@ -13,6 +13,7 @@ export declare const agentCandidateExperimentSchema: z.ZodObject<{
13
13
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
14
14
  prompt: z.ZodOptional<z.ZodObject<{
15
15
  systemPrompt: z.ZodOptional<z.ZodString>;
16
+ appendSystemPrompt: z.ZodOptional<z.ZodString>;
16
17
  instructions: z.ZodOptional<z.ZodArray<z.ZodString>>;
17
18
  }, z.core.$strict>>;
18
19
  model: z.ZodOptional<z.ZodObject<{
@@ -840,6 +841,7 @@ export declare const agentCandidateExperimentSchema: z.ZodObject<{
840
841
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
841
842
  prompt: z.ZodOptional<z.ZodObject<{
842
843
  systemPrompt: z.ZodOptional<z.ZodString>;
844
+ appendSystemPrompt: z.ZodOptional<z.ZodString>;
843
845
  instructions: z.ZodOptional<z.ZodArray<z.ZodString>>;
844
846
  }, z.core.$strict>>;
845
847
  model: z.ZodOptional<z.ZodObject<{
@@ -2079,6 +2081,10 @@ export declare const candidateExecutionEvidenceSchema: z.ZodObject<{
2079
2081
  kind: "public";
2080
2082
  value: string;
2081
2083
  } | undefined;
2084
+ appendSystemPrompt?: {
2085
+ kind: "public";
2086
+ value: string;
2087
+ } | undefined;
2082
2088
  };
2083
2089
  artifact: {
2084
2090
  locator: {
@@ -2126,6 +2132,10 @@ export declare const candidateExecutionEvidenceSchema: z.ZodObject<{
2126
2132
  kind: "public";
2127
2133
  value: string;
2128
2134
  } | undefined;
2135
+ appendSystemPrompt?: {
2136
+ kind: "public";
2137
+ value: string;
2138
+ } | undefined;
2129
2139
  };
2130
2140
  artifact: {
2131
2141
  locator: {
@@ -3571,6 +3581,7 @@ export declare const agentImprovementMeasuredComparisonSchema: z.ZodObject<{
3571
3581
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
3572
3582
  prompt: z.ZodOptional<z.ZodObject<{
3573
3583
  systemPrompt: z.ZodOptional<z.ZodString>;
3584
+ appendSystemPrompt: z.ZodOptional<z.ZodString>;
3574
3585
  instructions: z.ZodOptional<z.ZodArray<z.ZodString>>;
3575
3586
  }, z.core.$strict>>;
3576
3587
  model: z.ZodOptional<z.ZodObject<{
@@ -4398,6 +4409,7 @@ export declare const agentImprovementMeasuredComparisonSchema: z.ZodObject<{
4398
4409
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
4399
4410
  prompt: z.ZodOptional<z.ZodObject<{
4400
4411
  systemPrompt: z.ZodOptional<z.ZodString>;
4412
+ appendSystemPrompt: z.ZodOptional<z.ZodString>;
4401
4413
  instructions: z.ZodOptional<z.ZodArray<z.ZodString>>;
4402
4414
  }, z.core.$strict>>;
4403
4415
  model: z.ZodOptional<z.ZodObject<{
@@ -5638,6 +5650,10 @@ export declare const agentImprovementMeasuredComparisonSchema: z.ZodObject<{
5638
5650
  kind: "public";
5639
5651
  value: string;
5640
5652
  } | undefined;
5653
+ appendSystemPrompt?: {
5654
+ kind: "public";
5655
+ value: string;
5656
+ } | undefined;
5641
5657
  };
5642
5658
  artifact: {
5643
5659
  locator: {
@@ -5685,6 +5701,10 @@ export declare const agentImprovementMeasuredComparisonSchema: z.ZodObject<{
5685
5701
  kind: "public";
5686
5702
  value: string;
5687
5703
  } | undefined;
5704
+ appendSystemPrompt?: {
5705
+ kind: "public";
5706
+ value: string;
5707
+ } | undefined;
5688
5708
  };
5689
5709
  artifact: {
5690
5710
  locator: {
@@ -7018,6 +7038,10 @@ export declare const agentImprovementMeasuredComparisonSchema: z.ZodObject<{
7018
7038
  kind: "public";
7019
7039
  value: string;
7020
7040
  } | undefined;
7041
+ appendSystemPrompt?: {
7042
+ kind: "public";
7043
+ value: string;
7044
+ } | undefined;
7021
7045
  };
7022
7046
  artifact: {
7023
7047
  locator: {
@@ -7065,6 +7089,10 @@ export declare const agentImprovementMeasuredComparisonSchema: z.ZodObject<{
7065
7089
  kind: "public";
7066
7090
  value: string;
7067
7091
  } | undefined;
7092
+ appendSystemPrompt?: {
7093
+ kind: "public";
7094
+ value: string;
7095
+ } | undefined;
7068
7096
  };
7069
7097
  artifact: {
7070
7098
  locator: {
@@ -8540,6 +8568,7 @@ export declare const agentImprovementProposalSchema: z.ZodObject<{
8540
8568
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
8541
8569
  prompt: z.ZodOptional<z.ZodObject<{
8542
8570
  systemPrompt: z.ZodOptional<z.ZodString>;
8571
+ appendSystemPrompt: z.ZodOptional<z.ZodString>;
8543
8572
  instructions: z.ZodOptional<z.ZodArray<z.ZodString>>;
8544
8573
  }, z.core.$strict>>;
8545
8574
  model: z.ZodOptional<z.ZodObject<{
@@ -9367,6 +9396,7 @@ export declare const agentImprovementProposalSchema: z.ZodObject<{
9367
9396
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
9368
9397
  prompt: z.ZodOptional<z.ZodObject<{
9369
9398
  systemPrompt: z.ZodOptional<z.ZodString>;
9399
+ appendSystemPrompt: z.ZodOptional<z.ZodString>;
9370
9400
  instructions: z.ZodOptional<z.ZodArray<z.ZodString>>;
9371
9401
  }, z.core.$strict>>;
9372
9402
  model: z.ZodOptional<z.ZodObject<{
@@ -10607,6 +10637,10 @@ export declare const agentImprovementProposalSchema: z.ZodObject<{
10607
10637
  kind: "public";
10608
10638
  value: string;
10609
10639
  } | undefined;
10640
+ appendSystemPrompt?: {
10641
+ kind: "public";
10642
+ value: string;
10643
+ } | undefined;
10610
10644
  };
10611
10645
  artifact: {
10612
10646
  locator: {
@@ -10654,6 +10688,10 @@ export declare const agentImprovementProposalSchema: z.ZodObject<{
10654
10688
  kind: "public";
10655
10689
  value: string;
10656
10690
  } | undefined;
10691
+ appendSystemPrompt?: {
10692
+ kind: "public";
10693
+ value: string;
10694
+ } | undefined;
10657
10695
  };
10658
10696
  artifact: {
10659
10697
  locator: {
@@ -11987,6 +12025,10 @@ export declare const agentImprovementProposalSchema: z.ZodObject<{
11987
12025
  kind: "public";
11988
12026
  value: string;
11989
12027
  } | undefined;
12028
+ appendSystemPrompt?: {
12029
+ kind: "public";
12030
+ value: string;
12031
+ } | undefined;
11990
12032
  };
11991
12033
  artifact: {
11992
12034
  locator: {
@@ -12034,6 +12076,10 @@ export declare const agentImprovementProposalSchema: z.ZodObject<{
12034
12076
  kind: "public";
12035
12077
  value: string;
12036
12078
  } | undefined;
12079
+ appendSystemPrompt?: {
12080
+ kind: "public";
12081
+ value: string;
12082
+ } | undefined;
12037
12083
  };
12038
12084
  artifact: {
12039
12085
  locator: {
@@ -218,6 +218,10 @@ export declare const agentCandidateMaterializationReceiptSchema: z.ZodObject<{
218
218
  kind: "public";
219
219
  value: string;
220
220
  } | undefined;
221
+ appendSystemPrompt?: {
222
+ kind: "public";
223
+ value: string;
224
+ } | undefined;
221
225
  };
222
226
  artifact: {
223
227
  locator: {
@@ -265,6 +269,10 @@ export declare const agentCandidateMaterializationReceiptSchema: z.ZodObject<{
265
269
  kind: "public";
266
270
  value: string;
267
271
  } | undefined;
272
+ appendSystemPrompt?: {
273
+ kind: "public";
274
+ value: string;
275
+ } | undefined;
268
276
  };
269
277
  artifact: {
270
278
  locator: {
@@ -17,6 +17,7 @@ export declare const agentCandidateBundleSchema: z.ZodObject<{
17
17
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
18
18
  prompt: z.ZodOptional<z.ZodObject<{
19
19
  systemPrompt: z.ZodOptional<z.ZodString>;
20
+ appendSystemPrompt: z.ZodOptional<z.ZodString>;
20
21
  instructions: z.ZodOptional<z.ZodArray<z.ZodString>>;
21
22
  }, z.core.$strict>>;
22
23
  model: z.ZodOptional<z.ZodObject<{
@@ -344,8 +344,19 @@ export interface AgentCandidateProfilePlanMaterial {
344
344
  }>;
345
345
  env: Record<string, AgentCandidateConfigValue>;
346
346
  flags: AgentCandidateConfigValue[];
347
- /** Exact system-prompt replacement supplied to the harness, when supported. */
347
+ /**
348
+ * Exact system-prompt REPLACEMENT supplied to the harness, when supported.
349
+ * Present only where the harness's own system prompt is deleted in favor of
350
+ * this text.
351
+ */
348
352
  systemPrompt?: AgentCandidateConfigValue;
353
+ /**
354
+ * Exact text ADDED to the harness's own system prompt, which stays intact.
355
+ * Recorded separately from the replacement so plan identity distinguishes the
356
+ * two intents; a harness that lowers this into a flag or file still records
357
+ * it here, because the same bytes in the two fields are two different runs.
358
+ */
359
+ appendSystemPrompt?: AgentCandidateConfigValue;
349
360
  unsupported: Array<{
350
361
  dimension: string;
351
362
  reason: string;
@@ -164,10 +164,11 @@ export declare const agentExecutionPreparationAxisResultSchema: z.ZodObject<{
164
164
  harness: "harness";
165
165
  confidential: "confidential";
166
166
  extensions: "extensions";
167
+ systemPrompt: "systemPrompt";
168
+ appendSystemPrompt: "appendSystemPrompt";
169
+ instructions: "instructions";
167
170
  skills: "skills";
168
171
  commands: "commands";
169
- instructions: "instructions";
170
- systemPrompt: "systemPrompt";
171
172
  modelDefault: "modelDefault";
172
173
  modelSmall: "modelSmall";
173
174
  modelProvider: "modelProvider";
@@ -310,10 +311,11 @@ export declare const agentExecutionPreparationReceiptSchema: z.ZodObject<{
310
311
  harness: "harness";
311
312
  confidential: "confidential";
312
313
  extensions: "extensions";
314
+ systemPrompt: "systemPrompt";
315
+ appendSystemPrompt: "appendSystemPrompt";
316
+ instructions: "instructions";
313
317
  skills: "skills";
314
318
  commands: "commands";
315
- instructions: "instructions";
316
- systemPrompt: "systemPrompt";
317
319
  modelDefault: "modelDefault";
318
320
  modelSmall: "modelSmall";
319
321
  modelProvider: "modelProvider";
@@ -1,12 +1,15 @@
1
1
  import type { AgentProfile } from "./agent-profile.js";
2
2
  /**
3
- * The 29 canonical AgentProfile leaves that can affect one execution.
3
+ * The 30 canonical AgentProfile leaves that can affect one execution.
4
4
  *
5
5
  * Compound parents such as `model`, `prompt`, and `resources` are deliberately
6
6
  * absent. A producer must report the exact requested leaf instead of claiming
7
- * a parent while silently dropping one of its children.
7
+ * a parent while silently dropping one of its children. `systemPrompt` and
8
+ * `appendSystemPrompt` are separate leaves for the same reason: a backend that
9
+ * can only add text must report the requested replacement as unsatisfied rather
10
+ * than acknowledge it.
8
11
  */
9
- export declare const AGENT_PROFILE_MATERIALIZATION_AXES: readonly ["name", "description", "version", "tags", "systemPrompt", "instructions", "modelDefault", "modelSmall", "modelProvider", "modelReasoningEffort", "modelMetadata", "harness", "permissions", "tools", "mcp", "connections", "subagents", "files", "resourceTools", "skills", "resourceAgents", "commands", "resourceInstructions", "resourceFailOnError", "hooks", "modes", "confidential", "metadata", "extensions"];
12
+ export declare const AGENT_PROFILE_MATERIALIZATION_AXES: readonly ["name", "description", "version", "tags", "systemPrompt", "appendSystemPrompt", "instructions", "modelDefault", "modelSmall", "modelProvider", "modelReasoningEffort", "modelMetadata", "harness", "permissions", "tools", "mcp", "connections", "subagents", "files", "resourceTools", "skills", "resourceAgents", "commands", "resourceInstructions", "resourceFailOnError", "hooks", "modes", "confidential", "metadata", "extensions"];
10
13
  /** One exact leaf of the public AgentProfile contract. */
11
14
  export type AgentProfileMaterializationAxis = (typeof AGENT_PROFILE_MATERIALIZATION_AXES)[number];
12
15
  /** Compatibility name used by runtimes that distinguish canonical axes. */
@@ -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
@@ -219,3 +220,72 @@ export function harnessHonorsEffort(harness) {
219
220
  export function harnessHonorsSelectors(harness) {
220
221
  return harnessHonorsModel(harness) && harnessHonorsEffort(harness);
221
222
  }
223
+ // ── System-prompt intents (which prompt channel the harness actually owns) ────
224
+ /**
225
+ * The system-prompt intents a harness's own controls can execute, measured by reading the request
226
+ * each installed CLI sends — NOT taken from its help text:
227
+ *
228
+ * - claude-code 2.1.222 and pi 0.83.0 own both. `--system-prompt` drops the built-in prompt from
229
+ * the request (27,673 B → the caller's bytes on claude-code, 2,582 B → the caller's on pi);
230
+ * `--append-system-prompt` leaves it in place and adds the caller's text after it.
231
+ * - codex 0.146.0 owns replacement only: the `model_instructions_file` config key becomes the
232
+ * request's entire instructions text. It has no additive control — its AGENTS.md lands in a
233
+ * developer/user message, not the system channel.
234
+ * - gemini 0.26.0 owns replacement only: `.gemini/system.md` under `GEMINI_SYSTEM_MD=1` replaces
235
+ * the base prompt. Its one additive path is GEMINI.md memory, which IS the `instructions`
236
+ * surface, so an addition lowered there would be byte-indistinguishable from `instructions`.
237
+ * - opencode 1.17.18 owns addition only THROUGH A WORKSPACE: config-declared `instructions[]`
238
+ * files compose into the same single `role: "system"` message as its built-in prompt, which
239
+ * stays in place. Its replacement control (`agent.<name>.prompt`) binds to one agent chosen at
240
+ * launch, which a workspace plan cannot guarantee — but an executor that selects that agent
241
+ * can, so `replace: false` here is the plan-forwarding answer, not opencode's ceiling.
242
+ *
243
+ * Every other harness owns NEITHER, including the ones whose prompt path is a `role: "system"` chat
244
+ * message: that message is flattened into the user turn before the CLI sees it, so it is not a
245
+ * system-prompt channel at all — honoring an intent through it would put the caller's text in
246
+ * ordinary user content while the harness's own prompt ran unchanged. A harness with no entry
247
+ * refuses both, so one added later cannot inherit a capability by omission.
248
+ */
249
+ const harnessSystemPromptControls = {
250
+ "claude-code": { replace: true, append: true },
251
+ pi: { replace: true, append: true },
252
+ codex: { replace: true, append: false },
253
+ gemini: { replace: true, append: false },
254
+ opencode: { replace: false, append: true },
255
+ };
256
+ const noSystemPromptControls = {
257
+ replace: false,
258
+ append: false,
259
+ };
260
+ /**
261
+ * Which system-prompt intents a harness honors THROUGH A WORKSPACE — the value an adapter that
262
+ * lowers a profile to files, env vars, and CLI flags and then hands the result to a launcher it
263
+ * does not own should declare as {@link AgentProfileCapabilities.systemPrompt}. That is the shape
264
+ * of every caller today (the cli-bridge and tangle providers both forward a plan), which is why
265
+ * this answer depends on the harness alone.
266
+ *
267
+ * It is NOT the whole truth for an adapter that starts the harness itself, because one control in
268
+ * the table above lives outside any workspace: opencode's `agent.<name>.prompt` really does replace
269
+ * its built-in prompt, but it binds to the single agent whoever starts the server selects. A plan
270
+ * cannot name that agent, so `opencode` reads `replace: false` here — while an adapter that writes
271
+ * opencode's server config AND picks the primary agent (`sdk-provider-opencode`) does honor
272
+ * replacement, and declares `replace: true` for itself. The capability is a property of the
273
+ * (harness, executor) pair; this function answers it for the plan-forwarding executor.
274
+ *
275
+ * Do not widen the table to close that gap: a harness-keyed `true` would promise the intent to
276
+ * every plan-forwarding caller, and those callers cannot deliver it. An executor that owns a
277
+ * launcher control states so where it binds it — `materializeProfile`'s `binds` option in
278
+ * `@tangle-network/agent-profile-materialize`, which turns the plan's refusal into a binding that
279
+ * executor must then apply.
280
+ *
281
+ * Pass `undefined` when the harness is not known at declaration time: the answer is then
282
+ * `{ replace: false, append: false }`, because an adapter that cannot name its harness cannot
283
+ * promise either intent, and `false` means "refuse" rather than "silently substitute the other".
284
+ * An adapter that forwards a profile to some other layer must still declare what that layer's
285
+ * harness really does — being able to put the field on the wire is not the same as honoring it.
286
+ */
287
+ export function harnessSystemPromptIntents(harness) {
288
+ if (!harness)
289
+ return noSystemPromptControls;
290
+ return harnessSystemPromptControls[harness] ?? noSystemPromptControls;
291
+ }
@@ -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<{
@@ -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.44.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "license": "MIT",