impel-cli 0.20.4 → 0.20.6
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/RELEASE_NOTES.md +18 -0
- package/docs/native-agent-host-capability-matrix.md +38 -0
- package/package.json +2 -1
- package/src/agents.js +21 -6
- package/src/cliProfiles.js +13 -9
- package/src/codexCliCatalog.js +56 -0
- package/src/commands/launch.js +9 -1
- package/src/commands/mcp.js +36 -2
- package/src/nativeAgentTelemetry.js +110 -0
- package/src/nativeAgentTransport.js +245 -94
- package/src/selfInvocation.js +16 -1
- package/src/verbatimRelay.js +2 -1
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Release notes
|
|
2
2
|
|
|
3
|
+
## 0.20.6 — Native-agent canary controls
|
|
4
|
+
|
|
5
|
+
- Gives Codex verbatim-relay parents one explicit 60-second blocking wait so
|
|
6
|
+
direct-answer children do not trigger short-poll model turns.
|
|
7
|
+
- Forwards only the three opt-in, closed-schema native telemetry settings into
|
|
8
|
+
managed MCP processes; unrelated environment values remain excluded.
|
|
9
|
+
|
|
10
|
+
## 0.20.5 — Faster managed native agents
|
|
11
|
+
|
|
12
|
+
- Exposes exact bound tools eagerly on qualified Claude hosts while preserving
|
|
13
|
+
a measured durable fallback for Codex/ChatGPT code-mode hosts.
|
|
14
|
+
- Reuses one upstream MCP session, validates one exact binding, and removes
|
|
15
|
+
duplicate model-catalog/auth-helper launch work.
|
|
16
|
+
- Adds terminal-only 35-second reads, direct-answer continuation, durable
|
|
17
|
+
ambiguous-start recovery, and opt-in privacy-safe timing telemetry.
|
|
18
|
+
- Adds independent rollback flags for eager exposure and terminal-only reads;
|
|
19
|
+
rollback never deletes active handles or runs.
|
|
20
|
+
|
|
3
21
|
## Native remote migration
|
|
4
22
|
|
|
5
23
|
The former `impel remote handoff` implementation started a one-shot headless
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Native-agent host capability matrix
|
|
2
|
+
|
|
3
|
+
Validated on 2026-08-07 with isolated temporary homes and tenant profiles. The
|
|
4
|
+
matrix is intentionally pinned: a client upgrade must be re-qualified before it
|
|
5
|
+
is treated as an eager host.
|
|
6
|
+
|
|
7
|
+
| Surface | Pinned build | Exact selected tools | Selected path | Host behavior |
|
|
8
|
+
| --- | --- | --- | --- | --- |
|
|
9
|
+
| Claude Code | 2.1.212 | Yes, agent frontmatter `tools` | Eager | The bound run/answer, resume, and recovery tools are direct child tools. No catalog discovery or code-cell polling is required. |
|
|
10
|
+
| Claude Desktop | 1.24012.9 (embedded Code 2.1.219) | Yes, agent frontmatter `tools` | Eager | Same direct-tool contract as Claude Code. |
|
|
11
|
+
| Codex CLI | 0.146.0 | No; MCP tools remain code-mode-only | Durable fallback | `enabled_tools` narrows the server contract but the host still invokes through code mode. Discovery/polling overhead is measured separately and is not classified as eager completion. |
|
|
12
|
+
| ChatGPT desktop Codex | 26.727.40816 (embedded Codex alpha) | No; MCP tools remain code-mode-only | Durable fallback | Uses the same bounded durable composite and recovery state as Codex CLI. |
|
|
13
|
+
|
|
14
|
+
## Qualified contract
|
|
15
|
+
|
|
16
|
+
- Eager profiles expose only the exact fixed binding's answer/run, resume, and
|
|
17
|
+
recovery tools. `IMPEL_NATIVE_EAGER_TRANSPORT=0` independently restores the
|
|
18
|
+
legacy host-visible tool configuration without deleting profiles or runs.
|
|
19
|
+
- Every local composite keeps one upstream MCP session for its process lifetime,
|
|
20
|
+
validates the exact binding once, and persists the authoritative run handle
|
|
21
|
+
before ambiguous retries.
|
|
22
|
+
- Forwarded waits are negotiated at no more than 35 seconds under the gateway's
|
|
23
|
+
45-second tool deadline. A local attachment returns a typed handle before the
|
|
24
|
+
outer host deadline and resume never opens a replacement run.
|
|
25
|
+
- Parent guidance permits one spawn and one blocking wait. It forbids follow-up
|
|
26
|
+
nudges and independent synthesis while the child is running.
|
|
27
|
+
- Host and transport traces must never include prompts, answers, credentials, or
|
|
28
|
+
decrypted provider material. Production measurements use correlation ids,
|
|
29
|
+
phase timestamps, counts, durations, build identifiers, and terminal classes
|
|
30
|
+
only.
|
|
31
|
+
|
|
32
|
+
## Requalification
|
|
33
|
+
|
|
34
|
+
Re-run temporary-home profile parsing, direct tool visibility, cancellation,
|
|
35
|
+
typed continuation, byte-exact relay, and the 10/30/45/60/120-second bounded
|
|
36
|
+
wait fixture after a pinned client changes. A build stays on durable fallback
|
|
37
|
+
unless the child trace has zero `ALL_TOOLS`, `functions.exec`, and
|
|
38
|
+
`functions.wait` entries.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "impel-cli",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.6",
|
|
4
4
|
"description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"RELEASE_NOTES.md",
|
|
15
15
|
"bin",
|
|
16
16
|
"docs/experimental-managed-cursor.md",
|
|
17
|
+
"docs/native-agent-host-capability-matrix.md",
|
|
17
18
|
"src",
|
|
18
19
|
"README.md"
|
|
19
20
|
],
|
package/src/agents.js
CHANGED
|
@@ -42,6 +42,7 @@ export const AGENT_SYNC_TTL_MS = 6 * 60 * 60 * 1000;
|
|
|
42
42
|
export const MANAGED_AGENT_DIRECTORY = "impel-managed";
|
|
43
43
|
export const MANAGED_AGENT_MANIFEST = ".manifest.json";
|
|
44
44
|
export const NATIVE_AGENT_LIST_TOOL = "impel_specialists-list_native_agents";
|
|
45
|
+
export const NATIVE_AGENT_GET_TOOL = "impel_specialists-get_native_agent";
|
|
45
46
|
export const NATIVE_AGENT_START_TOOL = "impel_specialists-start_native_agent_run";
|
|
46
47
|
export const NATIVE_AGENT_READ_TOOL = "impel_specialists-read_native_agent_run";
|
|
47
48
|
export const NATIVE_AGENT_UPSTREAM_ANSWER_TOOL = "impel_specialists-answer_native_agent";
|
|
@@ -50,7 +51,7 @@ export const NATIVE_AGENT_RUN_TOOL = "run_native_agent";
|
|
|
50
51
|
export const NATIVE_AGENT_RESUME_TOOL = "resume_native_agent_run";
|
|
51
52
|
export const NATIVE_AGENT_RECOVER_TOOL = "recover_native_agent_runs";
|
|
52
53
|
export const MANAGED_AGENT_MCP_SERVER = "impel_agent";
|
|
53
|
-
export const MANAGED_AGENT_MANIFEST_VERSION =
|
|
54
|
+
export const MANAGED_AGENT_MANIFEST_VERSION = 8;
|
|
54
55
|
|
|
55
56
|
const NATIVE_AGENT_TOOL_NAMES = [
|
|
56
57
|
NATIVE_AGENT_RUN_TOOL,
|
|
@@ -59,7 +60,12 @@ const NATIVE_AGENT_TOOL_NAMES = [
|
|
|
59
60
|
];
|
|
60
61
|
const NATIVE_AGENT_RECOVERY_TOOL_NAMES = [NATIVE_AGENT_RESUME_TOOL, NATIVE_AGENT_RECOVER_TOOL];
|
|
61
62
|
function nativeAgentToolNames(agent) {
|
|
62
|
-
return usesDirectAnswer(agent)
|
|
63
|
+
return usesDirectAnswer(agent)
|
|
64
|
+
? [NATIVE_AGENT_ANSWER_TOOL, ...NATIVE_AGENT_RECOVERY_TOOL_NAMES]
|
|
65
|
+
: NATIVE_AGENT_TOOL_NAMES;
|
|
66
|
+
}
|
|
67
|
+
function eagerNativeAgentTransportEnabled() {
|
|
68
|
+
return process.env.IMPEL_NATIVE_EAGER_TRANSPORT !== "0";
|
|
63
69
|
}
|
|
64
70
|
const MAX_RETIRED_AGENT_BINDINGS = 50;
|
|
65
71
|
const MAX_NATIVE_AGENT_STATE_BYTES = 512 * 1024;
|
|
@@ -602,6 +608,7 @@ function claudeAdapterInstructions(tenantId, agent) {
|
|
|
602
608
|
`Confirm that the request fits the synchronized capabilities ${JSON.stringify(agent.capabilities)} and none of the exclusions ${JSON.stringify(agent.exclusions)}.${contextRequirement}`,
|
|
603
609
|
sideEffectInstruction,
|
|
604
610
|
`Call ${nativeToolName(NATIVE_AGENT_ANSWER_TOOL)} exactly once with question set to the complete assigned task, optional context set to one string containing all supplied context, and contextKeys naming the fields present in that string.`,
|
|
611
|
+
`If the bounded answer returns an ${JSON.stringify("impel.native-agent-run.v1")} continuation handle, call ${nativeToolName(NATIVE_AGENT_RESUME_TOOL)} with exactly that handle until terminal. Never call answer_native_agent again for this request.`,
|
|
605
612
|
completionGuidance,
|
|
606
613
|
].join(" ");
|
|
607
614
|
}
|
|
@@ -645,6 +652,7 @@ function codexAdapterInstructions(tenantId, agent) {
|
|
|
645
652
|
`Confirm that the assigned request fits the cataloged capabilities ${JSON.stringify(agent.capabilities)} and none of the exclusions ${JSON.stringify(agent.exclusions)} before answering.${contextRequirement}`,
|
|
646
653
|
sideEffectInstruction,
|
|
647
654
|
`Call ${nativeToolName(NATIVE_AGENT_ANSWER_TOOL)} exactly once with question set to the complete assigned task, optional context set to one string containing all supplied context, and contextKeys naming the fields present in that string.`,
|
|
655
|
+
`If the bounded answer returns an ${JSON.stringify("impel.native-agent-run.v1")} continuation handle, call ${nativeToolName(NATIVE_AGENT_RESUME_TOOL)} with exactly that handle until terminal. Never call answer_native_agent again for this request.`,
|
|
648
656
|
completionGuidance,
|
|
649
657
|
].join("\n\n");
|
|
650
658
|
}
|
|
@@ -687,13 +695,15 @@ function renderClaudeAgent({ tenantId, agent, name, invocation, recoveryOnly = f
|
|
|
687
695
|
? `Recovery-only access to pending runs for retired Impel binding ${agent.agentId} in tenant ${tenantId}.`
|
|
688
696
|
: customAgentDescription(tenantId, agent);
|
|
689
697
|
const toolNames = recoveryOnly ? NATIVE_AGENT_RECOVERY_TOOL_NAMES : nativeAgentToolNames(agent);
|
|
698
|
+
const eager = eagerNativeAgentTransportEnabled();
|
|
690
699
|
const lines = [
|
|
691
700
|
"---",
|
|
692
701
|
`name: ${JSON.stringify(name)}`,
|
|
693
702
|
`description: ${JSON.stringify(description)}`,
|
|
694
|
-
"model:
|
|
695
|
-
|
|
696
|
-
|
|
703
|
+
"model: haiku",
|
|
704
|
+
...(eager
|
|
705
|
+
? ["tools:", ...toolNames.map((tool) => ` - ${JSON.stringify(nativeToolName(tool))}`)]
|
|
706
|
+
: []),
|
|
697
707
|
"mcpServers:",
|
|
698
708
|
` - ${MANAGED_AGENT_MCP_SERVER}:`,
|
|
699
709
|
" type: stdio",
|
|
@@ -715,19 +725,24 @@ function renderCodexAgent({ tenantId, agent, name, invocation, recoveryOnly = fa
|
|
|
715
725
|
? `Recovery-only custom agent for pending runs from retired Impel binding ${agent.agentId} in tenant ${tenantId}.`
|
|
716
726
|
: customAgentDescription(tenantId, agent);
|
|
717
727
|
const toolNames = recoveryOnly ? NATIVE_AGENT_RECOVERY_TOOL_NAMES : nativeAgentToolNames(agent);
|
|
728
|
+
const eager = eagerNativeAgentTransportEnabled();
|
|
718
729
|
const envEntries = Object.entries(invocation.env || {})
|
|
719
730
|
.map(([key, value]) => `${JSON.stringify(key)} = ${JSON.stringify(value)}`)
|
|
720
731
|
.join(", ");
|
|
721
732
|
const lines = [
|
|
722
733
|
`name = ${JSON.stringify(name)}`,
|
|
723
734
|
`description = ${JSON.stringify(description)}`,
|
|
735
|
+
'model = "gpt-5.6-luna"',
|
|
736
|
+
'model_reasoning_effort = "low"',
|
|
724
737
|
'sandbox_mode = "read-only"',
|
|
725
738
|
`developer_instructions = ${JSON.stringify(recoveryOnly ? retiredAdapterInstructions(tenantId, agent) : codexAdapterInstructions(tenantId, agent))}`,
|
|
726
739
|
"",
|
|
727
740
|
`[mcp_servers.${MANAGED_AGENT_MCP_SERVER}]`,
|
|
728
741
|
`command = ${JSON.stringify(invocation.command)}`,
|
|
729
742
|
`args = [${invocation.args.map((argument) => JSON.stringify(argument)).join(", ")}]`,
|
|
730
|
-
|
|
743
|
+
...(eager
|
|
744
|
+
? [`enabled_tools = [${toolNames.map((tool) => JSON.stringify(tool)).join(", ")}]`]
|
|
745
|
+
: []),
|
|
731
746
|
...(envEntries ? [`env = { ${envEntries} }`] : []),
|
|
732
747
|
"",
|
|
733
748
|
];
|
package/src/cliProfiles.js
CHANGED
|
@@ -21,8 +21,12 @@ import { applyImpelClaudeSandbox } from "./claudeSandbox.js";
|
|
|
21
21
|
import { renameWithWindowsRetry } from "./windowsFs.js";
|
|
22
22
|
import { ensureClaudeSessionHooks, ensureCodexSessionHooks } from "./sessionHooks.js";
|
|
23
23
|
import { RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
24
|
+
import { managedCodexCliCatalog } from "./codexCliCatalog.js";
|
|
24
25
|
|
|
25
26
|
export const IMPEL_CLI_PROFILES_DIR = path.join(CONFIG_DIR, "cli");
|
|
27
|
+
export const CODEX_GATEWAY_TOKEN_ENV = `${RUNTIME_BRAND.cli.providerId
|
|
28
|
+
.toUpperCase()
|
|
29
|
+
.replace(/[^A-Z0-9]+/gu, "_")}_CODEX_GATEWAY_TOKEN`;
|
|
26
30
|
|
|
27
31
|
export function tenantCliProfilePaths(tenantId) {
|
|
28
32
|
const root = path.join(IMPEL_CLI_PROFILES_DIR, "tenants", normalizeTenantId(tenantId));
|
|
@@ -30,6 +34,7 @@ export function tenantCliProfilePaths(tenantId) {
|
|
|
30
34
|
root,
|
|
31
35
|
claudeConfigDir: path.join(root, "claude"),
|
|
32
36
|
codexHome: path.join(root, "codex"),
|
|
37
|
+
codexCatalog: path.join(root, "codex", "models.json"),
|
|
33
38
|
};
|
|
34
39
|
}
|
|
35
40
|
|
|
@@ -191,7 +196,6 @@ function splitTomlPreamble(text) {
|
|
|
191
196
|
}
|
|
192
197
|
|
|
193
198
|
function codexManagedBlock(gatewayUrl, tenantId) {
|
|
194
|
-
const auth = impelCliInvocation(["token", "--tenant", tenantId]);
|
|
195
199
|
const providerId = RUNTIME_BRAND.cli.providerId;
|
|
196
200
|
const lines = [
|
|
197
201
|
CODEX_START_MARK,
|
|
@@ -200,12 +204,7 @@ function codexManagedBlock(gatewayUrl, tenantId) {
|
|
|
200
204
|
`name = ${JSON.stringify(`${RUNTIME_BRAND.product.displayName} Gateway`)}`,
|
|
201
205
|
`base_url = ${JSON.stringify(impelCodexBaseUrl(gatewayUrl))}`,
|
|
202
206
|
'wire_api = "responses"',
|
|
203
|
-
|
|
204
|
-
`[model_providers.${providerId}.auth]`,
|
|
205
|
-
`command = ${JSON.stringify(auth.command)}`,
|
|
206
|
-
`args = [${auth.args.map((argument) => JSON.stringify(argument)).join(", ")}]`,
|
|
207
|
-
"timeout_ms = 5000",
|
|
208
|
-
"refresh_interval_ms = 300000",
|
|
207
|
+
`env_key = ${JSON.stringify(CODEX_GATEWAY_TOKEN_ENV)}`,
|
|
209
208
|
];
|
|
210
209
|
if (RUNTIME_BRAND.features.mcp) {
|
|
211
210
|
const mcp = impelCliInvocation(["mcp", "--tenant", tenantId]);
|
|
@@ -231,7 +230,7 @@ function codexManagedBlock(gatewayUrl, tenantId) {
|
|
|
231
230
|
|
|
232
231
|
export function ensureImpelCodexProfile(gatewayUrl, tenantId) {
|
|
233
232
|
secureAllManagedCodexHomes({ cliRoot: IMPEL_CLI_PROFILES_DIR });
|
|
234
|
-
const codexHome = tenantCliProfilePaths(tenantId)
|
|
233
|
+
const { codexHome, codexCatalog } = tenantCliProfilePaths(tenantId);
|
|
235
234
|
secureManagedCodexHome(codexHome);
|
|
236
235
|
const configPath = path.join(codexHome, "config.toml");
|
|
237
236
|
const original = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
|
|
@@ -259,6 +258,10 @@ export function ensureImpelCodexProfile(gatewayUrl, tenantId) {
|
|
|
259
258
|
let nextPreamble = CODEX_PROVIDER_LINE_RE.test(preamble)
|
|
260
259
|
? preamble.replace(CODEX_PROVIDER_LINE_RE, providerLine)
|
|
261
260
|
: `${preamble.trimEnd()}${preamble.trim() ? "\n" : ""}${providerLine}\n`;
|
|
261
|
+
const catalogLine = `model_catalog_json = ${JSON.stringify(codexCatalog)}`;
|
|
262
|
+
nextPreamble = /^model_catalog_json[ \t]*=/mu.test(nextPreamble)
|
|
263
|
+
? nextPreamble.replace(/^model_catalog_json[ \t]*=.*$/mu, catalogLine)
|
|
264
|
+
: `${nextPreamble.trimEnd()}\n${catalogLine}\n`;
|
|
262
265
|
nextPreamble = nextPreamble.replace(/^bypass_hook_trust[ \t]*=[ \t]*(?:true|false)[ \t]*\n?/gm, "");
|
|
263
266
|
const restText = rest.trim();
|
|
264
267
|
const next = [nextPreamble.trimEnd(), codexManagedBlock(gatewayUrl, tenantId), restText]
|
|
@@ -267,7 +270,8 @@ export function ensureImpelCodexProfile(gatewayUrl, tenantId) {
|
|
|
267
270
|
.concat("\n");
|
|
268
271
|
|
|
269
272
|
writePrivateFile(configPath, hardenManagedCodexToml(next, configPath));
|
|
273
|
+
writePrivateFile(codexCatalog, `${JSON.stringify(managedCodexCliCatalog(), null, 2)}\n`);
|
|
270
274
|
if (RUNTIME_BRAND.features.sessions) ensureCodexSessionHooks(codexHome, tenantId, "codex_cli");
|
|
271
275
|
secureManagedCodexHome(codexHome);
|
|
272
|
-
return { codexHome, configPath };
|
|
276
|
+
return { codexHome, configPath, catalogPath: codexCatalog };
|
|
273
277
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const LEVELS = ["low", "medium", "high", "xhigh"].map((effort) => ({
|
|
2
|
+
effort,
|
|
3
|
+
description: `${effort} reasoning effort`,
|
|
4
|
+
}));
|
|
5
|
+
|
|
6
|
+
const MODELS = [
|
|
7
|
+
["gpt-5.6-sol", "GPT-5.6-Sol", 1, "medium", "code_mode_only", "v2"],
|
|
8
|
+
["gpt-5.6-terra", "GPT-5.6-Terra", 2, "medium", "code_mode_only", "v2"],
|
|
9
|
+
["gpt-5.6-luna", "GPT-5.6-Luna", 3, "medium", "code_mode_only", "v1"],
|
|
10
|
+
["gpt-5.5", "GPT-5.5", 7, "medium", null, null],
|
|
11
|
+
["gpt-5.4", "GPT-5.4", 16, "medium", null, null],
|
|
12
|
+
["gpt-5.4-mini", "GPT-5.4-Mini", 23, "medium", null, null],
|
|
13
|
+
["gpt-5.3-codex-spark", "GPT-5.3-Codex-Spark", 26, "high", null, null],
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// Reviewed static projection of the exact pinned Codex 0.146 catalog. Keeping
|
|
17
|
+
// it process-local prevents the client from issuing unsupported online model
|
|
18
|
+
// refreshes during every managed launch.
|
|
19
|
+
export function managedCodexCliCatalog(now = new Date()) {
|
|
20
|
+
return {
|
|
21
|
+
fetched_at: now.toISOString(),
|
|
22
|
+
client_version: "impel-managed-codex-0.146",
|
|
23
|
+
models: MODELS.map(([slug, displayName, priority, effort, toolMode, multiAgentVersion]) => ({
|
|
24
|
+
slug,
|
|
25
|
+
display_name: displayName,
|
|
26
|
+
description: `Available through the Impel gateway (${slug}).`,
|
|
27
|
+
default_reasoning_level: effort,
|
|
28
|
+
supported_reasoning_levels: LEVELS,
|
|
29
|
+
service_tiers: [],
|
|
30
|
+
shell_type: "shell_command",
|
|
31
|
+
visibility: "list",
|
|
32
|
+
supported_in_api: true,
|
|
33
|
+
priority,
|
|
34
|
+
base_instructions: "You are Codex, an AI coding agent. Follow repository instructions and verify your work.",
|
|
35
|
+
include_skills_usage_instructions: false,
|
|
36
|
+
supports_reasoning_summaries: true,
|
|
37
|
+
default_reasoning_summary: "none",
|
|
38
|
+
support_verbosity: true,
|
|
39
|
+
default_verbosity: "low",
|
|
40
|
+
apply_patch_tool_type: "freeform",
|
|
41
|
+
web_search_tool_type: "text_and_image",
|
|
42
|
+
truncation_policy: { mode: "tokens", limit: 10_000 },
|
|
43
|
+
context_window: slug === "gpt-5.3-codex-spark" ? 128_000 : 272_000,
|
|
44
|
+
max_context_window: slug === "gpt-5.4" ? 1_000_000 : (slug === "gpt-5.3-codex-spark" ? 128_000 : 272_000),
|
|
45
|
+
effective_context_window_percent: 95,
|
|
46
|
+
experimental_supported_tools: [],
|
|
47
|
+
input_modalities: ["text", "image"],
|
|
48
|
+
supports_parallel_tool_calls: true,
|
|
49
|
+
supports_image_detail_original: true,
|
|
50
|
+
supports_search_tool: true,
|
|
51
|
+
use_responses_lite: slug.startsWith("gpt-5.6-"),
|
|
52
|
+
tool_mode: toolMode,
|
|
53
|
+
multi_agent_version: multiAgentVersion,
|
|
54
|
+
})),
|
|
55
|
+
};
|
|
56
|
+
}
|
package/src/commands/launch.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
CODEX_GATEWAY_TOKEN_ENV,
|
|
5
6
|
ensureImpelClaudeProfile,
|
|
6
7
|
ensureImpelCodexProfile,
|
|
7
8
|
} from "../cliProfiles.js";
|
|
@@ -40,7 +41,13 @@ const CLAUDE_DIRECT_AUTH_ENV = [
|
|
|
40
41
|
"CLAUDE_CODE_USE_VERTEX",
|
|
41
42
|
];
|
|
42
43
|
|
|
43
|
-
const CODEX_DIRECT_AUTH_ENV = [
|
|
44
|
+
const CODEX_DIRECT_AUTH_ENV = [
|
|
45
|
+
"CODEX_ACCESS_TOKEN",
|
|
46
|
+
"CODEX_API_KEY",
|
|
47
|
+
"OPENAI_API_KEY",
|
|
48
|
+
"OPENAI_BASE_URL",
|
|
49
|
+
CODEX_GATEWAY_TOKEN_ENV,
|
|
50
|
+
];
|
|
44
51
|
export {
|
|
45
52
|
escapeWindowsBareArgument,
|
|
46
53
|
escapeWindowsBatchArgument,
|
|
@@ -231,6 +238,7 @@ export async function cmdLaunch(tool, argv) {
|
|
|
231
238
|
agentProfile = { client: "codex", root: profile.codexHome, label: "Impel isolated Codex (impel codex)" };
|
|
232
239
|
deleteEnvironmentKeys(environment, CODEX_DIRECT_AUTH_ENV);
|
|
233
240
|
environment.CODEX_HOME = profile.codexHome;
|
|
241
|
+
environment[CODEX_GATEWAY_TOKEN_ENV] = gatewayCredential;
|
|
234
242
|
} else {
|
|
235
243
|
throw new Error(`unsupported CLI launcher: ${tool}`);
|
|
236
244
|
}
|
package/src/commands/mcp.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import readline from "node:readline";
|
|
2
3
|
|
|
3
4
|
import {
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
nativeAgentToolCallResult,
|
|
19
20
|
} from "../nativeAgentTransport.js";
|
|
20
21
|
import { IMPEL_NATIVE_AGENT_MCP_TARGET } from "../selfInvocation.js";
|
|
22
|
+
import { createNativeAgentTelemetry } from "../nativeAgentTelemetry.js";
|
|
21
23
|
import { ensureTenantSelection, normalizeTenantId, tenantCredential } from "../tenants.js";
|
|
22
24
|
|
|
23
25
|
const TASKS_TARGET = "tasks";
|
|
@@ -195,6 +197,7 @@ export function runNativeAgentMcpServer({
|
|
|
195
197
|
input = process.stdin,
|
|
196
198
|
output = process.stdout,
|
|
197
199
|
mode = "durable",
|
|
200
|
+
telemetry = () => {},
|
|
198
201
|
}) {
|
|
199
202
|
if (!["durable", "recovery", "answer"].includes(mode)) throw new Error("invalid native-agent MCP mode");
|
|
200
203
|
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
@@ -219,6 +222,9 @@ export function runNativeAgentMcpServer({
|
|
|
219
222
|
const controller = new AbortController();
|
|
220
223
|
active.set(message.id, controller);
|
|
221
224
|
const task = (async () => {
|
|
225
|
+
const correlationId = crypto.randomUUID();
|
|
226
|
+
const startedAt = Date.now();
|
|
227
|
+
let telemetryTool;
|
|
222
228
|
try {
|
|
223
229
|
if (message.method === "initialize") {
|
|
224
230
|
write(nativeRpcResult(message.id, {
|
|
@@ -242,7 +248,9 @@ export function runNativeAgentMcpServer({
|
|
|
242
248
|
throw new Error("unsupported native-agent MCP method");
|
|
243
249
|
}
|
|
244
250
|
const name = message.params?.name;
|
|
251
|
+
telemetryTool = name;
|
|
245
252
|
const args = message.params?.arguments ?? {};
|
|
253
|
+
telemetry("local_tool_received", { correlationId, tool: name });
|
|
246
254
|
const progressToken = nativeProgressToken(message);
|
|
247
255
|
const onProgress = progressToken === null ? undefined : ({ status }) => {
|
|
248
256
|
progress += 1;
|
|
@@ -261,17 +269,32 @@ export function runNativeAgentMcpServer({
|
|
|
261
269
|
if (mode === "answer") throw new Error("direct-answer native-agent bindings cannot start durable runs");
|
|
262
270
|
value = await transport.run(args, { signal: controller.signal, onProgress });
|
|
263
271
|
} else if (name === NATIVE_AGENT_RESUME_TOOL) {
|
|
264
|
-
if (mode === "answer") throw new Error("direct-answer native-agent bindings have no runs to resume");
|
|
265
272
|
value = await transport.resume(args, { signal: controller.signal, onProgress });
|
|
266
273
|
} else if (name === NATIVE_AGENT_RECOVER_TOOL) {
|
|
267
|
-
if (mode === "answer") throw new Error("direct-answer native-agent bindings have no runs to recover");
|
|
268
274
|
value = transport.recover(args);
|
|
269
275
|
} else {
|
|
270
276
|
throw new Error("unsupported native-agent MCP tool");
|
|
271
277
|
}
|
|
278
|
+
const continued = value?.schema === "impel.native-agent-run.v1";
|
|
279
|
+
telemetry("local_tool_completed", {
|
|
280
|
+
correlationId,
|
|
281
|
+
tool: name,
|
|
282
|
+
outcome: continued ? "continued" : "succeeded",
|
|
283
|
+
status: typeof value?.status === "string" ? value.status : undefined,
|
|
284
|
+
durationMs: Date.now() - startedAt,
|
|
285
|
+
durableRunCreated: Boolean(value?.runId || value?.handle?.runId),
|
|
286
|
+
});
|
|
272
287
|
write(nativeRpcResult(message.id, nativeAgentToolCallResult(value)));
|
|
273
288
|
} catch (error) {
|
|
274
289
|
const cancelled = error?.name === "AbortError";
|
|
290
|
+
if (message.method === "tools/call") {
|
|
291
|
+
telemetry("local_tool_completed", {
|
|
292
|
+
correlationId,
|
|
293
|
+
tool: telemetryTool,
|
|
294
|
+
outcome: cancelled ? "cancelled" : "failed",
|
|
295
|
+
durationMs: Date.now() - startedAt,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
275
298
|
write(JSON.stringify({
|
|
276
299
|
jsonrpc: "2.0",
|
|
277
300
|
id: message.id,
|
|
@@ -369,6 +392,15 @@ export async function cmdMcp(argv = []) {
|
|
|
369
392
|
const gatewayUrl = config.gatewayUrl || resolveDefaultGateway();
|
|
370
393
|
const credential = tasksTarget ? null : tenantCredential(config.pat, tenantId);
|
|
371
394
|
if (nativeAgentTarget) {
|
|
395
|
+
const telemetry = createNativeAgentTelemetry({
|
|
396
|
+
tenantId,
|
|
397
|
+
agentId: flags["agent-id"],
|
|
398
|
+
scopeParam: flags["scope-param"],
|
|
399
|
+
mode: flags["recovery-only"] === true
|
|
400
|
+
? "recovery"
|
|
401
|
+
: (flags["answer-only"] === true ? "answer" : "durable"),
|
|
402
|
+
});
|
|
403
|
+
telemetry("local_server_started");
|
|
372
404
|
return runNativeAgentMcpServer({
|
|
373
405
|
transport: new NativeAgentCompositeTransport({
|
|
374
406
|
tenantId,
|
|
@@ -377,10 +409,12 @@ export async function cmdMcp(argv = []) {
|
|
|
377
409
|
policyFingerprint: flags["policy-fingerprint"],
|
|
378
410
|
gatewayUrl,
|
|
379
411
|
credential,
|
|
412
|
+
telemetry,
|
|
380
413
|
}),
|
|
381
414
|
mode: flags["recovery-only"] === true
|
|
382
415
|
? "recovery"
|
|
383
416
|
: (flags["answer-only"] === true ? "answer" : "durable"),
|
|
417
|
+
telemetry,
|
|
384
418
|
});
|
|
385
419
|
}
|
|
386
420
|
const endpoint = tasksTarget ? null : `${gatewayUrl}/mcp`;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { redactCredentialText, redactSecretText } from "./config.js";
|
|
5
|
+
|
|
6
|
+
export const NATIVE_AGENT_TRANSPORT_VERSION = 2;
|
|
7
|
+
|
|
8
|
+
const SAFE_ID = /^[A-Za-z0-9_.:@/-]{1,200}$/u;
|
|
9
|
+
const SAFE_EVENTS = new Set([
|
|
10
|
+
"local_server_started",
|
|
11
|
+
"local_tool_received",
|
|
12
|
+
"local_tool_completed",
|
|
13
|
+
"session_prepared",
|
|
14
|
+
"upstream_request_completed",
|
|
15
|
+
"binding_completed",
|
|
16
|
+
]);
|
|
17
|
+
const SAFE_TOOLS = new Set([
|
|
18
|
+
"initialize",
|
|
19
|
+
"notifications/initialized",
|
|
20
|
+
"tools/list",
|
|
21
|
+
"get_native_agent",
|
|
22
|
+
"answer_native_agent",
|
|
23
|
+
"start_native_agent_run",
|
|
24
|
+
"read_native_agent_run",
|
|
25
|
+
"run_native_agent",
|
|
26
|
+
"resume_native_agent_run",
|
|
27
|
+
"recover_native_agent_runs",
|
|
28
|
+
]);
|
|
29
|
+
const SAFE_OUTCOMES = new Set(["succeeded", "continued", "failed", "cancelled", "ok", "error"]);
|
|
30
|
+
|
|
31
|
+
function safeId(value) {
|
|
32
|
+
return typeof value === "string" && SAFE_ID.test(value) ? value : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function safeInteger(value) {
|
|
36
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function appendPrivateJsonLine(filePath, value) {
|
|
40
|
+
const directory = path.dirname(filePath);
|
|
41
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
42
|
+
try {
|
|
43
|
+
fs.chmodSync(directory, 0o700);
|
|
44
|
+
} catch {
|
|
45
|
+
// Best effort on Windows.
|
|
46
|
+
}
|
|
47
|
+
const serialized = JSON.stringify(value);
|
|
48
|
+
if (redactCredentialText(serialized) !== serialized || redactSecretText(serialized) !== serialized) {
|
|
49
|
+
throw new Error("refusing to record sensitive native-agent telemetry");
|
|
50
|
+
}
|
|
51
|
+
fs.appendFileSync(filePath, `${serialized}\n`, { encoding: "utf8", mode: 0o600 });
|
|
52
|
+
try {
|
|
53
|
+
fs.chmodSync(filePath, 0o600);
|
|
54
|
+
} catch {
|
|
55
|
+
// Best effort on Windows.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates an opt-in, closed-schema performance recorder. It accepts identifiers,
|
|
61
|
+
* counters, durations, and terminal classes only; prompt and result fields do
|
|
62
|
+
* not exist in the schema.
|
|
63
|
+
*/
|
|
64
|
+
export function createNativeAgentTelemetry({
|
|
65
|
+
tenantId,
|
|
66
|
+
agentId,
|
|
67
|
+
scopeParam,
|
|
68
|
+
mode,
|
|
69
|
+
filePath = process.env.IMPEL_NATIVE_AGENT_TELEMETRY_PATH,
|
|
70
|
+
host = process.env.IMPEL_NATIVE_HOST,
|
|
71
|
+
hostBuild = process.env.IMPEL_NATIVE_HOST_BUILD,
|
|
72
|
+
now = Date.now,
|
|
73
|
+
} = {}) {
|
|
74
|
+
if (!filePath) return () => {};
|
|
75
|
+
const fixed = {
|
|
76
|
+
schema: "impel.native-agent-telemetry.v1",
|
|
77
|
+
transportVersion: NATIVE_AGENT_TRANSPORT_VERSION,
|
|
78
|
+
tenantId: safeId(tenantId),
|
|
79
|
+
agentId: safeId(agentId),
|
|
80
|
+
scopeParam: safeId(scopeParam),
|
|
81
|
+
mode: safeId(mode),
|
|
82
|
+
host: safeId(host),
|
|
83
|
+
hostBuild: safeId(hostBuild),
|
|
84
|
+
};
|
|
85
|
+
return (event, fields = {}) => {
|
|
86
|
+
if (!SAFE_EVENTS.has(event)) return;
|
|
87
|
+
const record = {
|
|
88
|
+
...fixed,
|
|
89
|
+
event,
|
|
90
|
+
timestamp: new Date(now()).toISOString(),
|
|
91
|
+
correlationId: safeId(fields.correlationId),
|
|
92
|
+
gatewayRequestId: safeId(fields.gatewayRequestId),
|
|
93
|
+
tool: SAFE_TOOLS.has(fields.tool) ? fields.tool : undefined,
|
|
94
|
+
outcome: SAFE_OUTCOMES.has(fields.outcome) ? fields.outcome : undefined,
|
|
95
|
+
status: safeId(fields.status),
|
|
96
|
+
durationMs: safeInteger(fields.durationMs),
|
|
97
|
+
pollCount: safeInteger(fields.pollCount),
|
|
98
|
+
handshakeReused: typeof fields.handshakeReused === "boolean" ? fields.handshakeReused : undefined,
|
|
99
|
+
bindingReused: typeof fields.bindingReused === "boolean" ? fields.bindingReused : undefined,
|
|
100
|
+
durableRunCreated: typeof fields.durableRunCreated === "boolean" ? fields.durableRunCreated : undefined,
|
|
101
|
+
};
|
|
102
|
+
try {
|
|
103
|
+
appendPrivateJsonLine(filePath, Object.fromEntries(
|
|
104
|
+
Object.entries(record).filter(([, value]) => value !== undefined),
|
|
105
|
+
));
|
|
106
|
+
} catch {
|
|
107
|
+
// Performance telemetry is opt-in and best effort; it cannot break a run.
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "./config.js";
|
|
11
11
|
import {
|
|
12
12
|
NATIVE_AGENT_ANSWER_TOOL,
|
|
13
|
-
|
|
13
|
+
NATIVE_AGENT_GET_TOOL,
|
|
14
14
|
NATIVE_AGENT_READ_TOOL,
|
|
15
15
|
NATIVE_AGENT_RECOVER_TOOL,
|
|
16
16
|
NATIVE_AGENT_RESUME_TOOL,
|
|
@@ -41,9 +41,9 @@ const SAFE_BINDING_RE = /^[A-Za-z0-9_.:-]{1,160}$/u;
|
|
|
41
41
|
const SAFE_FINGERPRINT_RE = /^[a-f0-9]{64}$/u;
|
|
42
42
|
const SAFE_INVOCATION_RE = /^[a-f0-9-]{36}$/u;
|
|
43
43
|
const DEFAULT_WAIT_SECONDS = 20;
|
|
44
|
-
const MAX_WAIT_SECONDS =
|
|
45
|
-
const DEFAULT_ATTACHMENT_WINDOW_MS =
|
|
46
|
-
const DEFAULT_UPSTREAM_TIMEOUT_MS =
|
|
44
|
+
const MAX_WAIT_SECONDS = 35;
|
|
45
|
+
const DEFAULT_ATTACHMENT_WINDOW_MS = 40_000;
|
|
46
|
+
const DEFAULT_UPSTREAM_TIMEOUT_MS = 42_000;
|
|
47
47
|
const DEFAULT_MAX_POLLS = 8;
|
|
48
48
|
const DEFAULT_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
49
49
|
const DEFAULT_MAX_RETAINED_RUNS = 500;
|
|
@@ -288,6 +288,7 @@ function readPrivateState(filePath, maxBytes) {
|
|
|
288
288
|
"upstreamRunId",
|
|
289
289
|
"status",
|
|
290
290
|
"startAttempts",
|
|
291
|
+
"startMode",
|
|
291
292
|
"createdAt",
|
|
292
293
|
"updatedAt",
|
|
293
294
|
"fence",
|
|
@@ -296,6 +297,7 @@ function readPrivateState(filePath, maxBytes) {
|
|
|
296
297
|
"result",
|
|
297
298
|
"output",
|
|
298
299
|
"error",
|
|
300
|
+
"version",
|
|
299
301
|
], "native-agent run state");
|
|
300
302
|
if (!SAFE_INVOCATION_RE.test(state.invocationId)
|
|
301
303
|
|| normalizeTenantId(state.tenantId) !== state.tenantId
|
|
@@ -309,10 +311,14 @@ function readPrivateState(filePath, maxBytes) {
|
|
|
309
311
|
|| !state.status
|
|
310
312
|
|| !Number.isSafeInteger(state.startAttempts)
|
|
311
313
|
|| state.startAttempts < 0
|
|
314
|
+
|| (state.startMode !== undefined
|
|
315
|
+
&& state.startMode !== "durable"
|
|
316
|
+
&& state.startMode !== "answer")
|
|
312
317
|
|| !Number.isFinite(Date.parse(state.createdAt))
|
|
313
318
|
|| !Number.isFinite(Date.parse(state.updatedAt))
|
|
314
319
|
|| (state.fence !== undefined && (!Number.isSafeInteger(state.fence) || state.fence < 0))
|
|
315
320
|
|| (state.sequence !== undefined && (!Number.isSafeInteger(state.sequence) || state.sequence < 0))
|
|
321
|
+
|| (state.version !== undefined && (!Number.isSafeInteger(state.version) || state.version < 0))
|
|
316
322
|
|| (state.upstreamRunId !== null && (typeof state.upstreamRunId !== "string" || !state.upstreamRunId))) {
|
|
317
323
|
throw new Error("invalid field");
|
|
318
324
|
}
|
|
@@ -449,10 +455,13 @@ function applyUpstreamPayload(state, payload, now) {
|
|
|
449
455
|
if (Object.hasOwn(payload, "error")) {
|
|
450
456
|
state.error = redactOpaqueValue(payload.error, state.idempotencyKey);
|
|
451
457
|
}
|
|
458
|
+
if (Number.isSafeInteger(payload.version) && payload.version >= 0) {
|
|
459
|
+
state.version = payload.version;
|
|
460
|
+
}
|
|
452
461
|
state.updatedAt = new Date(now).toISOString();
|
|
453
462
|
}
|
|
454
463
|
|
|
455
|
-
/** Use the read tool's advertised integer maximum, never exceeding
|
|
464
|
+
/** Use the read tool's advertised integer maximum, never exceeding 35 seconds. */
|
|
456
465
|
export function negotiateNativeAgentWaitSeconds(tools) {
|
|
457
466
|
const readTool = Array.isArray(tools)
|
|
458
467
|
? tools.find((tool) => tool?.name === NATIVE_AGENT_READ_TOOL)
|
|
@@ -575,6 +584,9 @@ export class NativeAgentUpstreamSession {
|
|
|
575
584
|
timeoutMs = DEFAULT_UPSTREAM_TIMEOUT_MS,
|
|
576
585
|
maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES,
|
|
577
586
|
signal,
|
|
587
|
+
telemetry = () => {},
|
|
588
|
+
now = Date.now,
|
|
589
|
+
requestIdFactory = crypto.randomUUID,
|
|
578
590
|
}) {
|
|
579
591
|
this.endpoint = `${normalizeGatewayUrl(gatewayUrl)}/mcp`;
|
|
580
592
|
this.credential = credential;
|
|
@@ -582,16 +594,24 @@ export class NativeAgentUpstreamSession {
|
|
|
582
594
|
this.timeoutMs = timeoutMs;
|
|
583
595
|
this.maxResponseBytes = maxResponseBytes;
|
|
584
596
|
this.signal = signal;
|
|
597
|
+
this.telemetry = telemetry;
|
|
598
|
+
this.now = now;
|
|
599
|
+
this.requestIdFactory = requestIdFactory;
|
|
585
600
|
this.sessionId = null;
|
|
586
601
|
this.nextId = 1;
|
|
587
602
|
}
|
|
588
603
|
|
|
589
|
-
async post(message) {
|
|
590
|
-
throwIfAborted(
|
|
604
|
+
async post(message, { signal = this.signal } = {}) {
|
|
605
|
+
throwIfAborted(signal);
|
|
606
|
+
const startedAt = this.now();
|
|
607
|
+
const correlationId = this.requestIdFactory();
|
|
608
|
+
const tool = message.method === "tools/call"
|
|
609
|
+
? String(message.params?.name || "").replace(/^impel_specialists-/u, "")
|
|
610
|
+
: message.method;
|
|
591
611
|
const controller = new AbortController();
|
|
592
612
|
let timedOut = false;
|
|
593
613
|
const onAbort = () => controller.abort();
|
|
594
|
-
|
|
614
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
595
615
|
const timeout = setTimeout(() => {
|
|
596
616
|
timedOut = true;
|
|
597
617
|
controller.abort();
|
|
@@ -605,6 +625,7 @@ export class NativeAgentUpstreamSession {
|
|
|
605
625
|
Authorization: `Bearer ${this.credential}`,
|
|
606
626
|
"Content-Type": "application/json",
|
|
607
627
|
Accept: "application/json, text/event-stream",
|
|
628
|
+
"X-Impel-Client-Request-Id": correlationId,
|
|
608
629
|
...(this.sessionId ? { "Mcp-Session-Id": this.sessionId } : {}),
|
|
609
630
|
},
|
|
610
631
|
body: JSON.stringify(message),
|
|
@@ -612,13 +633,26 @@ export class NativeAgentUpstreamSession {
|
|
|
612
633
|
});
|
|
613
634
|
body = await readBoundedResponseBody(response, this.maxResponseBytes);
|
|
614
635
|
} catch (error) {
|
|
615
|
-
|
|
636
|
+
this.telemetry("upstream_request_completed", {
|
|
637
|
+
correlationId,
|
|
638
|
+
tool,
|
|
639
|
+
outcome: signal?.aborted ? "cancelled" : "error",
|
|
640
|
+
durationMs: this.now() - startedAt,
|
|
641
|
+
});
|
|
642
|
+
if (signal?.aborted) throw abortError();
|
|
616
643
|
const detail = timedOut ? "request timed out" : redactSecretText(error?.message || error);
|
|
617
644
|
throw new NativeAgentUpstreamError(`native-agent MCP request failed: ${detail}`, { ambiguous: true });
|
|
618
645
|
} finally {
|
|
619
646
|
clearTimeout(timeout);
|
|
620
|
-
|
|
621
|
-
}
|
|
647
|
+
signal?.removeEventListener("abort", onAbort);
|
|
648
|
+
}
|
|
649
|
+
this.telemetry("upstream_request_completed", {
|
|
650
|
+
correlationId,
|
|
651
|
+
gatewayRequestId: response.headers.get("x-request-id") || undefined,
|
|
652
|
+
tool,
|
|
653
|
+
outcome: response.ok ? "ok" : "error",
|
|
654
|
+
durationMs: this.now() - startedAt,
|
|
655
|
+
});
|
|
622
656
|
this.sessionId = response.headers.get("mcp-session-id") || this.sessionId;
|
|
623
657
|
if (!response.ok) {
|
|
624
658
|
throw new NativeAgentUpstreamError(`native-agent MCP returned HTTP ${response.status}`, {
|
|
@@ -634,7 +668,7 @@ export class NativeAgentUpstreamSession {
|
|
|
634
668
|
}
|
|
635
669
|
}
|
|
636
670
|
|
|
637
|
-
async initialize() {
|
|
671
|
+
async initialize({ signal } = {}) {
|
|
638
672
|
const id = this.nextId++;
|
|
639
673
|
const initialized = await this.post({
|
|
640
674
|
jsonrpc: "2.0",
|
|
@@ -645,14 +679,17 @@ export class NativeAgentUpstreamSession {
|
|
|
645
679
|
capabilities: {},
|
|
646
680
|
clientInfo: { name: "impel-cli-native-agent-transport", version: "1.0.0" },
|
|
647
681
|
},
|
|
648
|
-
});
|
|
682
|
+
}, { signal });
|
|
649
683
|
const response = initialized.find((message) => message?.id === id);
|
|
650
684
|
if (!response?.result || response.error) {
|
|
651
685
|
throw new NativeAgentUpstreamError("native-agent MCP initialization failed");
|
|
652
686
|
}
|
|
653
|
-
await this.post({ jsonrpc: "2.0", method: "notifications/initialized", params: {} });
|
|
687
|
+
await this.post({ jsonrpc: "2.0", method: "notifications/initialized", params: {} }, { signal });
|
|
654
688
|
const toolsId = this.nextId++;
|
|
655
|
-
const listed = await this.post(
|
|
689
|
+
const listed = await this.post(
|
|
690
|
+
{ jsonrpc: "2.0", id: toolsId, method: "tools/list", params: {} },
|
|
691
|
+
{ signal },
|
|
692
|
+
);
|
|
656
693
|
const toolsResponse = listed.find((message) => message?.id === toolsId);
|
|
657
694
|
if (toolsResponse?.error || !Array.isArray(toolsResponse?.result?.tools)) {
|
|
658
695
|
throw new NativeAgentUpstreamError("native-agent MCP returned no tool catalog");
|
|
@@ -660,7 +697,7 @@ export class NativeAgentUpstreamSession {
|
|
|
660
697
|
return toolsResponse.result.tools;
|
|
661
698
|
}
|
|
662
699
|
|
|
663
|
-
async call(name, args) {
|
|
700
|
+
async call(name, args, { signal } = {}) {
|
|
664
701
|
const id = this.nextId++;
|
|
665
702
|
try {
|
|
666
703
|
const messages = await this.post({
|
|
@@ -668,7 +705,7 @@ export class NativeAgentUpstreamSession {
|
|
|
668
705
|
id,
|
|
669
706
|
method: "tools/call",
|
|
670
707
|
params: { name, arguments: args },
|
|
671
|
-
});
|
|
708
|
+
}, { signal });
|
|
672
709
|
return toolPayload(messages.find((message) => message?.id === id));
|
|
673
710
|
} catch (error) {
|
|
674
711
|
if (name === NATIVE_AGENT_START_TOOL
|
|
@@ -715,6 +752,7 @@ export class NativeAgentCompositeTransport {
|
|
|
715
752
|
beforeStateRename,
|
|
716
753
|
beforeRevisionCompaction,
|
|
717
754
|
beforePruneDelete,
|
|
755
|
+
telemetry = () => {},
|
|
718
756
|
}) {
|
|
719
757
|
this.tenantId = normalizeTenantId(tenantId);
|
|
720
758
|
for (const [field, value] of Object.entries({ agentId, scopeParam })) {
|
|
@@ -756,14 +794,19 @@ export class NativeAgentCompositeTransport {
|
|
|
756
794
|
this.beforeStateRename = beforeStateRename;
|
|
757
795
|
this.beforeRevisionCompaction = beforeRevisionCompaction;
|
|
758
796
|
this.beforePruneDelete = beforePruneDelete;
|
|
759
|
-
this.
|
|
797
|
+
this.telemetry = telemetry;
|
|
798
|
+
this.sessionFactory = sessionFactory || (() => new NativeAgentUpstreamSession({
|
|
760
799
|
gatewayUrl: this.gatewayUrl,
|
|
761
800
|
credential: this.credential,
|
|
762
801
|
fetchImpl: this.fetchImpl,
|
|
763
802
|
timeoutMs: this.upstreamTimeoutMs,
|
|
764
803
|
maxResponseBytes: this.maxResponseBytes,
|
|
765
|
-
|
|
804
|
+
telemetry: this.telemetry,
|
|
805
|
+
now: this.now,
|
|
806
|
+
requestIdFactory: this.randomUUID,
|
|
766
807
|
}));
|
|
808
|
+
this.preparedSession = null;
|
|
809
|
+
this.preparedAgent = null;
|
|
767
810
|
}
|
|
768
811
|
|
|
769
812
|
statePath(invocationId) {
|
|
@@ -1505,15 +1548,38 @@ export class NativeAgentCompositeTransport {
|
|
|
1505
1548
|
}
|
|
1506
1549
|
|
|
1507
1550
|
async prepareExistingSession(signal) {
|
|
1508
|
-
const
|
|
1509
|
-
const
|
|
1510
|
-
|
|
1551
|
+
const reused = Boolean(this.preparedSession);
|
|
1552
|
+
const startedAt = this.now();
|
|
1553
|
+
if (!this.preparedSession) {
|
|
1554
|
+
this.preparedSession = (async () => {
|
|
1555
|
+
const session = await this.sessionFactory({ signal });
|
|
1556
|
+
const tools = await session.initialize({ signal });
|
|
1557
|
+
return { session, waitSeconds: negotiateNativeAgentWaitSeconds(tools) };
|
|
1558
|
+
})();
|
|
1559
|
+
this.preparedSession.catch(() => {
|
|
1560
|
+
this.preparedSession = null;
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
const prepared = await this.preparedSession;
|
|
1564
|
+
this.telemetry("session_prepared", {
|
|
1565
|
+
durationMs: this.now() - startedAt,
|
|
1566
|
+
handshakeReused: reused,
|
|
1567
|
+
});
|
|
1568
|
+
return prepared;
|
|
1511
1569
|
}
|
|
1512
1570
|
|
|
1513
1571
|
async prepareNewSession(signal) {
|
|
1514
1572
|
const prepared = await this.prepareExistingSession(signal);
|
|
1573
|
+
const startedAt = this.now();
|
|
1574
|
+
if (this.preparedAgent) {
|
|
1575
|
+
this.telemetry("binding_completed", { durationMs: 0, bindingReused: true });
|
|
1576
|
+
return { ...prepared, agent: this.preparedAgent };
|
|
1577
|
+
}
|
|
1515
1578
|
const catalog = normalizeNativeAgentCatalog(
|
|
1516
|
-
await prepared.session.call(
|
|
1579
|
+
await prepared.session.call(NATIVE_AGENT_GET_TOOL, {
|
|
1580
|
+
agentId: this.agentId,
|
|
1581
|
+
scopeParam: this.scopeParam,
|
|
1582
|
+
}, { signal }),
|
|
1517
1583
|
this.tenantId,
|
|
1518
1584
|
);
|
|
1519
1585
|
const matches = catalog.agents.filter((agent) =>
|
|
@@ -1523,13 +1589,21 @@ export class NativeAgentCompositeTransport {
|
|
|
1523
1589
|
if (nativeAgentPolicyFingerprint(matches[0]) !== this.policyFingerprint) {
|
|
1524
1590
|
throw new Error("native-agent catalog policy fingerprint changed; synchronize agents again");
|
|
1525
1591
|
}
|
|
1526
|
-
|
|
1592
|
+
this.preparedAgent = matches[0];
|
|
1593
|
+
this.telemetry("binding_completed", {
|
|
1594
|
+
durationMs: this.now() - startedAt,
|
|
1595
|
+
bindingReused: false,
|
|
1596
|
+
});
|
|
1597
|
+
return { ...prepared, agent: this.preparedAgent };
|
|
1527
1598
|
}
|
|
1528
1599
|
|
|
1529
1600
|
async preparePersistedStartSession(signal, state) {
|
|
1530
1601
|
const prepared = await this.prepareExistingSession(signal);
|
|
1531
1602
|
const catalog = normalizeNativeAgentCatalog(
|
|
1532
|
-
await prepared.session.call(
|
|
1603
|
+
await prepared.session.call(NATIVE_AGENT_GET_TOOL, {
|
|
1604
|
+
agentId: state.agentId,
|
|
1605
|
+
scopeParam: state.scopeParam,
|
|
1606
|
+
}, { signal }),
|
|
1533
1607
|
this.tenantId,
|
|
1534
1608
|
);
|
|
1535
1609
|
const matches = catalog.agents.filter((agent) =>
|
|
@@ -1586,47 +1660,14 @@ export class NativeAgentCompositeTransport {
|
|
|
1586
1660
|
};
|
|
1587
1661
|
}
|
|
1588
1662
|
|
|
1589
|
-
|
|
1590
|
-
throwIfAborted(signal);
|
|
1591
|
-
const prepared = await this.prepareNewSession(signal);
|
|
1592
|
-
if (prepared.agent.directAnswer !== true) {
|
|
1593
|
-
throw new Error("native-agent binding is not configured for direct answers");
|
|
1594
|
-
}
|
|
1595
|
-
if (prepared.agent.sideEffects === "writes") {
|
|
1596
|
-
throw new Error(
|
|
1597
|
-
"write-capable native agents must use start/resume instead of answer_native_agent",
|
|
1598
|
-
);
|
|
1599
|
-
}
|
|
1600
|
-
const payload = await prepared.session.call(
|
|
1601
|
-
NATIVE_AGENT_UPSTREAM_ANSWER_TOOL,
|
|
1602
|
-
this.normalizeAnswerArguments(args, prepared.agent),
|
|
1603
|
-
);
|
|
1604
|
-
const finalText = extractAnswerFinalText(payload);
|
|
1605
|
-
if (finalText === null) throw new Error("native-agent answer returned no forUser or answer");
|
|
1606
|
-
return {
|
|
1607
|
-
...(typeof payload.forUser === "string" && payload.forUser.trim()
|
|
1608
|
-
? { forUser: payload.forUser }
|
|
1609
|
-
: {}),
|
|
1610
|
-
...(typeof payload.answer === "string" && payload.answer.trim()
|
|
1611
|
-
? { answer: payload.answer }
|
|
1612
|
-
: {}),
|
|
1613
|
-
agentId: this.agentId,
|
|
1614
|
-
scopeParam: this.scopeParam,
|
|
1615
|
-
};
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
async run(args, { signal, onProgress } = {}) {
|
|
1619
|
-
throwIfAborted(signal);
|
|
1620
|
-
this.ensureRoots();
|
|
1621
|
-
const attachment = boundedAttachmentSignal(signal, this.attachmentWindowMs);
|
|
1622
|
-
let state = null;
|
|
1623
|
-
let lock = null;
|
|
1663
|
+
allocateState(startArguments, { startMode = "durable" } = {}) {
|
|
1624
1664
|
let admission = null;
|
|
1625
1665
|
try {
|
|
1626
|
-
|
|
1627
|
-
const startArguments = this.normalizeRunArguments(args, prepared.agent);
|
|
1666
|
+
this.ensureRoots();
|
|
1628
1667
|
admission = this.acquireAdmissionLock();
|
|
1629
|
-
if (!admission)
|
|
1668
|
+
if (!admission) {
|
|
1669
|
+
throw new Error("native-agent run allocation is busy; retry without changing the request");
|
|
1670
|
+
}
|
|
1630
1671
|
this.prune(this.now(), { reserve: 1, guard: () => admission.assert() });
|
|
1631
1672
|
admission.assert();
|
|
1632
1673
|
const invocationId = this.randomUUID();
|
|
@@ -1640,7 +1681,7 @@ export class NativeAgentCompositeTransport {
|
|
|
1640
1681
|
startArguments,
|
|
1641
1682
|
});
|
|
1642
1683
|
const timestamp = new Date(this.now()).toISOString();
|
|
1643
|
-
state = {
|
|
1684
|
+
const state = {
|
|
1644
1685
|
schema: STATE_SCHEMA,
|
|
1645
1686
|
invocationId,
|
|
1646
1687
|
tenantId: this.tenantId,
|
|
@@ -1653,15 +1694,83 @@ export class NativeAgentCompositeTransport {
|
|
|
1653
1694
|
upstreamRunId: null,
|
|
1654
1695
|
status: "starting",
|
|
1655
1696
|
startAttempts: 0,
|
|
1697
|
+
...(startMode === "answer" ? { startMode } : {}),
|
|
1656
1698
|
createdAt: timestamp,
|
|
1657
1699
|
updatedAt: timestamp,
|
|
1658
1700
|
};
|
|
1701
|
+
this.persist(state);
|
|
1702
|
+
return state;
|
|
1703
|
+
} finally {
|
|
1704
|
+
admission?.release();
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
async answer(args, { signal } = {}) {
|
|
1709
|
+
throwIfAborted(signal);
|
|
1710
|
+
const attachment = boundedAttachmentSignal(signal, this.attachmentWindowMs);
|
|
1711
|
+
let state = null;
|
|
1712
|
+
let lock = null;
|
|
1713
|
+
try {
|
|
1714
|
+
const prepared = await this.prepareNewSession(attachment.signal);
|
|
1715
|
+
if (prepared.agent.directAnswer !== true) {
|
|
1716
|
+
throw new Error("native-agent binding is not configured for direct answers");
|
|
1717
|
+
}
|
|
1718
|
+
if (prepared.agent.sideEffects === "writes") {
|
|
1719
|
+
throw new Error(
|
|
1720
|
+
"write-capable native agents must use start/resume instead of answer_native_agent",
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
const answerArguments = this.normalizeAnswerArguments(args, prepared.agent);
|
|
1724
|
+
const startArguments = this.normalizeRunArguments({
|
|
1725
|
+
task: answerArguments.question,
|
|
1726
|
+
...(args.context !== undefined ? { context: args.context } : {}),
|
|
1727
|
+
contextKeys: args.contextKeys ?? [],
|
|
1728
|
+
}, prepared.agent);
|
|
1729
|
+
state = this.allocateState(startArguments, { startMode: "answer" });
|
|
1730
|
+
lock = this.acquireLock(state.invocationId);
|
|
1731
|
+
if (!lock) return handleForState(state);
|
|
1732
|
+
const result = await this.attach(state, prepared, {
|
|
1733
|
+
signal: attachment.signal,
|
|
1734
|
+
lock,
|
|
1735
|
+
});
|
|
1736
|
+
if (result?.schema === NATIVE_AGENT_RESULT_SCHEMA && result.status === "succeeded") {
|
|
1737
|
+
return {
|
|
1738
|
+
forUser: result.finalText,
|
|
1739
|
+
answer: result.finalText,
|
|
1740
|
+
agentId: this.agentId,
|
|
1741
|
+
scopeParam: this.scopeParam,
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
return result;
|
|
1745
|
+
} catch (error) {
|
|
1746
|
+
if (!state) throw error;
|
|
1747
|
+
if (lock) {
|
|
1748
|
+
try {
|
|
1749
|
+
this.persistDiagnostic(state, error, lock);
|
|
1750
|
+
} catch {
|
|
1751
|
+
// The persisted handle remains authoritative after a lost lease.
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
return handleForState(state);
|
|
1755
|
+
} finally {
|
|
1756
|
+
lock?.release();
|
|
1757
|
+
attachment.dispose();
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
async run(args, { signal, onProgress } = {}) {
|
|
1762
|
+
throwIfAborted(signal);
|
|
1763
|
+
this.ensureRoots();
|
|
1764
|
+
const attachment = boundedAttachmentSignal(signal, this.attachmentWindowMs);
|
|
1765
|
+
let state = null;
|
|
1766
|
+
let lock = null;
|
|
1767
|
+
try {
|
|
1768
|
+
const prepared = await this.prepareNewSession(attachment.signal);
|
|
1769
|
+
const startArguments = this.normalizeRunArguments(args, prepared.agent);
|
|
1770
|
+
state = this.allocateState(startArguments);
|
|
1659
1771
|
// The durable identity and exact retry arguments exist before the first
|
|
1660
1772
|
// upstream byte is sent.
|
|
1661
|
-
this.
|
|
1662
|
-
admission.release();
|
|
1663
|
-
admission = null;
|
|
1664
|
-
lock = this.acquireLock(invocationId);
|
|
1773
|
+
lock = this.acquireLock(state.invocationId);
|
|
1665
1774
|
if (!lock) return handleForState(state);
|
|
1666
1775
|
return await this.attach(state, prepared, {
|
|
1667
1776
|
signal: attachment.signal,
|
|
@@ -1677,7 +1786,6 @@ export class NativeAgentCompositeTransport {
|
|
|
1677
1786
|
}
|
|
1678
1787
|
throw error;
|
|
1679
1788
|
} finally {
|
|
1680
|
-
admission?.release();
|
|
1681
1789
|
lock?.release();
|
|
1682
1790
|
attachment.dispose();
|
|
1683
1791
|
}
|
|
@@ -1893,7 +2001,47 @@ export class NativeAgentCompositeTransport {
|
|
|
1893
2001
|
state.updatedAt = new Date(this.now()).toISOString();
|
|
1894
2002
|
this.persist(state, lock);
|
|
1895
2003
|
try {
|
|
1896
|
-
|
|
2004
|
+
const answerMode = state.startMode === "answer";
|
|
2005
|
+
const outboundArguments = answerMode
|
|
2006
|
+
? {
|
|
2007
|
+
agentId: state.agentId,
|
|
2008
|
+
scopeParam: state.scopeParam,
|
|
2009
|
+
question: state.startArguments.task,
|
|
2010
|
+
...(state.startArguments.context !== undefined
|
|
2011
|
+
? { context: state.startArguments.context }
|
|
2012
|
+
: {}),
|
|
2013
|
+
contextKeys: state.startArguments.contextKeys ?? [],
|
|
2014
|
+
idempotencyKey: state.idempotencyKey,
|
|
2015
|
+
}
|
|
2016
|
+
: state.startArguments;
|
|
2017
|
+
started = await session.call(
|
|
2018
|
+
answerMode ? NATIVE_AGENT_UPSTREAM_ANSWER_TOOL : NATIVE_AGENT_START_TOOL,
|
|
2019
|
+
outboundArguments,
|
|
2020
|
+
{ signal },
|
|
2021
|
+
);
|
|
2022
|
+
if (answerMode) {
|
|
2023
|
+
if (!started?.runId) {
|
|
2024
|
+
throw new Error("native-agent answer returned no durable runId");
|
|
2025
|
+
}
|
|
2026
|
+
const answerText = extractAnswerFinalText(started);
|
|
2027
|
+
if (started.status === "succeeded" && answerText !== null) {
|
|
2028
|
+
started = {
|
|
2029
|
+
runId: started.runId,
|
|
2030
|
+
status: "succeeded",
|
|
2031
|
+
result: { finalText: answerText },
|
|
2032
|
+
...(Number.isSafeInteger(started.version)
|
|
2033
|
+
? { version: started.version }
|
|
2034
|
+
: {}),
|
|
2035
|
+
};
|
|
2036
|
+
} else if (
|
|
2037
|
+
started.status !== "continuation_required"
|
|
2038
|
+
|| started.durableRunCreated !== true
|
|
2039
|
+
) {
|
|
2040
|
+
throw new Error(
|
|
2041
|
+
"native-agent answer returned neither a terminal answer nor a durable continuation",
|
|
2042
|
+
);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
1897
2045
|
break;
|
|
1898
2046
|
} catch (error) {
|
|
1899
2047
|
if (error?.name === "AbortError") throw error;
|
|
@@ -1935,7 +2083,9 @@ export class NativeAgentCompositeTransport {
|
|
|
1935
2083
|
const observed = await session.call(NATIVE_AGENT_READ_TOOL, {
|
|
1936
2084
|
runId: state.upstreamRunId,
|
|
1937
2085
|
waitSeconds: boundedWait,
|
|
1938
|
-
|
|
2086
|
+
terminalOnly: process.env.IMPEL_NATIVE_TERMINAL_ONLY_READS !== "0",
|
|
2087
|
+
...(Number.isSafeInteger(state.version) ? { sinceVersion: state.version } : {}),
|
|
2088
|
+
}, { signal });
|
|
1939
2089
|
this.commitPayload(state, observed, lock);
|
|
1940
2090
|
onProgress?.({ status: state.status, runId: state.upstreamRunId, poll: poll + 1 });
|
|
1941
2091
|
result = resultForState(state);
|
|
@@ -1980,31 +2130,29 @@ const HANDLE_SCHEMA = {
|
|
|
1980
2130
|
|
|
1981
2131
|
export function nativeAgentCompositeTools({ mode = "durable" } = {}) {
|
|
1982
2132
|
if (!["durable", "recovery", "answer"].includes(mode)) throw new Error("invalid native-agent MCP mode");
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
items: { type: "string", minLength: 1, maxLength: 160 },
|
|
2003
|
-
},
|
|
2133
|
+
const answerTool = {
|
|
2134
|
+
name: NATIVE_AGENT_ANSWER_TOOL,
|
|
2135
|
+
description: "Answer once through the direct-answer native agent fixed by this MCP server.",
|
|
2136
|
+
inputSchema: {
|
|
2137
|
+
type: "object",
|
|
2138
|
+
additionalProperties: false,
|
|
2139
|
+
anyOf: [
|
|
2140
|
+
{ required: ["question"], not: { required: ["task"] } },
|
|
2141
|
+
{ required: ["task"], not: { required: ["question"] } },
|
|
2142
|
+
],
|
|
2143
|
+
properties: {
|
|
2144
|
+
question: { type: "string", minLength: 1, maxLength: 40_000 },
|
|
2145
|
+
task: { type: "string", minLength: 1, maxLength: 40_000 },
|
|
2146
|
+
context: { type: "string", maxLength: 40_000 },
|
|
2147
|
+
contextKeys: {
|
|
2148
|
+
type: "array",
|
|
2149
|
+
maxItems: 30,
|
|
2150
|
+
uniqueItems: true,
|
|
2151
|
+
items: { type: "string", minLength: 1, maxLength: 160 },
|
|
2004
2152
|
},
|
|
2005
2153
|
},
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2154
|
+
},
|
|
2155
|
+
};
|
|
2008
2156
|
const tools = [
|
|
2009
2157
|
{
|
|
2010
2158
|
name: NATIVE_AGENT_RUN_TOOL,
|
|
@@ -2045,6 +2193,9 @@ export function nativeAgentCompositeTools({ mode = "durable" } = {}) {
|
|
|
2045
2193
|
},
|
|
2046
2194
|
},
|
|
2047
2195
|
];
|
|
2196
|
+
if (mode === "answer") {
|
|
2197
|
+
return [answerTool, ...tools.filter(({ name }) => name !== NATIVE_AGENT_RUN_TOOL)];
|
|
2198
|
+
}
|
|
2048
2199
|
return mode === "recovery" ? tools.filter(({ name }) => name !== NATIVE_AGENT_RUN_TOOL) : tools;
|
|
2049
2200
|
}
|
|
2050
2201
|
|
package/src/selfInvocation.js
CHANGED
|
@@ -67,12 +67,27 @@ export function impelCliInvocation(args = [], options = {}) {
|
|
|
67
67
|
export const IMPEL_MANAGED_MCP_ENV = brandedEnvironmentName("MANAGED_MCP");
|
|
68
68
|
export const IMPEL_TASKS_MCP_SERVER_NAME = `${RUNTIME_BRAND.cli.providerId}-tasks`;
|
|
69
69
|
export const IMPEL_NATIVE_AGENT_MCP_TARGET = "native-agent";
|
|
70
|
+
export const IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES = [
|
|
71
|
+
"IMPEL_NATIVE_AGENT_TELEMETRY_PATH",
|
|
72
|
+
"IMPEL_NATIVE_HOST",
|
|
73
|
+
"IMPEL_NATIVE_HOST_BUILD",
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
function managedMcpEnvironment(environment = process.env) {
|
|
77
|
+
const telemetry = Object.fromEntries(
|
|
78
|
+
IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES.flatMap((name) => {
|
|
79
|
+
const value = environment?.[name];
|
|
80
|
+
return typeof value === "string" && value.length > 0 ? [[name, value]] : [];
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
return { [IMPEL_MANAGED_MCP_ENV]: "1", ...telemetry };
|
|
84
|
+
}
|
|
70
85
|
|
|
71
86
|
export function impelMcpInvocation(args = [], options = {}) {
|
|
72
87
|
return {
|
|
73
88
|
type: "stdio",
|
|
74
89
|
...impelCliInvocation(["mcp", ...args], options),
|
|
75
|
-
env:
|
|
90
|
+
env: managedMcpEnvironment(options.environment),
|
|
76
91
|
};
|
|
77
92
|
}
|
|
78
93
|
|
package/src/verbatimRelay.js
CHANGED
|
@@ -13,7 +13,8 @@ export function parentVerbatimRelayAppendix() {
|
|
|
13
13
|
return (
|
|
14
14
|
`When an explicit custom agent's catalog-derived description declares ${VERBATIM_RELAY_OPT_IN_MARKER}, ` +
|
|
15
15
|
`spawn it with ${VERBATIM_SPAWN_REQUIREMENT} and relay its finalText verbatim with ${VERBATIM_FINAL_TEXT_CONSTRAINTS}; ` +
|
|
16
|
-
"preserve Sources sections and citations exactly. " +
|
|
16
|
+
"preserve Sources sections and citations exactly. Start one child, then use one blocking wait sized for the child budget; " +
|
|
17
|
+
"on Codex, call wait_agent exactly once with timeout_ms=60000. Do not send follow-ups, short-poll the child, or synthesize while it is running. " +
|
|
17
18
|
"Custom agents without that declaration keep the default delegation behavior."
|
|
18
19
|
);
|
|
19
20
|
}
|