@tangle-network/agent-interface 2.19.0 → 2.20.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
@@ -116,6 +116,7 @@ Providers that support recoverable workspace copies expose `workspaceBranching`
116
116
  Checkpoint and fork requests bind an idempotency key to a canonical request digest.
117
117
  Every returned resource repeats and validates that identity, lookups recover remote success after caller restart, changed-input key reuse returns a conflict, and cleanup binds its acknowledgement to the exact provider and target.
118
118
  A checkpoint with dependent forks returns `in_use` plus the blocking environment identifiers and remains recoverable until those forks are destroyed.
119
+ A provider whose forks hold their own copy of the checkpoint deletes it without waiting for them.
119
120
  The older `checkpoint()` and `fork()` methods remain source-compatible for providers that have not yet implemented recovery semantics, but clients must not present them as durable workspace branching.
120
121
 
121
122
  `CreateAgentEnvironmentInput.idempotencyKey` makes generic environment creation one retry-safe operation.
@@ -21,13 +21,76 @@ import type { HarnessType } from "./harness.js";
21
21
  export declare const reasoningLadder: readonly ReasoningEffort[];
22
22
  /** Provider prefix of a canonical model id (`anthropic/claude-…` → `anthropic`), or null. */
23
23
  export declare function modelProvider(modelId: string): string | null;
24
+ /**
25
+ * The provider a model id belongs to: its explicit prefix, else the provider of its bare-id family
26
+ * (`claude-sonnet-5-5` → `anthropic`). `null` for an id outside every family, such as the `default`
27
+ * sentinel, whose provider only the serving harness knows.
28
+ */
29
+ export declare function resolveModelProvider(modelId: string): string | null;
24
30
  /** The providers a harness is locked to, or `null` when it is router-backed (any model). */
25
31
  export declare function harnessProviders(harness: HarnessType): readonly string[] | null;
32
+ /** A harness × model pair that a production run measured to fail at its first model call. */
33
+ export interface HarnessModelExclusion {
34
+ readonly harness: HarnessType;
35
+ /** Canonical `provider/model` ids the measurement covers. */
36
+ readonly models: RegExp;
37
+ /**
38
+ * The failure needs a reasoning effort above `none`: the harness sends the rejected control only
39
+ * then, so the same pair without a reasoning effort is not excluded.
40
+ */
41
+ readonly onlyWithReasoningEffort: boolean;
42
+ /** What the provider rejected, and why the harness triggers it. */
43
+ readonly reason: string;
44
+ /** ISO date of the production measurement. */
45
+ readonly measuredOn: string;
46
+ /** Where the measurement is recorded. */
47
+ readonly evidence: string;
48
+ }
49
+ /**
50
+ * Pairs a router-backed harness cannot run, measured on production with the Router credential: one
51
+ * prompt per pair, profile reasoning effort `medium` (2026-10-05). The same run passed pi with
52
+ * `openai/gpt-5.5` and `zai/glm-5.3`, and opencode with every model listed here. Pairs not listed
53
+ * were not measured to fail. An entry is removed when its harness adapter or the Router is fixed and
54
+ * the pair passes a new production measurement.
55
+ *
56
+ * pi's chat-completions client (pi-ai 0.85.1) marks a model as reasoning only when the profile asks
57
+ * for a reasoning effort, and only a reasoning model is sent the `developer` role and
58
+ * `reasoning_effort`; that is why the first two entries need a reasoning effort.
59
+ */
60
+ export declare const harnessModelExclusions: readonly HarnessModelExclusion[];
61
+ /** The answer to "can this harness run this model", with the refusal a caller can show as-is. */
62
+ export type HarnessModelSupport = {
63
+ readonly supported: true;
64
+ /** Canonical `provider/model` id when the provider resolved, else the id as given. */
65
+ readonly model: string;
66
+ readonly provider: string | null;
67
+ } | {
68
+ readonly supported: false;
69
+ readonly model: string;
70
+ readonly provider: string | null;
71
+ /** The providers a vendor-locked harness accepts; absent for a measured exclusion. */
72
+ readonly allowedProviders?: readonly string[];
73
+ /** The measurement behind a refusal of a router-backed pair. */
74
+ readonly exclusion?: HarnessModelExclusion;
75
+ /** One sentence naming the harness, the model and the reason. */
76
+ readonly message: string;
77
+ };
78
+ export interface HarnessModelSupportOptions {
79
+ /** The run's reasoning effort; `null`, `undefined` and `none` all mean no reasoning effort. */
80
+ readonly reasoningEffort?: ReasoningEffort | null;
81
+ }
82
+ /**
83
+ * Whether a harness can run a model, and why not. A bare id resolves its provider through
84
+ * {@link resolveModelProvider}; an id whose provider cannot be resolved (a sentinel like `default`)
85
+ * is left to the harness, because its provider only the serving harness knows. A vendor-locked
86
+ * harness refuses every other provider; a router-backed harness refuses only the measured
87
+ * {@link harnessModelExclusions}.
88
+ */
89
+ export declare function harnessModelSupport(harness: HarnessType, modelId: string, options?: HarnessModelSupportOptions): HarnessModelSupport;
26
90
  /**
27
- * Whether a harness can run a model. Router-backed harnesses (no provider lock) accept anything;
28
- * a model id with no provider prefix (a sentinel like `default`) is treated as compatible too.
91
+ * Whether a harness can run a model; {@link harnessModelSupport} gives the reason when it cannot.
29
92
  */
30
- export declare function harnessSupportsModel(harness: HarnessType, modelId: string): boolean;
93
+ export declare function harnessSupportsModel(harness: HarnessType, modelId: string, options?: HarnessModelSupportOptions): boolean;
31
94
  /** The harness to adopt for a model whose provider is vendor-locked (`anthropic` → `claude-code`,
32
95
  * `openai` → `codex`, `moonshot` → `kimi-code`); `null` when any router-backed harness will do. */
33
96
  export declare function preferredHarnessForModel(modelId: string): HarnessType | null;
@@ -21,7 +21,8 @@ export const reasoningLadder = REASONING_EFFORTS;
21
21
  // ── Harness ↔ model compatibility ────────────────────────────────────────────
22
22
  /**
23
23
  * Provider prefixes a harness is vendor-locked to (canonical-id prefix, e.g. `anthropic`, `openai`).
24
- * A harness with no entry is router-backed: it runs any model.
24
+ * A harness with no entry is router-backed: it runs any model, except the measured exclusions in
25
+ * {@link harnessModelExclusions}.
25
26
  *
26
27
  * `nanoclaw` is deliberately absent despite the "claw" name: its runner routes every provider through
27
28
  * the Tangle router (canonical model id straight to the gateway), so it is router-backed like
@@ -37,25 +38,141 @@ export function modelProvider(modelId) {
37
38
  const slash = modelId.indexOf("/");
38
39
  return slash > 0 ? modelId.slice(0, slash) : null;
39
40
  }
41
+ /**
42
+ * Model families whose bare ids belong to one provider. The Router catalog serves most ids without a
43
+ * prefix (`claude-sonnet-5-5`, `gpt-5.5`, `glm-5.3`) and names the provider beside them, so a check
44
+ * that read every bare id as "any provider" offered `claude-code` with `glm-5.3`.
45
+ */
46
+ const bareModelFamilyProviders = [
47
+ [/^claude-/, "anthropic"],
48
+ [/^(gpt-|chatgpt-|codex-|o\d+(-|$))/, "openai"],
49
+ [/^(kimi-|moonshot-)/, "moonshot"],
50
+ [/^gemini-/, "google"],
51
+ [/^glm-/, "zai"],
52
+ [/^deepseek-/, "deepseek"],
53
+ [/^grok-/, "xai"],
54
+ ];
55
+ /**
56
+ * The provider a model id belongs to: its explicit prefix, else the provider of its bare-id family
57
+ * (`claude-sonnet-5-5` → `anthropic`). `null` for an id outside every family, such as the `default`
58
+ * sentinel, whose provider only the serving harness knows.
59
+ */
60
+ export function resolveModelProvider(modelId) {
61
+ const explicit = modelProvider(modelId);
62
+ if (explicit)
63
+ return explicit;
64
+ const id = modelId.trim().toLowerCase();
65
+ for (const [family, provider] of bareModelFamilyProviders) {
66
+ if (family.test(id))
67
+ return provider;
68
+ }
69
+ return null;
70
+ }
40
71
  /** The providers a harness is locked to, or `null` when it is router-backed (any model). */
41
72
  export function harnessProviders(harness) {
42
73
  return harnessProviderLock[harness] ?? null;
43
74
  }
75
+ const SANDBOX_HARNESS_MATRIX_EVIDENCE = "https://github.com/tangle-network/agent-dev-container/issues/9382";
76
+ /**
77
+ * Pairs a router-backed harness cannot run, measured on production with the Router credential: one
78
+ * prompt per pair, profile reasoning effort `medium` (2026-10-05). The same run passed pi with
79
+ * `openai/gpt-5.5` and `zai/glm-5.3`, and opencode with every model listed here. Pairs not listed
80
+ * were not measured to fail. An entry is removed when its harness adapter or the Router is fixed and
81
+ * the pair passes a new production measurement.
82
+ *
83
+ * pi's chat-completions client (pi-ai 0.85.1) marks a model as reasoning only when the profile asks
84
+ * for a reasoning effort, and only a reasoning model is sent the `developer` role and
85
+ * `reasoning_effort`; that is why the first two entries need a reasoning effort.
86
+ */
87
+ export const harnessModelExclusions = [
88
+ {
89
+ harness: "pi",
90
+ models: /^anthropic\//,
91
+ onlyWithReasoningEffort: true,
92
+ reason: 'the Router\'s chat-completions route for Anthropic models rejects the "developer" role pi sends ' +
93
+ 'to a reasoning model (400 Unexpected role "developer")',
94
+ measuredOn: "2026-10-05",
95
+ evidence: SANDBOX_HARNESS_MATRIX_EVIDENCE,
96
+ },
97
+ {
98
+ harness: "pi",
99
+ models: /^openai\/gpt-5\.6-luna$/,
100
+ onlyWithReasoningEffort: true,
101
+ reason: "OpenAI does not accept function tools together with reasoning_effort for this model on " +
102
+ "/v1/chat/completions, the only route pi speaks",
103
+ measuredOn: "2026-10-05",
104
+ evidence: SANDBOX_HARNESS_MATRIX_EVIDENCE,
105
+ },
106
+ {
107
+ harness: "pi",
108
+ models: /^google\/gemini-3\.8-flash$/,
109
+ onlyWithReasoningEffort: false,
110
+ reason: "the model requires each function call to return its thought_signature, which pi's " +
111
+ "chat-completions client does not send back (400 Function call is missing a thought_signature)",
112
+ measuredOn: "2026-10-05",
113
+ evidence: SANDBOX_HARNESS_MATRIX_EVIDENCE,
114
+ },
115
+ ];
116
+ /**
117
+ * Whether a harness can run a model, and why not. A bare id resolves its provider through
118
+ * {@link resolveModelProvider}; an id whose provider cannot be resolved (a sentinel like `default`)
119
+ * is left to the harness, because its provider only the serving harness knows. A vendor-locked
120
+ * harness refuses every other provider; a router-backed harness refuses only the measured
121
+ * {@link harnessModelExclusions}.
122
+ */
123
+ export function harnessModelSupport(harness, modelId, options = {}) {
124
+ const trimmed = modelId.trim();
125
+ const provider = resolveModelProvider(trimmed);
126
+ const model = provider !== null && modelProvider(trimmed) === null
127
+ ? `${provider}/${trimmed}`
128
+ : trimmed;
129
+ if (provider === null)
130
+ return { supported: true, model, provider };
131
+ const allowedProviders = harnessProviders(harness);
132
+ if (allowedProviders && !allowedProviders.includes(provider)) {
133
+ return {
134
+ supported: false,
135
+ model,
136
+ provider,
137
+ allowedProviders,
138
+ message: `Harness ${harness} runs only ${allowedProviders.join(" or ")} models; ` +
139
+ `${model} is a ${provider} model. Choose a model whose id starts with ` +
140
+ `${allowedProviders.map((p) => `${p}/`).join(" or ")}, or a harness that runs ` +
141
+ `${provider} models, such as opencode.`,
142
+ };
143
+ }
144
+ const reasoning = options.reasoningEffort !== undefined &&
145
+ options.reasoningEffort !== null &&
146
+ options.reasoningEffort !== "none";
147
+ const exclusion = harnessModelExclusions.find((entry) => entry.harness === harness &&
148
+ entry.models.test(model) &&
149
+ (!entry.onlyWithReasoningEffort || reasoning));
150
+ if (exclusion) {
151
+ return {
152
+ supported: false,
153
+ model,
154
+ provider,
155
+ exclusion,
156
+ message: `Harness ${harness} cannot run ${model}` +
157
+ (exclusion.onlyWithReasoningEffort ? " with a reasoning effort" : "") +
158
+ `: ${exclusion.reason} (measured ${exclusion.measuredOn}, ${exclusion.evidence}). ` +
159
+ (exclusion.onlyWithReasoningEffort
160
+ ? "Remove model.reasoningEffort, or choose another harness or model."
161
+ : "Choose another harness or model."),
162
+ };
163
+ }
164
+ return { supported: true, model, provider };
165
+ }
44
166
  /**
45
- * Whether a harness can run a model. Router-backed harnesses (no provider lock) accept anything;
46
- * a model id with no provider prefix (a sentinel like `default`) is treated as compatible too.
167
+ * Whether a harness can run a model; {@link harnessModelSupport} gives the reason when it cannot.
47
168
  */
48
- export function harnessSupportsModel(harness, modelId) {
49
- const providers = harnessProviders(harness);
50
- if (!providers)
51
- return true;
52
- const provider = modelProvider(modelId);
53
- return provider === null || providers.includes(provider);
169
+ export function harnessSupportsModel(harness, modelId, options) {
170
+ return harnessModelSupport(harness, modelId, options).supported;
54
171
  }
55
172
  /** The harness to adopt for a model whose provider is vendor-locked (`anthropic` → `claude-code`,
56
173
  * `openai` → `codex`, `moonshot` → `kimi-code`); `null` when any router-backed harness will do. */
57
174
  export function preferredHarnessForModel(modelId) {
58
- const provider = modelProvider(modelId);
175
+ const provider = resolveModelProvider(modelId);
59
176
  if (!provider)
60
177
  return null;
61
178
  for (const [harness, providers] of Object.entries(harnessProviderLock)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-interface",
3
- "version": "2.19.0",
3
+ "version": "2.20.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "license": "MIT",