@co0ontty/wand 1.37.0 → 1.39.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/process-manager.d.ts +16 -0
- package/dist/process-manager.js +179 -0
- package/dist/server-session-routes.js +110 -0
- package/dist/structured-session-manager.d.ts +48 -0
- package/dist/structured-session-manager.js +293 -30
- package/dist/types.d.ts +2 -0
- package/dist/web-ui/content/scripts.js +464 -77
- package/dist/web-ui/content/styles.css +110 -49
- package/package.json +1 -1
|
@@ -580,7 +580,7 @@ export class StructuredSessionManager {
|
|
|
580
580
|
output: "",
|
|
581
581
|
archived: false,
|
|
582
582
|
archivedAt: null,
|
|
583
|
-
claudeSessionId: null,
|
|
583
|
+
claudeSessionId: options.claudeSessionId?.trim() || null,
|
|
584
584
|
messages: [],
|
|
585
585
|
queuedMessages: [],
|
|
586
586
|
structuredState: {
|
|
@@ -1161,6 +1161,7 @@ export class StructuredSessionManager {
|
|
|
1161
1161
|
sessionId: session.claudeSessionId,
|
|
1162
1162
|
model: session.selectedModel ?? session.structuredState?.model,
|
|
1163
1163
|
usage: undefined,
|
|
1164
|
+
codexBlockIndex: new Map(),
|
|
1164
1165
|
};
|
|
1165
1166
|
let lineBuf = "";
|
|
1166
1167
|
let stderr = "";
|
|
@@ -1232,23 +1233,24 @@ export class StructuredSessionManager {
|
|
|
1232
1233
|
return;
|
|
1233
1234
|
}
|
|
1234
1235
|
if (parsed?.type === "item.started" && parsed.item) {
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1236
|
+
this.applyCodexItem(turnState, parsed.item, "started");
|
|
1237
|
+
syncSnapshot();
|
|
1238
|
+
scheduleEmit();
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
if (parsed?.type === "item.updated" && parsed.item) {
|
|
1242
|
+
// codex `item.updated` 重新发送完整 ThreadItem(不是 delta)。
|
|
1243
|
+
// 对 text/thinking/TodoWrite 走 codexBlockIndex 替换;对 tool_use
|
|
1244
|
+
// 仍然按现有 id 复用,避免重复卡片。
|
|
1245
|
+
this.applyCodexItem(turnState, parsed.item, "updated");
|
|
1246
|
+
syncSnapshot();
|
|
1247
|
+
scheduleEmit();
|
|
1241
1248
|
return;
|
|
1242
1249
|
}
|
|
1243
1250
|
if (parsed?.type === "item.completed" && parsed.item) {
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
turnState.result = block.text;
|
|
1248
|
-
this.upsertCodexBlock(turnState.blocks, block);
|
|
1249
|
-
syncSnapshot();
|
|
1250
|
-
scheduleEmit();
|
|
1251
|
-
}
|
|
1251
|
+
this.applyCodexItem(turnState, parsed.item, "completed");
|
|
1252
|
+
syncSnapshot();
|
|
1253
|
+
scheduleEmit();
|
|
1252
1254
|
return;
|
|
1253
1255
|
}
|
|
1254
1256
|
if (parsed?.type === "turn.completed") {
|
|
@@ -2563,16 +2565,88 @@ export class StructuredSessionManager {
|
|
|
2563
2565
|
}
|
|
2564
2566
|
return "";
|
|
2565
2567
|
}
|
|
2568
|
+
/**
|
|
2569
|
+
* Merge one codex `item.*` event into `turnState.blocks`.
|
|
2570
|
+
*
|
|
2571
|
+
* 三种 phase 行为:
|
|
2572
|
+
* - "started": 首次出现的 item,块直接 push(tool_result 走 upsert 配对)。
|
|
2573
|
+
* text/thinking/TodoWrite 这种"靠 id 替换"的块记录到
|
|
2574
|
+
* codexBlockIndex 里,方便后续 updated/completed 找回原位。
|
|
2575
|
+
* - "updated": codex 重发完整 ThreadItem(不是 delta)。已记录过的块就
|
|
2576
|
+
* 替换;新块按 started 路径处理。
|
|
2577
|
+
* - "completed": 把"in_progress"卡片定型——text 同时更新 turnState.result
|
|
2578
|
+
* 以便 result fallback 不为空;tool_use ↔ tool_result 通过
|
|
2579
|
+
* 共享 id 配对到一起(包括 file_change 子项的 `${id}#i`)。
|
|
2580
|
+
*/
|
|
2581
|
+
applyCodexItem(turnState, item, phase) {
|
|
2582
|
+
const completed = phase === "completed";
|
|
2583
|
+
const itemId = typeof item.id === "string" ? item.id : "";
|
|
2584
|
+
const blocks = this.extractCodexItemBlock(item, completed);
|
|
2585
|
+
if (blocks.length === 0)
|
|
2586
|
+
return;
|
|
2587
|
+
const index = turnState.codexBlockIndex ??= new Map();
|
|
2588
|
+
for (const block of blocks) {
|
|
2589
|
+
// text / thinking / TodoWrite tool_use 的卡片是"按 item id 整体替换"语义,
|
|
2590
|
+
// 否则一个 agent_message 在 updated/completed 时会被重复 push 多次。
|
|
2591
|
+
const replaceable = block.type === "text"
|
|
2592
|
+
|| block.type === "thinking"
|
|
2593
|
+
|| (block.type === "tool_use" && block.name === "TodoWrite");
|
|
2594
|
+
if (replaceable && itemId) {
|
|
2595
|
+
const existing = index.get(itemId);
|
|
2596
|
+
if (existing !== undefined && existing < turnState.blocks.length) {
|
|
2597
|
+
turnState.blocks[existing] = block;
|
|
2598
|
+
}
|
|
2599
|
+
else {
|
|
2600
|
+
index.set(itemId, turnState.blocks.length);
|
|
2601
|
+
turnState.blocks.push(block);
|
|
2602
|
+
}
|
|
2603
|
+
if (block.type === "text" && completed) {
|
|
2604
|
+
turnState.result = block.text;
|
|
2605
|
+
}
|
|
2606
|
+
continue;
|
|
2607
|
+
}
|
|
2608
|
+
// 其它块(tool_use 非 Todo / tool_result / 文件改动的多 sub-id 块)
|
|
2609
|
+
// 仍然走原有 upsert:tool_result 按 tool_use_id 配对,其余直接 push。
|
|
2610
|
+
this.upsertCodexBlock(turnState.blocks, block);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
/**
|
|
2614
|
+
* Map a codex `item.{started,updated,completed}` payload into wand's
|
|
2615
|
+
* `ContentBlock[]` so the chat UI's existing tool/diff/todo cards just work.
|
|
2616
|
+
*
|
|
2617
|
+
* Codex `exec --json` emits 8 item.type values (see
|
|
2618
|
+
* `codex-rs/exec/src/exec_events.rs`); below they're routed to whatever wand
|
|
2619
|
+
* tool name reuses an existing renderer:
|
|
2620
|
+
*
|
|
2621
|
+
* agent_message → text
|
|
2622
|
+
* reasoning → thinking
|
|
2623
|
+
* command_execution → tool_use "Bash" + tool_result
|
|
2624
|
+
* file_change → one tool_use per file, named Edit/Write/Bash by `kind`
|
|
2625
|
+
* (codex does NOT carry old_string/new_string in the
|
|
2626
|
+
* exec stream, only the path list; diff card body is
|
|
2627
|
+
* empty but the file row + status still render)
|
|
2628
|
+
* mcp_tool_call → tool_use named "<server>__<tool>" + tool_result
|
|
2629
|
+
* web_search → tool_use "WebSearch" + tool_result (results not in stream)
|
|
2630
|
+
* todo_list → tool_use "TodoWrite" (replaced in place on each update)
|
|
2631
|
+
* error → text block prefixed with ❌
|
|
2632
|
+
*
|
|
2633
|
+
* Returns [] when there is nothing to emit yet (e.g. agent_message at
|
|
2634
|
+
* `item.started` before any text has been produced).
|
|
2635
|
+
*
|
|
2636
|
+
* Callers handle in-place replacement for `item.updated` via
|
|
2637
|
+
* `turnState.codexBlockIndex`; tool_use ↔ tool_result pairing still goes
|
|
2638
|
+
* through `upsertCodexBlock` by matching ids.
|
|
2639
|
+
*/
|
|
2566
2640
|
extractCodexItemBlock(item, completed) {
|
|
2567
2641
|
const id = typeof item.id === "string" ? item.id : randomUUID();
|
|
2568
2642
|
const type = typeof item.type === "string" ? item.type : "unknown";
|
|
2569
2643
|
if (type === "agent_message") {
|
|
2570
2644
|
const text = this.extractCodexText(item);
|
|
2571
|
-
return text ? { type: "text", text } :
|
|
2645
|
+
return text ? [{ type: "text", text }] : [];
|
|
2572
2646
|
}
|
|
2573
2647
|
if (type === "reasoning") {
|
|
2574
2648
|
const text = this.extractCodexText(item);
|
|
2575
|
-
return text ? { type: "thinking", thinking: text } :
|
|
2649
|
+
return text ? [{ type: "thinking", thinking: text }] : [];
|
|
2576
2650
|
}
|
|
2577
2651
|
if (type === "command_execution") {
|
|
2578
2652
|
const command = typeof item.command === "string" ? item.command : "";
|
|
@@ -2580,28 +2654,213 @@ export class StructuredSessionManager {
|
|
|
2580
2654
|
const exitCode = typeof item.exit_code === "number" ? item.exit_code : null;
|
|
2581
2655
|
const status = typeof item.status === "string" ? item.status : completed ? "completed" : "in_progress";
|
|
2582
2656
|
if (!completed) {
|
|
2657
|
+
return [{
|
|
2658
|
+
type: "tool_use",
|
|
2659
|
+
id,
|
|
2660
|
+
name: "Bash",
|
|
2661
|
+
input: { command, status },
|
|
2662
|
+
}];
|
|
2663
|
+
}
|
|
2664
|
+
// codex 的 status 可能是 declined(sandbox 拒了命令)/ failed(执行失败)—
|
|
2665
|
+
// 这时 exit_code 经常是 null,光靠 exitCode !== 0 判 is_error 会漏。
|
|
2666
|
+
const isError = status === "failed" || status === "declined"
|
|
2667
|
+
|| (typeof exitCode === "number" && exitCode !== 0);
|
|
2668
|
+
const fallbackText = status === "declined"
|
|
2669
|
+
? "command declined by sandbox"
|
|
2670
|
+
: (exitCode === null ? "" : `exit_code: ${exitCode}`);
|
|
2671
|
+
return [{
|
|
2672
|
+
type: "tool_result",
|
|
2673
|
+
tool_use_id: id,
|
|
2674
|
+
content: aggregatedOutput || fallbackText,
|
|
2675
|
+
is_error: isError,
|
|
2676
|
+
}];
|
|
2677
|
+
}
|
|
2678
|
+
if (type === "file_change") {
|
|
2679
|
+
// 注意:codex exec stream 没有 old_string/new_string——只给 path + kind。
|
|
2680
|
+
// 这里每个 file 一个 sub-id(`${item.id}#${i}`),这样如果 codex 一次给多
|
|
2681
|
+
// 个文件,每个文件能独立成卡片 + 独立 tool_result 状态。
|
|
2682
|
+
const rawChanges = Array.isArray(item.changes) ? item.changes : [];
|
|
2683
|
+
const status = typeof item.status === "string" ? item.status : completed ? "completed" : "in_progress";
|
|
2684
|
+
const isError = status === "failed";
|
|
2685
|
+
const blocks = [];
|
|
2686
|
+
rawChanges.forEach((entry, idx) => {
|
|
2687
|
+
if (!entry || typeof entry !== "object")
|
|
2688
|
+
return;
|
|
2689
|
+
const change = entry;
|
|
2690
|
+
const path = typeof change.path === "string" ? change.path : "";
|
|
2691
|
+
const kind = typeof change.kind === "string" ? change.kind : "update";
|
|
2692
|
+
const subId = `${id}#${idx}`;
|
|
2693
|
+
let toolName;
|
|
2694
|
+
let input;
|
|
2695
|
+
if (kind === "add") {
|
|
2696
|
+
toolName = "Write";
|
|
2697
|
+
input = { file_path: path, content: "" };
|
|
2698
|
+
}
|
|
2699
|
+
else if (kind === "delete") {
|
|
2700
|
+
// 复用 Bash 终端卡,rm 语义直观
|
|
2701
|
+
toolName = "Bash";
|
|
2702
|
+
input = { command: `rm ${path}`, description: `delete ${path}`, status };
|
|
2703
|
+
}
|
|
2704
|
+
else {
|
|
2705
|
+
toolName = "Edit";
|
|
2706
|
+
input = { file_path: path, old_string: "", new_string: "" };
|
|
2707
|
+
}
|
|
2708
|
+
if (!completed) {
|
|
2709
|
+
blocks.push({ type: "tool_use", id: subId, name: toolName, input });
|
|
2710
|
+
}
|
|
2711
|
+
else {
|
|
2712
|
+
blocks.push({ type: "tool_use", id: subId, name: toolName, input });
|
|
2713
|
+
blocks.push({
|
|
2714
|
+
type: "tool_result",
|
|
2715
|
+
tool_use_id: subId,
|
|
2716
|
+
content: isError ? `file change failed: ${path}` : "",
|
|
2717
|
+
is_error: isError,
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2720
|
+
});
|
|
2721
|
+
return blocks;
|
|
2722
|
+
}
|
|
2723
|
+
if (type === "mcp_tool_call") {
|
|
2724
|
+
const server = typeof item.server === "string" ? item.server : "mcp";
|
|
2725
|
+
const tool = typeof item.tool === "string" ? item.tool : "tool";
|
|
2726
|
+
const args = item.arguments && typeof item.arguments === "object" ? item.arguments : {};
|
|
2727
|
+
const errObj = item.error && typeof item.error === "object" ? item.error : null;
|
|
2728
|
+
const status = typeof item.status === "string" ? item.status : completed ? "completed" : "in_progress";
|
|
2729
|
+
const isError = !!errObj || status === "failed";
|
|
2730
|
+
if (!completed) {
|
|
2731
|
+
return [{
|
|
2732
|
+
type: "tool_use",
|
|
2733
|
+
id,
|
|
2734
|
+
name: `${server}__${tool}`,
|
|
2735
|
+
input: args,
|
|
2736
|
+
}];
|
|
2737
|
+
}
|
|
2738
|
+
let resultText = "";
|
|
2739
|
+
if (errObj && typeof errObj.message === "string") {
|
|
2740
|
+
resultText = errObj.message;
|
|
2741
|
+
}
|
|
2742
|
+
else if (item.result && typeof item.result === "object") {
|
|
2743
|
+
const resultRec = item.result;
|
|
2744
|
+
const inner = this.extractCodexText(resultRec.content);
|
|
2745
|
+
resultText = inner || JSON.stringify(resultRec).slice(0, 4096);
|
|
2746
|
+
}
|
|
2747
|
+
return [{
|
|
2748
|
+
type: "tool_result",
|
|
2749
|
+
tool_use_id: id,
|
|
2750
|
+
content: resultText,
|
|
2751
|
+
is_error: isError,
|
|
2752
|
+
}];
|
|
2753
|
+
}
|
|
2754
|
+
if (type === "web_search") {
|
|
2755
|
+
const query = typeof item.query === "string" ? item.query : "";
|
|
2756
|
+
if (!completed) {
|
|
2757
|
+
return [{
|
|
2758
|
+
type: "tool_use",
|
|
2759
|
+
id,
|
|
2760
|
+
name: "WebSearch",
|
|
2761
|
+
input: { query },
|
|
2762
|
+
}];
|
|
2763
|
+
}
|
|
2764
|
+
return [{
|
|
2765
|
+
type: "tool_result",
|
|
2766
|
+
tool_use_id: id,
|
|
2767
|
+
// codex 不在 exec 流里回 search 结果,这里给个占位让 UI 卡片完成态。
|
|
2768
|
+
content: query ? `query: ${query}` : "",
|
|
2769
|
+
}];
|
|
2770
|
+
}
|
|
2771
|
+
if (type === "collab_tool_call") {
|
|
2772
|
+
// codex 的子-agent 编排(spawn_agent / send_input / wait / close_agent)。
|
|
2773
|
+
// 没有对应 Claude tool,所以名称用 "Codex/<op>" 让 UI 默认 tool 卡渲染时
|
|
2774
|
+
// 一眼能看出来是 codex 多 agent 操作。
|
|
2775
|
+
const tool = typeof item.tool === "string" ? item.tool : "collab";
|
|
2776
|
+
const prompt = typeof item.prompt === "string" ? item.prompt : "";
|
|
2777
|
+
const senderId = typeof item.sender_thread_id === "string" ? item.sender_thread_id : "";
|
|
2778
|
+
const receiverIds = Array.isArray(item.receiver_thread_ids)
|
|
2779
|
+
? item.receiver_thread_ids.filter((v) => typeof v === "string")
|
|
2780
|
+
: [];
|
|
2781
|
+
const agentsStates = item.agents_states && typeof item.agents_states === "object"
|
|
2782
|
+
? item.agents_states
|
|
2783
|
+
: {};
|
|
2784
|
+
const status = typeof item.status === "string" ? item.status : completed ? "completed" : "in_progress";
|
|
2785
|
+
const toolName = `Codex/${tool}`;
|
|
2786
|
+
const input = { tool };
|
|
2787
|
+
if (prompt)
|
|
2788
|
+
input.prompt = prompt;
|
|
2789
|
+
if (senderId)
|
|
2790
|
+
input.sender_thread_id = senderId;
|
|
2791
|
+
if (receiverIds.length > 0)
|
|
2792
|
+
input.receiver_thread_ids = receiverIds;
|
|
2793
|
+
if (Object.keys(agentsStates).length > 0)
|
|
2794
|
+
input.agents_states = agentsStates;
|
|
2795
|
+
if (!completed) {
|
|
2796
|
+
return [{ type: "tool_use", id, name: toolName, input }];
|
|
2797
|
+
}
|
|
2798
|
+
// 完成态:把每个 receiver agent 的最终状态汇总成可读 result。
|
|
2799
|
+
const summaryLines = [];
|
|
2800
|
+
for (const [tid, state] of Object.entries(agentsStates)) {
|
|
2801
|
+
if (!state || typeof state !== "object")
|
|
2802
|
+
continue;
|
|
2803
|
+
const rec = state;
|
|
2804
|
+
const s = typeof rec.status === "string" ? rec.status : "?";
|
|
2805
|
+
const msg = typeof rec.message === "string" && rec.message ? ` — ${rec.message}` : "";
|
|
2806
|
+
summaryLines.push(`${tid.slice(0, 8)}: ${s}${msg}`);
|
|
2807
|
+
}
|
|
2808
|
+
const isError = status === "failed"
|
|
2809
|
+
|| summaryLines.some((l) => /errored|not_found|interrupted/.test(l));
|
|
2810
|
+
const content = summaryLines.length > 0
|
|
2811
|
+
? summaryLines.join("\n")
|
|
2812
|
+
: (status === "completed" ? "ok" : status);
|
|
2813
|
+
return [
|
|
2814
|
+
{ type: "tool_use", id, name: toolName, input },
|
|
2815
|
+
{ type: "tool_result", tool_use_id: id, content, is_error: isError },
|
|
2816
|
+
];
|
|
2817
|
+
}
|
|
2818
|
+
if (type === "todo_list") {
|
|
2819
|
+
// codex 的 todo: { items: [{ text, completed: bool }] }
|
|
2820
|
+
// wand UI(renderTodoWrite)读的是 block.input.todos = [{content, status, activeForm}]
|
|
2821
|
+
// 这里做形状翻译;in_progress 状态 codex 不区分,全部 pending → completed 二值。
|
|
2822
|
+
const rawItems = Array.isArray(item.items) ? item.items : [];
|
|
2823
|
+
const todos = rawItems.map((entry) => {
|
|
2824
|
+
const rec = (entry && typeof entry === "object") ? entry : {};
|
|
2825
|
+
const text = typeof rec.text === "string" ? rec.text : "";
|
|
2826
|
+
const done = rec.completed === true;
|
|
2583
2827
|
return {
|
|
2828
|
+
content: text,
|
|
2829
|
+
status: done ? "completed" : "pending",
|
|
2830
|
+
activeForm: text,
|
|
2831
|
+
};
|
|
2832
|
+
});
|
|
2833
|
+
return [{
|
|
2584
2834
|
type: "tool_use",
|
|
2585
2835
|
id,
|
|
2586
|
-
name: "
|
|
2587
|
-
input: {
|
|
2588
|
-
};
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
is_error: typeof exitCode === "number" && exitCode !== 0,
|
|
2595
|
-
};
|
|
2836
|
+
name: "TodoWrite",
|
|
2837
|
+
input: { todos },
|
|
2838
|
+
}];
|
|
2839
|
+
}
|
|
2840
|
+
if (type === "error") {
|
|
2841
|
+
// item-level error(不是 top-level error 事件,那个走 codexErrors / 退出报错路径)
|
|
2842
|
+
const message = this.extractCodexText(item) || "codex item error";
|
|
2843
|
+
return [{ type: "text", text: `❌ ${message}` }];
|
|
2596
2844
|
}
|
|
2845
|
+
// unknown / 兜底:completed 时尝试取 text 字段免得 silently 丢
|
|
2597
2846
|
if (completed) {
|
|
2598
2847
|
const text = this.extractCodexText(item);
|
|
2599
2848
|
if (text)
|
|
2600
|
-
return { type: "text", text };
|
|
2849
|
+
return [{ type: "text", text }];
|
|
2601
2850
|
}
|
|
2602
|
-
return
|
|
2851
|
+
return [];
|
|
2603
2852
|
}
|
|
2604
2853
|
upsertCodexBlock(blocks, block) {
|
|
2854
|
+
// tool_use 按 id 去重——file_change 在 item.started 已经 push 过一份 tool_use,
|
|
2855
|
+
// 到 item.completed 还会再发一份相同 id 的(带 status 更新),不去重就出现
|
|
2856
|
+
// 两张同名卡片。command_execution 不受影响(它在 completed 只 emit tool_result)。
|
|
2857
|
+
if (block.type === "tool_use") {
|
|
2858
|
+
const existingIndex = blocks.findIndex((existing) => existing.type === "tool_use" && existing.id === block.id);
|
|
2859
|
+
if (existingIndex >= 0) {
|
|
2860
|
+
blocks[existingIndex] = block;
|
|
2861
|
+
return;
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2605
2864
|
if (block.type === "tool_result") {
|
|
2606
2865
|
const toolUseIndex = blocks.findIndex((existing) => existing.type === "tool_use" && existing.id === block.tool_use_id);
|
|
2607
2866
|
if (toolUseIndex >= 0) {
|
|
@@ -2725,8 +2984,12 @@ export class StructuredSessionManager {
|
|
|
2725
2984
|
inputTokens: typeof source.input_tokens === "number" ? source.input_tokens : undefined,
|
|
2726
2985
|
outputTokens: typeof source.output_tokens === "number" ? source.output_tokens : undefined,
|
|
2727
2986
|
cacheReadInputTokens: typeof source.cached_input_tokens === "number" ? source.cached_input_tokens : undefined,
|
|
2987
|
+
reasoningOutputTokens: typeof source.reasoning_output_tokens === "number" ? source.reasoning_output_tokens : undefined,
|
|
2728
2988
|
};
|
|
2729
|
-
if (value.inputTokens === undefined
|
|
2989
|
+
if (value.inputTokens === undefined
|
|
2990
|
+
&& value.outputTokens === undefined
|
|
2991
|
+
&& value.cacheReadInputTokens === undefined
|
|
2992
|
+
&& value.reasoningOutputTokens === undefined) {
|
|
2730
2993
|
return undefined;
|
|
2731
2994
|
}
|
|
2732
2995
|
return value;
|
package/dist/types.d.ts
CHANGED
|
@@ -365,6 +365,8 @@ export interface ConversationTurn {
|
|
|
365
365
|
outputTokens?: number;
|
|
366
366
|
cacheReadInputTokens?: number;
|
|
367
367
|
cacheCreationInputTokens?: number;
|
|
368
|
+
/** codex 专属:reasoning_output_tokens(GPT-5 等带思考模型,per-turn 计费)。 */
|
|
369
|
+
reasoningOutputTokens?: number;
|
|
368
370
|
totalCostUsd?: number;
|
|
369
371
|
};
|
|
370
372
|
}
|