@tangle-network/agent-profile-materialize 0.20.5 → 0.20.7

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.
@@ -0,0 +1,22 @@
1
+ import type { AgentProfile } from "@tangle-network/agent-interface";
2
+ import type { HarnessId } from "./harness.js";
3
+ /**
4
+ * Whether a run keeps the harness's own system prompt, so that a host's
5
+ * environment context joins the profile's addition instead of replacing the
6
+ * harness prompt.
7
+ *
8
+ * A profile that declares prompt intent (`prompt.instructions` or
9
+ * `prompt.appendSystemPrompt`) without `prompt.systemPrompt` asked for the
10
+ * harness prompt to stay in force: that is what `appendSystemPrompt` means.
11
+ * A host that replaced it with its own template would delete exactly what the
12
+ * profile kept. On 2026-10-03 every Discovery research director ran under the
13
+ * sandbox's app-builder template this way.
14
+ *
15
+ * Only a harness whose addition is a launch argument can carry the host's
16
+ * context as an addition. claude-code is the one wired end to end through the
17
+ * sidecar and the CLI adapter, so it is the only one listed. Every other
18
+ * harness keeps the host's replacement, unchanged.
19
+ */
20
+ export declare function keepsHarnessSystemPrompt(profile: AgentProfile | undefined, harness: HarnessId | string | undefined): boolean;
21
+ /** Join prompt additions in order, skipping empty ones. */
22
+ export declare function joinSystemPromptAdditions(...additions: Array<string | undefined>): string | undefined;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Whether a run keeps the harness's own system prompt, so that a host's
3
+ * environment context joins the profile's addition instead of replacing the
4
+ * harness prompt.
5
+ *
6
+ * A profile that declares prompt intent (`prompt.instructions` or
7
+ * `prompt.appendSystemPrompt`) without `prompt.systemPrompt` asked for the
8
+ * harness prompt to stay in force: that is what `appendSystemPrompt` means.
9
+ * A host that replaced it with its own template would delete exactly what the
10
+ * profile kept. On 2026-10-03 every Discovery research director ran under the
11
+ * sandbox's app-builder template this way.
12
+ *
13
+ * Only a harness whose addition is a launch argument can carry the host's
14
+ * context as an addition. claude-code is the one wired end to end through the
15
+ * sidecar and the CLI adapter, so it is the only one listed. Every other
16
+ * harness keeps the host's replacement, unchanged.
17
+ */
18
+ export function keepsHarnessSystemPrompt(profile, harness) {
19
+ const prompt = profile?.prompt;
20
+ if (!prompt || prompt.systemPrompt !== undefined)
21
+ return false;
22
+ const declaresIntent = (prompt.instructions?.length ?? 0) > 0 ||
23
+ prompt.appendSystemPrompt !== undefined;
24
+ return declaresIntent && harness === "claude-code";
25
+ }
26
+ /** Join prompt additions in order, skipping empty ones. */
27
+ export function joinSystemPromptAdditions(...additions) {
28
+ const parts = additions
29
+ .map((addition) => addition?.trim())
30
+ .filter((addition) => Boolean(addition));
31
+ return parts.length > 0 ? parts.join("\n\n") : undefined;
32
+ }
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export type { AgentCandidateWorkspacePlan, AgentCandidateWorkspacePlanReceipt, A
4
4
  export { OPENCODE_NATIVE_TOOL_IDS, openCodeNativeToolId, } from "./control-maps.js";
5
5
  export type { HarnessId } from "./harness.js";
6
6
  export { HARNESS_SKILL_DIRS, harnessAutoLoadsAgentContextFile, harnessAutoLoadsContextFile, harnessContextFile, isMaterializerHarness, MATERIALIZER_CONTEXT_PATHS, MATERIALIZER_HARNESSES, markMaterializerContextFile, skillDirForHarness, } from "./harness.js";
7
+ export { joinSystemPromptAdditions, keepsHarnessSystemPrompt, } from "./harness-prompt.js";
7
8
  export type { OpenCodeMcpConfig } from "./mcp.js";
8
9
  export { openCodeMcpConfig } from "./mcp.js";
9
10
  export { modelIdsMatch, qualifyModelId, unqualifyModelId, } from "./model-id.js";
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  export { applyAgentCandidateWorkspacePlan, materializeCandidateProfile, } from "./candidate.js";
3
3
  export { OPENCODE_NATIVE_TOOL_IDS, openCodeNativeToolId, } from "./control-maps.js";
4
4
  export { HARNESS_SKILL_DIRS, harnessAutoLoadsAgentContextFile, harnessAutoLoadsContextFile, harnessContextFile, isMaterializerHarness, MATERIALIZER_CONTEXT_PATHS, MATERIALIZER_HARNESSES, markMaterializerContextFile, skillDirForHarness, } from "./harness.js";
5
+ export { joinSystemPromptAdditions, keepsHarnessSystemPrompt, } from "./harness-prompt.js";
5
6
  export { openCodeMcpConfig } from "./mcp.js";
6
7
  export { modelIdsMatch, qualifyModelId, unqualifyModelId, } from "./model-id.js";
7
8
  export { buildPiModelsConfig, buildPiModelsConfigFile, PI_MODELS_CONFIG_PATH, resolvePiRuntimePaths, } from "./pi-models-config.js";
package/dist/mcp.d.ts CHANGED
@@ -1,4 +1,11 @@
1
1
  export declare function materializeMcp(h: HarnessId, mcp: Record<string, ActiveAgentProfileMcpServer>, plan: WorkspacePlan, add: AddPlanFile, unsupported: (d: Unsupported["dimension"], r: string) => void): void;
2
+ /**
3
+ * The header a harness sends to a remote MCP server whose tool-call timeout it
4
+ * raised, carrying the timeout it applied in milliseconds. A server may hold a
5
+ * call for nearly that long. A server that does not see the header must assume
6
+ * the harness default, 60 s for Claude Code and Codex.
7
+ */
8
+ export declare const MCP_TOOL_TIMEOUT_HEADER = "X-Mcp-Tool-Timeout-Ms";
2
9
  /** OpenCode-native MCP configuration used by workspace and startup paths. */
3
10
  export type OpenCodeMcpConfig = Record<string, {
4
11
  type: "remote";
package/dist/mcp.js CHANGED
@@ -1,13 +1,21 @@
1
1
  export function materializeMcp(h, mcp, plan, add, unsupported) {
2
2
  switch (h) {
3
3
  case "claude-code": {
4
+ // Claude applies one MCP tool-call timeout to every server, so the
5
+ // longest declared one wins and is what each declaring server is told.
6
+ const timeouts = mcpToolTimeouts(mcp, h, unsupported);
7
+ const toolTimeoutMs = timeouts.size > 0 ? Math.max(...timeouts.values()) : undefined;
8
+ if (toolTimeoutMs !== undefined) {
9
+ setPlanEnvironment(plan, "MCP_TOOL_TIMEOUT", String(toolTimeoutMs));
10
+ }
4
11
  const servers = {};
5
12
  for (const [name, server] of Object.entries(mcp)) {
6
13
  if (server.url) {
14
+ const headers = withAppliedToolTimeout(server.headers, timeouts.has(name) ? toolTimeoutMs : undefined);
7
15
  servers[name] = {
8
16
  type: server.transport ?? "http",
9
17
  url: server.url,
10
- ...(server.headers ? { headers: server.headers } : {}),
18
+ ...(headers ? { headers } : {}),
11
19
  };
12
20
  continue;
13
21
  }
@@ -30,13 +38,14 @@ export function materializeMcp(h, mcp, plan, add, unsupported) {
30
38
  unsupported("mcp", "nanoclaw MCP must be injected through options.mcpServers by its SDK adapter");
31
39
  break;
32
40
  case "codex": {
41
+ const timeouts = mcpToolTimeouts(mcp, h, unsupported);
33
42
  const servers = [];
34
43
  for (const [name, server] of Object.entries(mcp).sort(([left], [right]) => compareStrings(left, right))) {
35
44
  if (server.url && server.transport === "sse") {
36
45
  unsupported("mcp", `codex: remote MCP server "${name}" uses unsupported SSE transport`);
37
46
  continue;
38
47
  }
39
- servers.push(concatWorkspacePlanArguments(`${tomlStr(name)} = `, codexMcpServerToml(name, server, plan)));
48
+ servers.push(concatWorkspacePlanArguments(`${tomlStr(name)} = `, codexMcpServerToml(name, server, plan, timeouts.get(name))));
40
49
  }
41
50
  if (servers.length > 0) {
42
51
  // Runtime overrides do not require persisted project trust. Header values
@@ -143,13 +152,69 @@ function tomlInlineStringTable(values) {
143
152
  .map(([key, value]) => `${tomlStr(key)} = ${tomlStr(value)}`)
144
153
  .join(", ")} }`;
145
154
  }
146
- function codexMcpServerToml(name, server, plan) {
155
+ /**
156
+ * The header a harness sends to a remote MCP server whose tool-call timeout it
157
+ * raised, carrying the timeout it applied in milliseconds. A server may hold a
158
+ * call for nearly that long. A server that does not see the header must assume
159
+ * the harness default, 60 s for Claude Code and Codex.
160
+ */
161
+ export const MCP_TOOL_TIMEOUT_HEADER = "X-Mcp-Tool-Timeout-Ms";
162
+ const MIN_MCP_TOOL_TIMEOUT_MS = 1_000;
163
+ const MAX_MCP_TOOL_TIMEOUT_MS = 3_600_000;
164
+ /**
165
+ * The tool-call timeout a profile asks the harness to give one MCP server,
166
+ * from `metadata.toolTimeoutMs`. Profile metadata is open in every published
167
+ * schema, so a profile carrying it validates against older consumers, which
168
+ * ignore it. A malformed value is reported, never guessed.
169
+ */
170
+ function mcpToolTimeoutMs(server, name, h, unsupported) {
171
+ const value = server.metadata?.toolTimeoutMs;
172
+ if (value === undefined)
173
+ return undefined;
174
+ if (typeof value !== "number" ||
175
+ !Number.isSafeInteger(value) ||
176
+ value < MIN_MCP_TOOL_TIMEOUT_MS ||
177
+ value > MAX_MCP_TOOL_TIMEOUT_MS) {
178
+ unsupported("mcp", `${h}: MCP server "${name}" metadata.toolTimeoutMs must be an integer from ${MIN_MCP_TOOL_TIMEOUT_MS} to ${MAX_MCP_TOOL_TIMEOUT_MS}`);
179
+ return undefined;
180
+ }
181
+ return value;
182
+ }
183
+ function mcpToolTimeouts(mcp, h, unsupported) {
184
+ const timeouts = new Map();
185
+ for (const [name, server] of Object.entries(mcp)) {
186
+ const value = mcpToolTimeoutMs(server, name, h, unsupported);
187
+ if (value !== undefined)
188
+ timeouts.set(name, value);
189
+ }
190
+ return timeouts;
191
+ }
192
+ function withAppliedToolTimeout(headers, appliedTimeoutMs) {
193
+ if (appliedTimeoutMs === undefined)
194
+ return headers;
195
+ return {
196
+ ...headers,
197
+ [MCP_TOOL_TIMEOUT_HEADER]: {
198
+ kind: "public",
199
+ value: String(appliedTimeoutMs),
200
+ },
201
+ };
202
+ }
203
+ function codexMcpServerToml(name, server, plan, toolTimeoutMs) {
147
204
  const fields = [];
205
+ // Codex takes whole seconds per server; the server is told what was applied.
206
+ const appliedTimeoutMs = toolTimeoutMs === undefined
207
+ ? undefined
208
+ : Math.ceil(toolTimeoutMs / 1000) * 1000;
209
+ if (appliedTimeoutMs !== undefined) {
210
+ fields.push(`tool_timeout_sec = ${appliedTimeoutMs / 1000}`);
211
+ }
148
212
  if (server.url) {
149
213
  fields.push(`url = ${tomlStr(server.url)}`);
150
- if (server.headers && Object.keys(server.headers).length > 0) {
214
+ const headers = withAppliedToolTimeout(server.headers, appliedTimeoutMs);
215
+ if (headers && Object.keys(headers).length > 0) {
151
216
  const envHeaders = {};
152
- for (const [header, value] of Object.entries(server.headers).sort(([left], [right]) => compareStrings(left, right))) {
217
+ for (const [header, value] of Object.entries(headers).sort(([left], [right]) => compareStrings(left, right))) {
153
218
  const envName = `TANGLE_MCP_HEADER_${sha256(`${name}\0${header}`)
154
219
  .slice(0, 24)
155
220
  .toUpperCase()}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-profile-materialize",
3
- "version": "0.20.5",
3
+ "version": "0.20.7",
4
4
  "description": "Materialize an AgentProfile into the native per-harness workspace files (the one shared materializer for cli-bridge, agent-dev-container, and VB).",
5
5
  "type": "module",
6
6
  "repository": {
@@ -29,7 +29,7 @@
29
29
  "@tangle-network/agent-interface-current": "npm:@tangle-network/agent-interface@2.13.0",
30
30
  "@types/node": "25.6.0",
31
31
  "typescript": "^6.0.3",
32
- "vitest": "^4.1.10"
32
+ "vitest": "^4.1.11"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "@tangle-network/agent-interface": "^2.13.0"