@tangle-network/agent-interface 2.19.0 → 2.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/harness-capabilities.d.ts +66 -3
- package/dist/harness-capabilities.js +127 -10
- package/package.json +1 -1
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
|
|
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
|
+
`not ${model} (provider ${provider}). 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
|
|
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
|
-
|
|
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 =
|
|
175
|
+
const provider = resolveModelProvider(modelId);
|
|
59
176
|
if (!provider)
|
|
60
177
|
return null;
|
|
61
178
|
for (const [harness, providers] of Object.entries(harnessProviderLock)) {
|