impel-cli 0.20.14 → 0.20.16
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 +16 -0
- package/RELEASE_NOTES.md +22 -0
- package/docs/native-agent-host-capability-matrix.md +14 -6
- package/package.json +4 -1
- package/scripts/analyze-native-codex.mjs +454 -0
- package/scripts/profile-native-codex.mjs +584 -0
- package/src/agents.js +230 -19
- package/src/apps.js +5 -3
- package/src/cli.js +1 -0
- package/src/commands/agents.js +4 -0
- package/src/commands/launch.js +124 -6
package/README.md
CHANGED
|
@@ -180,6 +180,21 @@ impel claude --model opus
|
|
|
180
180
|
impel codex exec "review this repository"
|
|
181
181
|
```
|
|
182
182
|
|
|
183
|
+
To run one explicitly selected tenant agent in a single Codex thread, select
|
|
184
|
+
it by exact agent ID (preferred) or unique exact title:
|
|
185
|
+
|
|
186
|
+
```sh
|
|
187
|
+
impel codex --agent research-agent exec "answer this question"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
This path loads a generated, tenant-bound profile with Luna/low reasoning, a
|
|
191
|
+
read-only local sandbox, the exact fixed MCP binding, and only that binding's
|
|
192
|
+
answer/run, resume, and recovery tools. It omits the normal parent specialist
|
|
193
|
+
instructions, so there is no parent spawn, parent wait, or relay turn. Model,
|
|
194
|
+
profile, developer-instruction, MCP, approval, sandbox, and feature overrides
|
|
195
|
+
are rejected with `--agent`. The selected tenant and profile integrity must
|
|
196
|
+
match before Codex starts. Ordinary `impel codex` passthrough is unchanged.
|
|
197
|
+
|
|
183
198
|
## Remote Fargate sessions
|
|
184
199
|
|
|
185
200
|
Remote operations use the tenant broker. The laptop sends its PAT only to that
|
|
@@ -496,6 +511,7 @@ impel setup
|
|
|
496
511
|
impel update
|
|
497
512
|
impel claude
|
|
498
513
|
impel codex
|
|
514
|
+
impel codex --agent <id|exact-title> ...
|
|
499
515
|
impel tenant list|current|use
|
|
500
516
|
impel tasks ...
|
|
501
517
|
impel pat ...
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Release notes
|
|
2
2
|
|
|
3
|
+
## 0.20.16 — Correlated Codex transport timing
|
|
4
|
+
|
|
5
|
+
- Derives native-agent MCP duration from correlated, timestamped Codex rollout
|
|
6
|
+
events when durable profiles correctly omit transient telemetry destinations.
|
|
7
|
+
- Retries one unestablished tenant-preflight result while still requiring one
|
|
8
|
+
exact selected tenant before every profiled Codex attempt.
|
|
9
|
+
|
|
10
|
+
## 0.20.15 — Direct Codex native agents
|
|
11
|
+
|
|
12
|
+
- Classifies the exact `mcp__impel_agent` namespace as direct-only in generated
|
|
13
|
+
Codex 0.146.0 agents, removing child code cells and code-mode waits while
|
|
14
|
+
preserving the fixed tool allowlist and recovery contract.
|
|
15
|
+
- Adds `impel codex --agent <id|exact-title>` with integrity-tracked top-level
|
|
16
|
+
profiles, removing the parent spawn/wait/relay thread for explicit CLI runs.
|
|
17
|
+
- Bumps managed agent and app profile contracts so already-fresh isolated CLI
|
|
18
|
+
and ChatGPT profiles regenerate immediately after update.
|
|
19
|
+
- Adds a private-by-default production profiler and structural analyzer with
|
|
20
|
+
exact-tenant checks, strict JSONL, process-tree cancellation, original versus
|
|
21
|
+
replacement cohorts, and prompt/answer-free aggregate reports.
|
|
22
|
+
- Refreshes the exact Windows ChatGPT/Codex Store build after the production
|
|
23
|
+
manifest advanced during release qualification.
|
|
24
|
+
|
|
3
25
|
## 0.20.14 — Opaque direct-answer continuations
|
|
4
26
|
|
|
5
27
|
- Returns a minimal local continuation locator for read-only direct answers,
|
|
@@ -8,8 +8,8 @@ is treated as an eager host.
|
|
|
8
8
|
| --- | --- | --- | --- | --- |
|
|
9
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
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 |
|
|
12
|
-
| ChatGPT desktop Codex | 26.727.40816 (embedded Codex
|
|
11
|
+
| Codex CLI through `impel codex` | 0.146.0 | Yes, with `features.code_mode.direct_only_tool_namespaces = ["mcp__impel_agent"]` | Eager; optional single-thread profile | Existing `@Agent` children call the fixed MCP namespace directly. `impel codex --agent` also removes the parent spawn/wait/relay thread. |
|
|
12
|
+
| ChatGPT desktop Codex | 26.727.40816 (embedded Codex 0.146.0 contract) | Yes, with the same exact native namespace | Eager child, compatible parent | Keeps the app's supported `@Agent` parent/child route, but the child no longer needs code cells or code-mode waits. |
|
|
13
13
|
|
|
14
14
|
## Qualified contract
|
|
15
15
|
|
|
@@ -28,11 +28,19 @@ is treated as an eager host.
|
|
|
28
28
|
decrypted provider material. Production measurements use correlation ids,
|
|
29
29
|
phase timestamps, counts, durations, build identifiers, and terminal classes
|
|
30
30
|
only.
|
|
31
|
+
- Top-level direct profiles exist only in Impel-isolated Codex and managed
|
|
32
|
+
ChatGPT homes. Bare/native Codex homes do not receive the 0.146.0-only key or
|
|
33
|
+
the new top-level profiles. `impel codex` resolves only the reviewed pinned
|
|
34
|
+
binary; an older or newer unqualified build must be updated and re-qualified
|
|
35
|
+
rather than receiving an unknown configuration key.
|
|
36
|
+
- `mcp__impel_agent` is the native tool namespace. The server id
|
|
37
|
+
`impel_agent` is not equivalent and does not qualify as direct exposure.
|
|
31
38
|
|
|
32
39
|
## Requalification
|
|
33
40
|
|
|
34
|
-
Re-run temporary-home profile parsing
|
|
41
|
+
Re-run temporary-home profile parsing (including active, direct-answer,
|
|
42
|
+
durable, and recovery profiles), direct tool visibility, cancellation,
|
|
35
43
|
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
|
|
37
|
-
unless the child trace has zero `ALL_TOOLS`, `
|
|
38
|
-
`
|
|
44
|
+
wait fixture after a pinned client changes. A build stays fail-closed without
|
|
45
|
+
the direct-only key unless the child trace has zero `ALL_TOOLS`, `exec`, and
|
|
46
|
+
`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.16",
|
|
4
4
|
"description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"bin",
|
|
16
16
|
"docs/experimental-managed-cursor.md",
|
|
17
17
|
"docs/native-agent-host-capability-matrix.md",
|
|
18
|
+
"scripts",
|
|
18
19
|
"src",
|
|
19
20
|
"README.md"
|
|
20
21
|
],
|
|
@@ -31,6 +32,8 @@
|
|
|
31
32
|
"url": "git+https://github.com/UseImpel/impel-cli.git"
|
|
32
33
|
},
|
|
33
34
|
"scripts": {
|
|
35
|
+
"analyze:native-codex": "node scripts/analyze-native-codex.mjs",
|
|
36
|
+
"profile:native-codex": "node scripts/profile-native-codex.mjs",
|
|
34
37
|
"test": "node --test"
|
|
35
38
|
}
|
|
36
39
|
}
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const AGGREGATE_SCHEMA = "impel.native-codex-profile.v1";
|
|
8
|
+
const REPORT_SCHEMA = "impel.native-codex-analysis.v1";
|
|
9
|
+
const SAFE_MCP_TOOLS = new Set([
|
|
10
|
+
"answer_native_agent",
|
|
11
|
+
"run_native_agent",
|
|
12
|
+
"resume_native_agent_run",
|
|
13
|
+
"recover_native_agent_runs",
|
|
14
|
+
]);
|
|
15
|
+
const SAFE_TOOL_NAMES = new Set([
|
|
16
|
+
...SAFE_MCP_TOOLS,
|
|
17
|
+
"spawn_agent",
|
|
18
|
+
"wait_agent",
|
|
19
|
+
"send_message",
|
|
20
|
+
"followup_task",
|
|
21
|
+
"interrupt_agent",
|
|
22
|
+
"exec",
|
|
23
|
+
"wait",
|
|
24
|
+
"tool_search",
|
|
25
|
+
"ALL_TOOLS",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function finiteNonnegative(value) {
|
|
29
|
+
return Number.isFinite(value) && value >= 0 ? value : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function integerNonnegative(value) {
|
|
33
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseStrictJsonLines(text, label = "JSONL") {
|
|
37
|
+
const records = [];
|
|
38
|
+
for (const [index, line] of text.split(/\r?\n/u).entries()) {
|
|
39
|
+
if (!line.trim()) continue;
|
|
40
|
+
let value;
|
|
41
|
+
try {
|
|
42
|
+
value = JSON.parse(line);
|
|
43
|
+
} catch {
|
|
44
|
+
throw new Error(`${label} line ${index + 1} is not valid JSON`);
|
|
45
|
+
}
|
|
46
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
47
|
+
throw new Error(`${label} line ${index + 1} is not a JSON object`);
|
|
48
|
+
}
|
|
49
|
+
records.push(value);
|
|
50
|
+
}
|
|
51
|
+
return records;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function safeEventType(value) {
|
|
55
|
+
return typeof value === "string" && /^[A-Za-z0-9_.:-]{1,120}$/u.test(value)
|
|
56
|
+
? value
|
|
57
|
+
: null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function safeToolName(value) {
|
|
61
|
+
if (typeof value !== "string") return null;
|
|
62
|
+
const unprefixed = value.replace(/^mcp__impel_agent__/u, "");
|
|
63
|
+
return SAFE_TOOL_NAMES.has(unprefixed) ? unprefixed : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function eventItem(event) {
|
|
67
|
+
return event.item && typeof event.item === "object" && !Array.isArray(event.item)
|
|
68
|
+
? event.item
|
|
69
|
+
: event;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function eventTool(item) {
|
|
73
|
+
const server = item.server || item.server_name || item.mcp_server;
|
|
74
|
+
const tool = item.tool || item.tool_name || item.name;
|
|
75
|
+
if (server === "impel_agent" && SAFE_MCP_TOOLS.has(tool)) return tool;
|
|
76
|
+
return safeToolName(tool);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function increment(counts, key) {
|
|
80
|
+
if (key) counts[key] = (counts[key] || 0) + 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function summarizeCodexEvents(events) {
|
|
84
|
+
const eventTypes = {};
|
|
85
|
+
const toolCounts = {};
|
|
86
|
+
const threads = new Set();
|
|
87
|
+
let inputTokens = 0;
|
|
88
|
+
let cachedInputTokens = 0;
|
|
89
|
+
let outputTokens = 0;
|
|
90
|
+
let turnCompleted = false;
|
|
91
|
+
let errorEvents = 0;
|
|
92
|
+
let codeCells = 0;
|
|
93
|
+
let codeWaits = 0;
|
|
94
|
+
|
|
95
|
+
for (const event of events) {
|
|
96
|
+
const type = safeEventType(event.type);
|
|
97
|
+
increment(eventTypes, type);
|
|
98
|
+
const threadId = event.thread_id || event.threadId;
|
|
99
|
+
if (typeof threadId === "string" && /^[A-Za-z0-9_.:-]{1,200}$/u.test(threadId)) threads.add(threadId);
|
|
100
|
+
if (type === "turn.completed") turnCompleted = true;
|
|
101
|
+
if (type?.includes("error") || event.error) errorEvents += 1;
|
|
102
|
+
|
|
103
|
+
const item = eventItem(event);
|
|
104
|
+
const itemType = safeEventType(item.type);
|
|
105
|
+
const tool = eventTool(item);
|
|
106
|
+
increment(toolCounts, tool);
|
|
107
|
+
if (itemType && /(?:code_mode|command_execution|code_cell)/u.test(itemType)) codeCells += 1;
|
|
108
|
+
if (tool === "exec") codeCells += 1;
|
|
109
|
+
if (tool === "wait") codeWaits += 1;
|
|
110
|
+
|
|
111
|
+
const usage = event.usage && typeof event.usage === "object" ? event.usage : null;
|
|
112
|
+
if (usage) {
|
|
113
|
+
inputTokens = Math.max(inputTokens, integerNonnegative(usage.input_tokens) || 0);
|
|
114
|
+
cachedInputTokens = Math.max(cachedInputTokens, integerNonnegative(usage.cached_input_tokens) || 0);
|
|
115
|
+
outputTokens = Math.max(outputTokens, integerNonnegative(usage.output_tokens) || 0);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
eventCount: events.length,
|
|
121
|
+
eventTypes,
|
|
122
|
+
toolCounts,
|
|
123
|
+
threadCount: threads.size,
|
|
124
|
+
turnCompleted,
|
|
125
|
+
errorEvents,
|
|
126
|
+
codeCells,
|
|
127
|
+
codeWaits,
|
|
128
|
+
inputTokens,
|
|
129
|
+
cachedInputTokens,
|
|
130
|
+
outputTokens,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function rolloutSessionMetadata(events) {
|
|
135
|
+
const metadata = events.find((event) => event.type === "session_meta")?.payload;
|
|
136
|
+
return metadata && typeof metadata === "object" ? {
|
|
137
|
+
threadId: typeof (metadata.session_id || metadata.id) === "string"
|
|
138
|
+
? metadata.session_id || metadata.id
|
|
139
|
+
: null,
|
|
140
|
+
parentThreadId: typeof metadata.parent_thread_id === "string" ? metadata.parent_thread_id : null,
|
|
141
|
+
} : { threadId: null, parentThreadId: null };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function summarizeCodexRollouts(rollouts) {
|
|
145
|
+
const toolCounts = {};
|
|
146
|
+
const parentToolCounts = {};
|
|
147
|
+
const childToolCounts = {};
|
|
148
|
+
let eventCount = 0;
|
|
149
|
+
let codeCells = 0;
|
|
150
|
+
let codeWaits = 0;
|
|
151
|
+
let inputTokens = 0;
|
|
152
|
+
let cachedInputTokens = 0;
|
|
153
|
+
let outputTokens = 0;
|
|
154
|
+
let mcpDurationMs = 0;
|
|
155
|
+
let mcpCompletedCalls = 0;
|
|
156
|
+
|
|
157
|
+
for (const events of rollouts) {
|
|
158
|
+
eventCount += events.length;
|
|
159
|
+
const metadata = rolloutSessionMetadata(events);
|
|
160
|
+
const scopedCounts = metadata.parentThreadId ? childToolCounts : parentToolCounts;
|
|
161
|
+
let latestUsage = null;
|
|
162
|
+
const mcpStarts = new Map();
|
|
163
|
+
for (const event of events) {
|
|
164
|
+
const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
|
|
165
|
+
if (event.type === "response_item"
|
|
166
|
+
&& ["function_call", "custom_tool_call"].includes(payload.type)) {
|
|
167
|
+
const tool = safeToolName(payload.name);
|
|
168
|
+
increment(toolCounts, tool);
|
|
169
|
+
increment(scopedCounts, tool);
|
|
170
|
+
if (tool === "exec" || payload.type === "custom_tool_call") codeCells += tool === "exec" ? 1 : 0;
|
|
171
|
+
if (tool === "wait") codeWaits += 1;
|
|
172
|
+
const startedAt = Date.parse(event.timestamp || "");
|
|
173
|
+
if (SAFE_MCP_TOOLS.has(tool)
|
|
174
|
+
&& typeof payload.call_id === "string"
|
|
175
|
+
&& Number.isFinite(startedAt)) {
|
|
176
|
+
mcpStarts.set(payload.call_id, startedAt);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (event.type === "event_msg"
|
|
180
|
+
&& payload.type === "mcp_tool_call_end"
|
|
181
|
+
&& typeof payload.call_id === "string") {
|
|
182
|
+
const startedAt = mcpStarts.get(payload.call_id);
|
|
183
|
+
const endedAt = Date.parse(event.timestamp || "");
|
|
184
|
+
const duration = endedAt - startedAt;
|
|
185
|
+
if (Number.isFinite(startedAt)
|
|
186
|
+
&& Number.isFinite(endedAt)
|
|
187
|
+
&& duration >= 0
|
|
188
|
+
&& duration <= 24 * 60 * 60 * 1000) {
|
|
189
|
+
mcpDurationMs += duration;
|
|
190
|
+
mcpCompletedCalls += 1;
|
|
191
|
+
mcpStarts.delete(payload.call_id);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (event.type === "event_msg" && payload.type === "token_count") {
|
|
195
|
+
const usage = payload.info?.total_token_usage;
|
|
196
|
+
if (usage && typeof usage === "object") latestUsage = usage;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (latestUsage) {
|
|
200
|
+
inputTokens += integerNonnegative(latestUsage.input_tokens) || 0;
|
|
201
|
+
cachedInputTokens += integerNonnegative(latestUsage.cached_input_tokens) || 0;
|
|
202
|
+
outputTokens += integerNonnegative(latestUsage.output_tokens) || 0;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
eventCount,
|
|
208
|
+
threadCount: rollouts.length,
|
|
209
|
+
toolCounts,
|
|
210
|
+
parentToolCounts,
|
|
211
|
+
childToolCounts,
|
|
212
|
+
codeCells,
|
|
213
|
+
codeWaits,
|
|
214
|
+
inputTokens,
|
|
215
|
+
cachedInputTokens,
|
|
216
|
+
outputTokens,
|
|
217
|
+
mcpDurationMs,
|
|
218
|
+
mcpCompletedCalls,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function summarizeNativeTelemetry(events) {
|
|
223
|
+
const eventCounts = {};
|
|
224
|
+
const toolCounts = {};
|
|
225
|
+
const correlations = new Set();
|
|
226
|
+
const gatewayRequests = new Set();
|
|
227
|
+
let mcpDurationMs = 0;
|
|
228
|
+
let localFailures = 0;
|
|
229
|
+
let upstreamDurationMs = 0;
|
|
230
|
+
for (const event of events) {
|
|
231
|
+
if (event.schema !== "impel.native-agent-telemetry.v1") {
|
|
232
|
+
throw new Error("native-agent telemetry has an unexpected schema");
|
|
233
|
+
}
|
|
234
|
+
const type = safeEventType(event.event);
|
|
235
|
+
increment(eventCounts, type);
|
|
236
|
+
const tool = safeToolName(event.tool);
|
|
237
|
+
increment(toolCounts, tool);
|
|
238
|
+
if (typeof event.correlationId === "string") correlations.add(event.correlationId);
|
|
239
|
+
if (typeof event.gatewayRequestId === "string") gatewayRequests.add(event.gatewayRequestId);
|
|
240
|
+
const duration = integerNonnegative(event.durationMs) || 0;
|
|
241
|
+
if (type === "local_tool_completed" && SAFE_MCP_TOOLS.has(tool)) {
|
|
242
|
+
mcpDurationMs += duration;
|
|
243
|
+
if (["failed", "cancelled"].includes(event.outcome)) localFailures += 1;
|
|
244
|
+
}
|
|
245
|
+
if (type === "upstream_request_completed") upstreamDurationMs += duration;
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
eventCount: events.length,
|
|
249
|
+
eventCounts,
|
|
250
|
+
toolCounts,
|
|
251
|
+
correlationCount: correlations.size,
|
|
252
|
+
gatewayRequestCount: gatewayRequests.size,
|
|
253
|
+
mcpDurationMs,
|
|
254
|
+
upstreamDurationMs,
|
|
255
|
+
localFailures,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function summarizeRawAttempt({ stdoutText, telemetryText = "", rolloutTexts = [] }) {
|
|
260
|
+
const codexEvents = parseStrictJsonLines(stdoutText, "Codex stdout");
|
|
261
|
+
const telemetryEvents = telemetryText.trim()
|
|
262
|
+
? parseStrictJsonLines(telemetryText, "native-agent telemetry")
|
|
263
|
+
: [];
|
|
264
|
+
const stdout = summarizeCodexEvents(codexEvents);
|
|
265
|
+
const rollouts = rolloutTexts.map((text, index) => parseStrictJsonLines(text, `Codex rollout ${index + 1}`));
|
|
266
|
+
const rollout = summarizeCodexRollouts(rollouts);
|
|
267
|
+
return {
|
|
268
|
+
codex: {
|
|
269
|
+
...stdout,
|
|
270
|
+
...(rollouts.length ? {
|
|
271
|
+
eventCount: rollout.eventCount,
|
|
272
|
+
toolCounts: rollout.toolCounts,
|
|
273
|
+
threadCount: rollout.threadCount,
|
|
274
|
+
codeCells: rollout.codeCells,
|
|
275
|
+
codeWaits: rollout.codeWaits,
|
|
276
|
+
inputTokens: rollout.inputTokens,
|
|
277
|
+
cachedInputTokens: rollout.cachedInputTokens,
|
|
278
|
+
outputTokens: rollout.outputTokens,
|
|
279
|
+
mcpDurationMs: rollout.mcpDurationMs,
|
|
280
|
+
mcpCompletedCalls: rollout.mcpCompletedCalls,
|
|
281
|
+
} : {}),
|
|
282
|
+
parentToolCounts: rollout.parentToolCounts,
|
|
283
|
+
childToolCounts: rollout.childToolCounts,
|
|
284
|
+
},
|
|
285
|
+
telemetry: summarizeNativeTelemetry(telemetryEvents),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function percentile(values, probability) {
|
|
290
|
+
if (values.length === 0) return null;
|
|
291
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
292
|
+
if (sorted.length === 1) return sorted[0];
|
|
293
|
+
const position = (sorted.length - 1) * probability;
|
|
294
|
+
const lower = Math.floor(position);
|
|
295
|
+
const upper = Math.ceil(position);
|
|
296
|
+
const weight = position - lower;
|
|
297
|
+
return Math.round((sorted[lower] * (1 - weight) + sorted[upper] * weight) * 1000) / 1000;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function distribution(attempts, field) {
|
|
301
|
+
const values = attempts.map((attempt) => finiteNonnegative(attempt[field])).filter((value) => value !== null);
|
|
302
|
+
return { samples: values.length, p50: percentile(values, 0.5), p95: percentile(values, 0.95) };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function mergedCounts(attempts, field) {
|
|
306
|
+
const counts = {};
|
|
307
|
+
for (const attempt of attempts) {
|
|
308
|
+
for (const [key, value] of Object.entries(attempt[field] || {})) {
|
|
309
|
+
if (SAFE_TOOL_NAMES.has(key) && Number.isSafeInteger(value) && value >= 0) {
|
|
310
|
+
counts[key] = (counts[key] || 0) + value;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return counts;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function groupKey(attempt) {
|
|
318
|
+
return `${attempt.tenantId}\0${attempt.sloClass}\0${attempt.mode}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function directGate(report) {
|
|
322
|
+
return {
|
|
323
|
+
reliability: report.original.total === 30 && report.original.successful === 30,
|
|
324
|
+
e2e: report.timings.durationMs.p50 !== null
|
|
325
|
+
&& report.timings.durationMs.p50 <= (report.sloClass === "cibi" ? 40_000 : 90_000)
|
|
326
|
+
&& report.timings.durationMs.p95 <= (report.sloClass === "cibi" ? 50_000 : 105_000),
|
|
327
|
+
overhead: report.timings.nonMcpOverheadMs.p50 !== null
|
|
328
|
+
&& report.timings.nonMcpOverheadMs.p50 <= 25_000
|
|
329
|
+
&& report.timings.nonMcpOverheadMs.p95 <= 35_000,
|
|
330
|
+
input: report.tokens.input.p50 !== null
|
|
331
|
+
&& report.tokens.input.p50 <= 45_000
|
|
332
|
+
&& report.tokens.input.p95 <= 55_000,
|
|
333
|
+
structure: report.structure.maximumThreadCount <= 1
|
|
334
|
+
&& report.structure.totalCodeCells === 0
|
|
335
|
+
&& report.structure.totalCodeWaits === 0
|
|
336
|
+
&& ["spawn_agent", "wait_agent", "send_message", "exec", "wait", "tool_search", "ALL_TOOLS"]
|
|
337
|
+
.every((tool) => !report.structure.allToolCounts[tool])
|
|
338
|
+
&& (report.structure.toolCounts.answer_native_agent || 0) === report.successfulSamples
|
|
339
|
+
&& !report.structure.toolCounts.run_native_agent,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function compatibleGate(report) {
|
|
344
|
+
return {
|
|
345
|
+
reliability: report.original.total === 30 && report.original.successful >= 29,
|
|
346
|
+
e2e: report.timings.durationMs.p50 !== null
|
|
347
|
+
&& report.timings.durationMs.p50 <= (report.sloClass === "cibi" ? 55_000 : 100_000),
|
|
348
|
+
childStructure: report.structure.totalCodeCells === 0
|
|
349
|
+
&& report.structure.totalCodeWaits === 0
|
|
350
|
+
&& ["exec", "wait", "send_message", "tool_search", "ALL_TOOLS"]
|
|
351
|
+
.every((tool) => !report.structure.childToolCounts[tool]),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function analyzeAggregates(aggregates) {
|
|
356
|
+
for (const aggregate of aggregates) {
|
|
357
|
+
if (!aggregate || aggregate.schema !== AGGREGATE_SCHEMA || !Array.isArray(aggregate.attempts)) {
|
|
358
|
+
throw new Error("profile input has an unexpected schema");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const groups = new Map();
|
|
362
|
+
for (const attempt of aggregates.flatMap((aggregate) => aggregate.attempts)) {
|
|
363
|
+
if (!attempt || typeof attempt !== "object") throw new Error("profile input has an invalid attempt");
|
|
364
|
+
const key = groupKey(attempt);
|
|
365
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
366
|
+
groups.get(key).push(attempt);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const reports = [...groups.values()].map((attempts) => {
|
|
370
|
+
const original = attempts.filter((attempt) => attempt.cohort === "original");
|
|
371
|
+
const replacements = attempts.filter((attempt) => attempt.cohort === "replacement");
|
|
372
|
+
const successful = attempts.filter((attempt) => attempt.success === true);
|
|
373
|
+
const report = {
|
|
374
|
+
tenantId: attempts[0].tenantId,
|
|
375
|
+
sloClass: attempts[0].sloClass,
|
|
376
|
+
mode: attempts[0].mode,
|
|
377
|
+
original: {
|
|
378
|
+
total: original.length,
|
|
379
|
+
successful: original.filter((attempt) => attempt.success === true).length,
|
|
380
|
+
},
|
|
381
|
+
replacements: {
|
|
382
|
+
total: replacements.length,
|
|
383
|
+
successful: replacements.filter((attempt) => attempt.success === true).length,
|
|
384
|
+
},
|
|
385
|
+
latencySamples: {
|
|
386
|
+
original: original.filter((attempt) => attempt.success === true).length,
|
|
387
|
+
replacement: replacements.filter((attempt) => attempt.success === true).length,
|
|
388
|
+
},
|
|
389
|
+
successfulSamples: successful.length,
|
|
390
|
+
timings: {
|
|
391
|
+
durationMs: distribution(successful, "durationMs"),
|
|
392
|
+
mcpDurationMs: distribution(successful, "mcpDurationMs"),
|
|
393
|
+
nonMcpOverheadMs: distribution(successful, "nonMcpOverheadMs"),
|
|
394
|
+
},
|
|
395
|
+
tokens: {
|
|
396
|
+
input: distribution(successful, "inputTokens"),
|
|
397
|
+
cachedInput: distribution(successful, "cachedInputTokens"),
|
|
398
|
+
output: distribution(successful, "outputTokens"),
|
|
399
|
+
},
|
|
400
|
+
structure: {
|
|
401
|
+
toolCounts: mergedCounts(successful, "toolCounts"),
|
|
402
|
+
allToolCounts: mergedCounts(attempts, "toolCounts"),
|
|
403
|
+
parentToolCounts: mergedCounts(successful, "parentToolCounts"),
|
|
404
|
+
childToolCounts: mergedCounts(successful, "childToolCounts"),
|
|
405
|
+
maximumThreadCount: successful.reduce((maximum, attempt) => Math.max(maximum, attempt.threadCount || 0), 0),
|
|
406
|
+
totalCodeCells: successful.reduce((total, attempt) => total + (attempt.codeCells || 0), 0),
|
|
407
|
+
totalCodeWaits: successful.reduce((total, attempt) => total + (attempt.codeWaits || 0), 0),
|
|
408
|
+
},
|
|
409
|
+
failures: {
|
|
410
|
+
timeout: attempts.filter((attempt) => attempt.timedOut).length,
|
|
411
|
+
invalidJson: attempts.filter((attempt) => !attempt.jsonValid).length,
|
|
412
|
+
descendantsRemaining: attempts.filter((attempt) => attempt.descendantsRemaining === true).length,
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
report.gates = report.mode === "direct" ? directGate(report) : compatibleGate(report);
|
|
416
|
+
report.gatesPassed = Object.values(report.gates).every(Boolean);
|
|
417
|
+
return report;
|
|
418
|
+
}).sort((left, right) => left.tenantId.localeCompare(right.tenantId) || left.mode.localeCompare(right.mode));
|
|
419
|
+
|
|
420
|
+
return { schema: REPORT_SCHEMA, generatedAt: new Date().toISOString(), reports };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function parseArguments(argv) {
|
|
424
|
+
const inputs = [];
|
|
425
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
426
|
+
if (argv[index] === "--input") {
|
|
427
|
+
const value = argv[index += 1];
|
|
428
|
+
if (!value) throw new Error("--input requires an aggregate JSON path");
|
|
429
|
+
inputs.push(value);
|
|
430
|
+
} else if (argv[index] === "--help" || argv[index] === "-h") {
|
|
431
|
+
return { help: true, inputs: [] };
|
|
432
|
+
} else {
|
|
433
|
+
inputs.push(argv[index]);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (inputs.length === 0) throw new Error("provide at least one aggregate JSON path");
|
|
437
|
+
return { help: false, inputs };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function main(argv) {
|
|
441
|
+
const parsed = parseArguments(argv);
|
|
442
|
+
if (parsed.help) {
|
|
443
|
+
process.stdout.write("Usage: node scripts/analyze-native-codex.mjs --input <aggregate.json> [...]\n");
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const aggregates = parsed.inputs.map((input) => JSON.parse(fs.readFileSync(path.resolve(input), "utf8")));
|
|
447
|
+
process.stdout.write(`${JSON.stringify(analyzeAggregates(aggregates), null, 2)}\n`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
451
|
+
if (isMain) main(process.argv.slice(2)).catch((error) => {
|
|
452
|
+
process.stderr.write(`analyze-native-codex: ${error.message}\n`);
|
|
453
|
+
process.exitCode = 1;
|
|
454
|
+
});
|