lark-remote 0.1.5 → 0.1.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/CHANGELOG.md +17 -0
- package/README.en.md +0 -4
- package/README.md +0 -4
- package/dist/bridge/approval-coordinator.d.ts +108 -0
- package/dist/bridge/approval-coordinator.js +258 -0
- package/dist/bridge/index.d.ts +62 -0
- package/dist/bridge/index.js +402 -21
- package/dist/bridge/queue-manager.d.ts +9 -0
- package/dist/bridge/queue-manager.js +19 -11
- package/dist/card/approval-render.d.ts +16 -0
- package/dist/card/approval-render.js +247 -0
- package/dist/card/card-shared.d.ts +14 -0
- package/dist/card/card-shared.js +28 -0
- package/dist/card/run-renderer.js +77 -6
- package/dist/card/run-state.d.ts +25 -3
- package/dist/card/run-state.js +203 -2
- package/dist/config/dir.js +1 -1
- package/dist/config/index.d.ts +42 -0
- package/dist/config/index.js +23 -1
- package/dist/connector/index.d.ts +5 -0
- package/dist/connector/index.js +12 -0
- package/dist/index.js +44 -22
- package/dist/router/config/codex.d.ts +1 -1
- package/dist/router/config/codex.js +49 -0
- package/dist/router/config/common/render.js +4 -0
- package/dist/router/config/types.d.ts +1 -1
- package/dist/router/index.d.ts +27 -2
- package/dist/router/index.js +109 -28
- package/dist/router/utils.d.ts +2 -0
- package/dist/router/utils.js +5 -2
- package/dist/runner/codex/app-server/client.d.ts +82 -0
- package/dist/runner/codex/app-server/client.js +234 -0
- package/dist/runner/codex/app-server/connection-manager.d.ts +56 -0
- package/dist/runner/codex/app-server/connection-manager.js +170 -0
- package/dist/runner/codex/app-server/protocol-types.d.ts +527 -0
- package/dist/runner/codex/app-server/protocol-types.js +67 -0
- package/dist/runner/codex/app-server/runner.d.ts +121 -0
- package/dist/runner/codex/app-server/runner.js +554 -0
- package/dist/runner/codex/app-server/translator.d.ts +109 -0
- package/dist/runner/codex/app-server/translator.js +734 -0
- package/dist/runner/codex/app-server/transport.d.ts +49 -0
- package/dist/runner/codex/app-server/transport.js +231 -0
- package/dist/runner/codex/factory.d.ts +29 -0
- package/dist/runner/codex/factory.js +39 -0
- package/dist/runner/codex/index.d.ts +2 -0
- package/dist/runner/codex/index.js +2 -0
- package/dist/runner/codex/runner.d.ts +3 -0
- package/dist/runner/codex/runner.js +5 -0
- package/dist/runner/common/usage.d.ts +2 -0
- package/dist/runner/common/usage.js +2 -0
- package/dist/runner/index.d.ts +2 -2
- package/dist/runner/index.js +1 -1
- package/dist/runner/types.d.ts +123 -2
- package/dist/session/codex/rollout-reader.js +43 -0
- package/package.json +1 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSONL-RPC transport for the Codex app-server protocol.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a child process (spawned from `codex app-server`) with
|
|
5
|
+
* JSON-line-based bidirectional communication: each line on stdout is a JSON
|
|
6
|
+
* message from the server; each `write()` sends a JSON line to stdin.
|
|
7
|
+
*/
|
|
8
|
+
export interface TransportEvents {
|
|
9
|
+
onMessage(msg: unknown): void;
|
|
10
|
+
onClose(reason: string): void;
|
|
11
|
+
}
|
|
12
|
+
export declare class JsonlRpcTransport {
|
|
13
|
+
private proc;
|
|
14
|
+
private _closed;
|
|
15
|
+
private events;
|
|
16
|
+
private stopper;
|
|
17
|
+
private readonly binary;
|
|
18
|
+
private readonly args;
|
|
19
|
+
private readonly cwd;
|
|
20
|
+
private readonly env;
|
|
21
|
+
/** JSON lines buffered while stdin is under backpressure (bounded by callers). */
|
|
22
|
+
private writeQueue;
|
|
23
|
+
private flushing;
|
|
24
|
+
constructor(opts: {
|
|
25
|
+
binary: string;
|
|
26
|
+
args: string[];
|
|
27
|
+
cwd: string;
|
|
28
|
+
env?: Record<string, string | undefined>;
|
|
29
|
+
});
|
|
30
|
+
get closed(): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Spawn the child process and start reading its stdout.
|
|
33
|
+
* Returns when the process closes (or immediately if spawn fails).
|
|
34
|
+
*/
|
|
35
|
+
start(events: TransportEvents): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Write a JSON message as a single line to the process's stdin.
|
|
38
|
+
* On EPIPE, triggers onClose('epipe') without throwing.
|
|
39
|
+
*/
|
|
40
|
+
write(msg: object): void;
|
|
41
|
+
/** Send queued lines to stdin, pausing on backpressure until drain. */
|
|
42
|
+
private flush;
|
|
43
|
+
/**
|
|
44
|
+
* Gracefully stop the child process: SIGTERM → grace → SIGKILL.
|
|
45
|
+
* Idempotent — safe to call multiple times.
|
|
46
|
+
*/
|
|
47
|
+
close(): Promise<void>;
|
|
48
|
+
private handleClose;
|
|
49
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSONL-RPC transport for the Codex app-server protocol.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a child process (spawned from `codex app-server`) with
|
|
5
|
+
* JSON-line-based bidirectional communication: each line on stdout is a JSON
|
|
6
|
+
* message from the server; each `write()` sends a JSON line to stdin.
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import { ProcessStopper } from '../../common/process-stopper.js';
|
|
10
|
+
import { getLogger } from '../../../logger/index.js';
|
|
11
|
+
/** Max bytes for a single line before we disconnect (10MB). */
|
|
12
|
+
const MAX_LINE_BYTES = 10 * 1024 * 1024;
|
|
13
|
+
/** Max bytes retained from stderr for diagnostics (64KB). */
|
|
14
|
+
const MAX_STDERR_BYTES = 64 * 1024;
|
|
15
|
+
export class JsonlRpcTransport {
|
|
16
|
+
proc = null;
|
|
17
|
+
_closed = false;
|
|
18
|
+
events = null;
|
|
19
|
+
stopper;
|
|
20
|
+
binary;
|
|
21
|
+
args;
|
|
22
|
+
cwd;
|
|
23
|
+
env;
|
|
24
|
+
/** JSON lines buffered while stdin is under backpressure (bounded by callers). */
|
|
25
|
+
writeQueue = [];
|
|
26
|
+
flushing = false;
|
|
27
|
+
constructor(opts) {
|
|
28
|
+
this.binary = opts.binary;
|
|
29
|
+
this.args = opts.args;
|
|
30
|
+
this.cwd = opts.cwd;
|
|
31
|
+
this.env = opts.env ?? {};
|
|
32
|
+
this.stopper = new ProcessStopper({ graceMs: 5000 });
|
|
33
|
+
}
|
|
34
|
+
get closed() {
|
|
35
|
+
return this._closed;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Spawn the child process and start reading its stdout.
|
|
39
|
+
* Returns when the process closes (or immediately if spawn fails).
|
|
40
|
+
*/
|
|
41
|
+
async start(events) {
|
|
42
|
+
this.events = events;
|
|
43
|
+
// 与 exec 模式对齐(SpawningRunner.getSpawnEnv 全量透传 process.env):
|
|
44
|
+
// codex 是用户自己的可信二进制,provider 认证依赖 OPENAI_API_KEY /
|
|
45
|
+
// ANTHROPIC_API_KEY / 自定义 provider 的 env_key(codex-config.ts
|
|
46
|
+
// providerEnvKeys),代理环境依赖 HTTP(S)_PROXY、TMPDIR 等;任何白名单
|
|
47
|
+
// 收窄都会打断认证或网络。调用方 this.env 覆盖 process.env 同名键。
|
|
48
|
+
const childEnv = { ...process.env, ...this.env };
|
|
49
|
+
const proc = spawn(this.binary, this.args, {
|
|
50
|
+
cwd: this.cwd,
|
|
51
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
52
|
+
detached: true,
|
|
53
|
+
env: childEnv,
|
|
54
|
+
});
|
|
55
|
+
this.proc = proc;
|
|
56
|
+
if (proc.pid === undefined) {
|
|
57
|
+
// ENOENT early check
|
|
58
|
+
const err = await new Promise((resolve) => {
|
|
59
|
+
proc.once('error', (e) => resolve(e));
|
|
60
|
+
setTimeout(() => resolve(undefined), 5000);
|
|
61
|
+
});
|
|
62
|
+
this._closed = true;
|
|
63
|
+
const reason = err ? `ENOENT: ${this.binary} not found` : `spawn failed: ${this.binary}`;
|
|
64
|
+
events.onClose(reason);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
getLogger().info(`[codex-transport] spawned pid=${proc.pid} binary=${this.binary}`);
|
|
68
|
+
proc.stdin?.on('error', (err) => {
|
|
69
|
+
getLogger().warn(`[codex-transport] stdin error: ${err.message}`);
|
|
70
|
+
this.handleClose('epipe');
|
|
71
|
+
});
|
|
72
|
+
// Stderr: rolling buffer, last 64KB retained, only log
|
|
73
|
+
let stderrBuf = '';
|
|
74
|
+
proc.stderr?.on('data', (chunk) => {
|
|
75
|
+
const text = chunk.toString('utf-8');
|
|
76
|
+
stderrBuf = (stderrBuf + text).slice(-MAX_STDERR_BYTES);
|
|
77
|
+
getLogger().debug(`[codex-transport] stderr: ${text.trimEnd()}`);
|
|
78
|
+
});
|
|
79
|
+
// Stdout: line-split with 10MB line limit
|
|
80
|
+
let remainder = '';
|
|
81
|
+
proc.stdout?.on('data', (chunk) => {
|
|
82
|
+
remainder += chunk.toString('utf-8');
|
|
83
|
+
const lines = remainder.split('\n');
|
|
84
|
+
// Lines except the last (incomplete) one
|
|
85
|
+
remainder = lines.pop() ?? '';
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
if (line.length === 0)
|
|
88
|
+
continue;
|
|
89
|
+
if (line.length > MAX_LINE_BYTES) {
|
|
90
|
+
getLogger().error(`[codex-transport] line exceeds ${MAX_LINE_BYTES} bytes, disconnecting`);
|
|
91
|
+
this.handleClose('parse_error');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const msg = JSON.parse(line);
|
|
96
|
+
events.onMessage(msg);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
getLogger().warn(`[codex-transport] failed to parse JSON: ${err.message} line=${line.slice(0, 200)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
// Handle process exit
|
|
104
|
+
const onExit = (code, signal) => {
|
|
105
|
+
this._closed = true;
|
|
106
|
+
this.proc = null;
|
|
107
|
+
// Flush remaining buffer
|
|
108
|
+
if (remainder.length > 0) {
|
|
109
|
+
try {
|
|
110
|
+
const msg = JSON.parse(remainder);
|
|
111
|
+
events.onMessage(msg);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// ignore trailing incomplete line
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const reason = signal ? `signal:${signal}` : `exit:${code}`;
|
|
118
|
+
events.onClose(reason);
|
|
119
|
+
getLogger().info(`[codex-transport] process exited pid=${proc.pid} code=${code} signal=${signal}`);
|
|
120
|
+
};
|
|
121
|
+
proc.once('exit', onExit);
|
|
122
|
+
proc.once('error', (err) => {
|
|
123
|
+
getLogger().error(`[codex-transport] process error: ${err.message}`);
|
|
124
|
+
this.handleClose(`error:${err.message}`);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Write a JSON message as a single line to the process's stdin.
|
|
129
|
+
* On EPIPE, triggers onClose('epipe') without throwing.
|
|
130
|
+
*/
|
|
131
|
+
write(msg) {
|
|
132
|
+
if (this._closed || !this.proc?.stdin) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
// 入队后顺序 flush;write() 返回 false(高水位)时暂停直到 drain,
|
|
137
|
+
// 避免大消息/突发消息在 Node 内部无限缓冲(review P3-8)。
|
|
138
|
+
this.writeQueue.push(Buffer.from(JSON.stringify(msg) + '\n', 'utf8'));
|
|
139
|
+
this.flush();
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
this.handleClose('epipe');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Send queued lines to stdin, pausing on backpressure until drain. */
|
|
146
|
+
flush() {
|
|
147
|
+
const stdin = this.proc?.stdin;
|
|
148
|
+
if (this._closed || !stdin || this.flushing)
|
|
149
|
+
return;
|
|
150
|
+
this.flushing = true;
|
|
151
|
+
const next = () => {
|
|
152
|
+
if (this._closed) {
|
|
153
|
+
this.writeQueue = [];
|
|
154
|
+
this.flushing = false;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const chunk = this.writeQueue.shift();
|
|
158
|
+
if (!chunk) {
|
|
159
|
+
this.flushing = false;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const ok = stdin.write(chunk, (err) => {
|
|
164
|
+
if (err) {
|
|
165
|
+
this.handleClose('epipe');
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
if (!ok) {
|
|
169
|
+
// 流满:等待 drain 再发下一条,队列有界(调用方同步节奏)。
|
|
170
|
+
stdin.once('drain', next);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
next();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
this.handleClose('epipe');
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
next();
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Gracefully stop the child process: SIGTERM → grace → SIGKILL.
|
|
184
|
+
* Idempotent — safe to call multiple times.
|
|
185
|
+
*/
|
|
186
|
+
async close() {
|
|
187
|
+
if (this._closed)
|
|
188
|
+
return;
|
|
189
|
+
this._closed = true;
|
|
190
|
+
const proc = this.proc;
|
|
191
|
+
if (proc) {
|
|
192
|
+
try {
|
|
193
|
+
await this.stopper.stop(proc);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// Ignore stop errors — process may already be dead
|
|
197
|
+
}
|
|
198
|
+
if (proc.stdin && !proc.stdin.destroyed) {
|
|
199
|
+
proc.stdin.end();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
handleClose(reason) {
|
|
204
|
+
if (this._closed)
|
|
205
|
+
return;
|
|
206
|
+
this._closed = true;
|
|
207
|
+
this.writeQueue = [];
|
|
208
|
+
// Remove the exit listener to avoid double-firing
|
|
209
|
+
if (this.proc) {
|
|
210
|
+
this.proc.removeAllListeners('exit');
|
|
211
|
+
this.proc.removeAllListeners('error');
|
|
212
|
+
}
|
|
213
|
+
const proc = this.proc;
|
|
214
|
+
this.proc = null;
|
|
215
|
+
if (proc) {
|
|
216
|
+
if (proc.stdin && !proc.stdin.destroyed) {
|
|
217
|
+
proc.stdin.end();
|
|
218
|
+
}
|
|
219
|
+
// 异常路径(EPIPE/超长行/进程 error)也要收掉子进程,避免孤儿进程
|
|
220
|
+
// 继续运行(如超长行场景子进程还在往 stdout 灌数据)。
|
|
221
|
+
void this.stopper.stop(proc).catch((err) => {
|
|
222
|
+
getLogger().warn(`[codex-transport] failed to stop process during close: ${err instanceof Error ? err.message : String(err)}`);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
// 统一关闭出口:EPIPE/超长行/进程 error 都必须通知上层(connection-manager
|
|
226
|
+
// 靠 onClose 删除 slot、client 靠它 failPending),否则 slot 悬挂死连接。
|
|
227
|
+
this.events?.onClose(reason);
|
|
228
|
+
getLogger().info(`[codex-transport] closed: ${reason}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex runner factory: selects the concrete runner based on `serviceMode`.
|
|
3
|
+
*
|
|
4
|
+
* `exec` (default) → CodexExecRunner (spawn-per-message `codex exec`).
|
|
5
|
+
* `app-server` → CodexAppServerRunner (persistent `codex app-server` JSON-RPC
|
|
6
|
+
* connection with approval support and sandbox enforcement).
|
|
7
|
+
*/
|
|
8
|
+
import type { AgentSessionReader } from '../types.js';
|
|
9
|
+
import { CodexExecRunner } from './runner.js';
|
|
10
|
+
import { CodexAppServerRunner } from './app-server/runner.js';
|
|
11
|
+
import type { AskForApproval, SandboxMode } from './app-server/protocol-types.js';
|
|
12
|
+
export interface CreateCodexRunnerOptions {
|
|
13
|
+
model?: string;
|
|
14
|
+
modelProvider?: string;
|
|
15
|
+
reasoningEffort?: string;
|
|
16
|
+
stopGraceMs?: number;
|
|
17
|
+
pidDir: string;
|
|
18
|
+
workspace: string;
|
|
19
|
+
sessionReader: AgentSessionReader;
|
|
20
|
+
serviceMode?: 'exec' | 'app-server';
|
|
21
|
+
sandbox?: SandboxMode;
|
|
22
|
+
approvalPolicy?: AskForApproval;
|
|
23
|
+
appServerBinary?: string;
|
|
24
|
+
appServerArgs?: string[];
|
|
25
|
+
appServerRequestTimeoutMs?: number;
|
|
26
|
+
appServerIdleTtlMs?: number;
|
|
27
|
+
appServerTurnIdleTimeoutMinutes?: number;
|
|
28
|
+
}
|
|
29
|
+
export declare function createCodexRunner(opts: CreateCodexRunnerOptions): CodexExecRunner | CodexAppServerRunner;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex runner factory: selects the concrete runner based on `serviceMode`.
|
|
3
|
+
*
|
|
4
|
+
* `exec` (default) → CodexExecRunner (spawn-per-message `codex exec`).
|
|
5
|
+
* `app-server` → CodexAppServerRunner (persistent `codex app-server` JSON-RPC
|
|
6
|
+
* connection with approval support and sandbox enforcement).
|
|
7
|
+
*/
|
|
8
|
+
import { CodexExecRunner } from './runner.js';
|
|
9
|
+
import { CodexAppServerRunner } from './app-server/runner.js';
|
|
10
|
+
export function createCodexRunner(opts) {
|
|
11
|
+
if (opts.serviceMode === 'app-server') {
|
|
12
|
+
return new CodexAppServerRunner({
|
|
13
|
+
kind: 'codex',
|
|
14
|
+
sessionReader: opts.sessionReader,
|
|
15
|
+
binary: opts.appServerBinary ?? 'codex',
|
|
16
|
+
appServerArgs: opts.appServerArgs,
|
|
17
|
+
requestTimeoutMs: opts.appServerRequestTimeoutMs,
|
|
18
|
+
idleTtlMs: opts.appServerIdleTtlMs,
|
|
19
|
+
turnTimeoutMs: opts.appServerTurnIdleTimeoutMinutes != null
|
|
20
|
+
? opts.appServerTurnIdleTimeoutMinutes * 60_000
|
|
21
|
+
: undefined,
|
|
22
|
+
model: opts.model,
|
|
23
|
+
modelProvider: opts.modelProvider,
|
|
24
|
+
reasoningEffort: opts.reasoningEffort,
|
|
25
|
+
sandbox: opts.sandbox,
|
|
26
|
+
approvalPolicy: opts.approvalPolicy,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return new CodexExecRunner({
|
|
30
|
+
model: opts.model,
|
|
31
|
+
modelProvider: opts.modelProvider,
|
|
32
|
+
reasoningEffort: opts.reasoningEffort,
|
|
33
|
+
stopGraceMs: opts.stopGraceMs,
|
|
34
|
+
pidDir: opts.pidDir,
|
|
35
|
+
workspace: opts.workspace,
|
|
36
|
+
sessionReader: opts.sessionReader,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=factory.js.map
|
|
@@ -52,6 +52,9 @@ export declare class CodexExecRunner extends SpawningRunner implements AgentRunn
|
|
|
52
52
|
private readonly reasoningEffort?;
|
|
53
53
|
constructor(opts: CodexExecRunnerOptions);
|
|
54
54
|
getStatusInfo(): AgentStatusInfo;
|
|
55
|
+
/** exec 模式的 turn.completed.usage 是会话累计值,flow 字段必须 jsonl 优先
|
|
56
|
+
* (review P3-7:桥侧统一走本接口,替代 agentKind 硬编码)。 */
|
|
57
|
+
getUsageAuthority(): 'jsonl';
|
|
55
58
|
protected buildArgv(opts: SpawnOptions): string[];
|
|
56
59
|
protected getStdio(): ('ignore' | 'pipe')[];
|
|
57
60
|
protected setupStdin(proc: ChildProcess, message: string): void;
|
|
@@ -72,6 +72,11 @@ export class CodexExecRunner extends SpawningRunner {
|
|
|
72
72
|
extras: { sandbox: 'danger-full-access' },
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
|
+
/** exec 模式的 turn.completed.usage 是会话累计值,flow 字段必须 jsonl 优先
|
|
76
|
+
* (review P3-7:桥侧统一走本接口,替代 agentKind 硬编码)。 */
|
|
77
|
+
getUsageAuthority() {
|
|
78
|
+
return 'jsonl';
|
|
79
|
+
}
|
|
75
80
|
buildArgv(opts) {
|
|
76
81
|
return buildCodexExecArgs({
|
|
77
82
|
cwd: opts.cwd,
|
|
@@ -23,6 +23,8 @@ interface NormalizedResultUsage {
|
|
|
23
23
|
cacheReadTokens?: number;
|
|
24
24
|
cacheCreationTokens?: number;
|
|
25
25
|
totalTokens?: number;
|
|
26
|
+
/** Model context window limit (codex app-server `modelContextWindow` passthrough). */
|
|
27
|
+
contextLimit?: number;
|
|
26
28
|
/** Reconstructed or agent-declared context length (see JSDoc above). */
|
|
27
29
|
contextLength: number;
|
|
28
30
|
}
|
|
@@ -14,6 +14,7 @@ export function normalizeResultUsage(usage) {
|
|
|
14
14
|
const cacheRead = usage.cache_read_tokens ?? usage.cache_read_input_tokens;
|
|
15
15
|
const cacheCreation = usage.cache_creation_tokens ?? usage.cache_creation_input_tokens;
|
|
16
16
|
const totalTokens = usage.total_tokens;
|
|
17
|
+
const contextLimit = usage.context_limit;
|
|
17
18
|
// total_tokens (agent-declared total) takes priority; otherwise reconstruct
|
|
18
19
|
// from the parts: input + cacheRead + cacheCreation + output.
|
|
19
20
|
const contextLength = totalTokens ?? inputTokens + (cacheRead ?? 0) + (cacheCreation ?? 0) + outputTokens;
|
|
@@ -23,6 +24,7 @@ export function normalizeResultUsage(usage) {
|
|
|
23
24
|
cacheReadTokens: cacheRead,
|
|
24
25
|
cacheCreationTokens: cacheCreation,
|
|
25
26
|
totalTokens,
|
|
27
|
+
contextLimit,
|
|
26
28
|
contextLength,
|
|
27
29
|
};
|
|
28
30
|
}
|
package/dist/runner/index.d.ts
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* (共享工具由消费方直接 import `common/*` 具体模块,不走 barrel)
|
|
9
9
|
*/
|
|
10
10
|
export { ClaudeRunner } from './claude/index.js';
|
|
11
|
-
export { CodexExecRunner } from './codex/index.js';
|
|
11
|
+
export { CodexExecRunner, CodexAppServerRunner, createCodexRunner } from './codex/index.js';
|
|
12
12
|
export { OpencodeExecRunner } from './opencode/index.js';
|
|
13
13
|
export { PiRunner } from './pi/index.js';
|
|
14
14
|
export { KimiRunner } from './kimi/index.js';
|
|
15
15
|
export { BashProcessRunner, type BashRunner } from './bash/index.js';
|
|
16
16
|
export { AgentRegistry } from './registry.js';
|
|
17
|
-
export type { ResultEvent, PlanEvent, FileChangeEvent, AgentEvent, Runner, AgentKind, AgentSession, AgentSessionContentEvent, AgentSessionUsage, SessionContent, AgentSessionReader, AgentRunner, } from './types.js';
|
|
17
|
+
export type { ResultEvent, PlanEvent, FileChangeEvent, AgentEvent, Runner, RunnerLifetime, AgentKind, AgentSession, AgentSessionContentEvent, AgentSessionUsage, SessionContent, AgentSessionReader, AgentRunner, TurnStartedEvent, TurnDiffEvent, ApprovalView, ApprovalRequestedEvent, ApprovalResolvedEvent, } from './types.js';
|
|
18
18
|
export { resolveAgentChoices } from './resolve-agent-choices.js';
|
|
19
19
|
export { syncAgentChoices } from './sync-agent-choices.js';
|
package/dist/runner/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
// Re-export AgentRunner 实现(从各 agent 目录)
|
|
11
11
|
export { ClaudeRunner } from './claude/index.js';
|
|
12
|
-
export { CodexExecRunner } from './codex/index.js';
|
|
12
|
+
export { CodexExecRunner, CodexAppServerRunner, createCodexRunner } from './codex/index.js';
|
|
13
13
|
export { OpencodeExecRunner } from './opencode/index.js';
|
|
14
14
|
export { PiRunner } from './pi/index.js';
|
|
15
15
|
export { KimiRunner } from './kimi/index.js';
|
package/dist/runner/types.d.ts
CHANGED
|
@@ -21,6 +21,75 @@ interface SystemCompactEvent {
|
|
|
21
21
|
preTokens: number;
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
+
export interface ApprovalRequestedEvent {
|
|
25
|
+
type: 'approval_requested';
|
|
26
|
+
/** JSON-RPC id of the server request. Wire type is string | integer — keep raw. */
|
|
27
|
+
requestId: number | string;
|
|
28
|
+
kind: 'command' | 'file' | 'permissions';
|
|
29
|
+
threadId: string;
|
|
30
|
+
turnId: string;
|
|
31
|
+
itemId: string;
|
|
32
|
+
view: ApprovalView;
|
|
33
|
+
timestamp?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface ApprovalResolvedEvent {
|
|
36
|
+
type: 'approval_resolved';
|
|
37
|
+
requestId: number | string;
|
|
38
|
+
outcome: 'resolved' | 'expired';
|
|
39
|
+
timestamp?: string;
|
|
40
|
+
}
|
|
41
|
+
/** Approval card content update (out-of-order item/started arrives late). */
|
|
42
|
+
export interface ApprovalViewUpdatedEvent {
|
|
43
|
+
type: 'approval_view_updated';
|
|
44
|
+
requestId: number | string;
|
|
45
|
+
view: ApprovalView;
|
|
46
|
+
timestamp?: string;
|
|
47
|
+
}
|
|
48
|
+
/** Approval expired locally (bridge-side timeout) — card should show expired UI. */
|
|
49
|
+
export interface ApprovalExpiredEvent {
|
|
50
|
+
type: 'approval_expired';
|
|
51
|
+
requestId: number | string;
|
|
52
|
+
timestamp?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface ApprovalView {
|
|
55
|
+
requestId: number | string;
|
|
56
|
+
kind: 'command' | 'file' | 'permissions';
|
|
57
|
+
reason?: string;
|
|
58
|
+
threadShort: string;
|
|
59
|
+
turnShort: string;
|
|
60
|
+
workspace: string;
|
|
61
|
+
command?: string;
|
|
62
|
+
commandCwd?: string;
|
|
63
|
+
fileChanges?: Array<{
|
|
64
|
+
path: string;
|
|
65
|
+
kind: 'add' | 'update' | 'delete';
|
|
66
|
+
diff?: string;
|
|
67
|
+
}>;
|
|
68
|
+
network?: {
|
|
69
|
+
host: string;
|
|
70
|
+
protocol: string;
|
|
71
|
+
};
|
|
72
|
+
permissions?: {
|
|
73
|
+
networkEnabled?: boolean;
|
|
74
|
+
fileSystemRead?: string[];
|
|
75
|
+
fileSystemWrite?: string[];
|
|
76
|
+
items: Array<{
|
|
77
|
+
id: string;
|
|
78
|
+
label: string;
|
|
79
|
+
target: {
|
|
80
|
+
kind: 'network';
|
|
81
|
+
} | {
|
|
82
|
+
kind: 'fsRead' | 'fsWrite';
|
|
83
|
+
path: string;
|
|
84
|
+
};
|
|
85
|
+
selected: boolean;
|
|
86
|
+
}>;
|
|
87
|
+
};
|
|
88
|
+
availableDecisions: string[];
|
|
89
|
+
/** Raw payloads for structured decisions (e.g. acceptWithExecpolicyAmendment). */
|
|
90
|
+
decisionPayloads?: Record<string, unknown>;
|
|
91
|
+
pendingTotal: number;
|
|
92
|
+
}
|
|
24
93
|
interface ThinkingContent {
|
|
25
94
|
type: 'thinking';
|
|
26
95
|
thinking: string;
|
|
@@ -58,7 +127,7 @@ export interface UserEvent {
|
|
|
58
127
|
}
|
|
59
128
|
export interface ResultEvent {
|
|
60
129
|
type: 'result';
|
|
61
|
-
subtype: 'success' | 'error';
|
|
130
|
+
subtype: 'success' | 'error' | 'interrupted';
|
|
62
131
|
session_id: string;
|
|
63
132
|
timestamp?: string;
|
|
64
133
|
usage?: {
|
|
@@ -75,6 +144,9 @@ export interface ResultEvent {
|
|
|
75
144
|
cache_creation_input_tokens?: number;
|
|
76
145
|
/** Agent-declared total; when present the display uses max(total, sum of parts). */
|
|
77
146
|
total_tokens?: number;
|
|
147
|
+
/** 当前模型 context window 上限(codex app-server tokenUsage.modelContextWindow
|
|
148
|
+
* 透传)。仅 app-server 提供;缺省时卡片只显示绝对量、不显示百分比。 */
|
|
149
|
+
context_limit?: number;
|
|
78
150
|
};
|
|
79
151
|
total_cost_usd?: number;
|
|
80
152
|
errorMessage?: string;
|
|
@@ -99,7 +171,50 @@ export interface FileChangeEvent {
|
|
|
99
171
|
operation: 'create' | 'edit' | 'delete' | 'read';
|
|
100
172
|
timestamp?: string;
|
|
101
173
|
}
|
|
102
|
-
export
|
|
174
|
+
export interface TurnStartedEvent {
|
|
175
|
+
type: 'turn_started';
|
|
176
|
+
threadId: string;
|
|
177
|
+
turnId: string;
|
|
178
|
+
operationKind: 'turn' | 'compaction';
|
|
179
|
+
timestamp?: string;
|
|
180
|
+
}
|
|
181
|
+
export interface TurnDiffEvent {
|
|
182
|
+
type: 'turn_diff';
|
|
183
|
+
/**
|
|
184
|
+
* Thread item id this snapshot belongs to (app-server item-scoped stream).
|
|
185
|
+
* Each content item (reasoning / agentMessage / commandExecution / plan /
|
|
186
|
+
* fileChange) is tracked independently, so interleaved items keep their
|
|
187
|
+
* real chronology and update their own block instead of a shared one.
|
|
188
|
+
*/
|
|
189
|
+
itemId: string;
|
|
190
|
+
/** Full accumulated assistant text snapshot for this item (replaces previous). */
|
|
191
|
+
text?: string;
|
|
192
|
+
/** Full accumulated reasoning snapshot for this item (replaces previous). */
|
|
193
|
+
reasoning?: string;
|
|
194
|
+
/** Full accumulated tool output snapshot for this item (replaces previous). */
|
|
195
|
+
toolOutput?: string;
|
|
196
|
+
/** Full accumulated plan text snapshot for this item (replaces previous). */
|
|
197
|
+
plan?: string;
|
|
198
|
+
/** Current set of file changes for this item (replaces previous). */
|
|
199
|
+
fileChanges?: Array<{
|
|
200
|
+
path: string;
|
|
201
|
+
kind: 'add' | 'update' | 'delete';
|
|
202
|
+
diff?: string;
|
|
203
|
+
}>;
|
|
204
|
+
/**
|
|
205
|
+
* Authoritative completion snapshot (item/completed or turn/completed).
|
|
206
|
+
* The reducer finalizes the block in place: thinking/plan active=false,
|
|
207
|
+
* tool status ok, completedAt stamped. Never reorders blocks.
|
|
208
|
+
*/
|
|
209
|
+
complete?: boolean;
|
|
210
|
+
/** Authoritative tool status at completion (commandExecution item). */
|
|
211
|
+
toolStatus?: 'ok' | 'error';
|
|
212
|
+
threadId: string;
|
|
213
|
+
turnId: string;
|
|
214
|
+
timestamp?: string;
|
|
215
|
+
}
|
|
216
|
+
export type RunnerLifetime = 'turn' | 'workspace';
|
|
217
|
+
export type AgentEvent = SystemInitEvent | SystemCompactEvent | ApprovalRequestedEvent | ApprovalResolvedEvent | ApprovalViewUpdatedEvent | ApprovalExpiredEvent | AssistantEvent | UserEvent | ResultEvent | PlanEvent | FileChangeEvent | TurnStartedEvent | TurnDiffEvent;
|
|
103
218
|
export interface SpawnOptions {
|
|
104
219
|
cwd: string;
|
|
105
220
|
sessionId?: string;
|
|
@@ -117,6 +232,8 @@ export interface SpawnOptions {
|
|
|
117
232
|
*/
|
|
118
233
|
export interface Runner {
|
|
119
234
|
readonly isRunning: boolean;
|
|
235
|
+
readonly lifetime?: RunnerLifetime;
|
|
236
|
+
dispose?(): Promise<void>;
|
|
120
237
|
run(message: string, opts: SpawnOptions): AsyncGenerator<AgentEvent>;
|
|
121
238
|
/**
|
|
122
239
|
* Stop the running process. SIGTERM → grace → SIGKILL on the per-workspace
|
|
@@ -158,6 +275,8 @@ export interface AgentSessionUsage {
|
|
|
158
275
|
contextLimit?: number;
|
|
159
276
|
/** Claude auto-compact 次数(数 compact_boundary 事件);codex/opencode 无此概念,留 undefined。 */
|
|
160
277
|
compactCount?: number;
|
|
278
|
+
/** 压缩前上下文水位(codex compact 后从会话 jsonl 读取,仅会话以压缩收尾时有值)。 */
|
|
279
|
+
compactPreContextLength?: number;
|
|
161
280
|
/** 从缓存读取的 token 数(节省的费用)。 */
|
|
162
281
|
cacheReadTokens?: number;
|
|
163
282
|
/** 新建缓存的 token 数。 */
|
|
@@ -236,6 +355,8 @@ export interface AgentRunner extends Runner {
|
|
|
236
355
|
readonly sessionReader: AgentSessionReader;
|
|
237
356
|
/** Return current status info for /status display. */
|
|
238
357
|
getStatusInfo(): AgentStatusInfo;
|
|
358
|
+
/** Whether this runner provides live usage data (vs. file-based). */
|
|
359
|
+
getUsageAuthority?(): 'live' | 'jsonl';
|
|
239
360
|
}
|
|
240
361
|
/** Agent self-describing status info for /status display. */
|
|
241
362
|
export interface AgentStatusInfo {
|
|
@@ -58,6 +58,18 @@ export function readCodexRollout(filePath) {
|
|
|
58
58
|
// Run card's "累计" display. Codex emits this alongside last_token_usage.
|
|
59
59
|
let lastTotalUsage;
|
|
60
60
|
let previousTotals;
|
|
61
|
+
// Compaction 统计:codex 的 compact turn 在会话文件里写顶层 `compacted`
|
|
62
|
+
// 事件(含摘要),压缩前后的上下文水位由相邻 token_count 表达。压缩收尾的
|
|
63
|
+
// token_count 增量是 input/cached/output 全 0、只有 total_tokens 有值
|
|
64
|
+
// (窗口被摘要+replacement history 占据),现有 zero-filter 会跳过它,
|
|
65
|
+
// 因此这里必须单独捕获,不能复用 lastRawUsage。
|
|
66
|
+
// 单位口径(review P3-9):前后两侧统一用 total_tokens —— 压缩前取最近一次
|
|
67
|
+
// 非零 token_count 的 total_tokens(input+output,即该 turn 在窗口内的全部
|
|
68
|
+
// token),压缩后取收尾事件的 total_tokens(压缩后窗口)。不得一边 input
|
|
69
|
+
// 一边 total,否则「压缩前 X → 压缩后 Y」两边单位不一致。
|
|
70
|
+
let compactCount = 0;
|
|
71
|
+
let compactPreContextLength;
|
|
72
|
+
let compactPostContextLength;
|
|
61
73
|
for (const line of lines) {
|
|
62
74
|
const trimmed = line.trim();
|
|
63
75
|
if (!trimmed)
|
|
@@ -101,6 +113,27 @@ export function readCodexRollout(filePath) {
|
|
|
101
113
|
const ctxWindow = info?.model_context_window;
|
|
102
114
|
lastContextLimit = typeof ctxWindow === 'number' ? ctxWindow : undefined;
|
|
103
115
|
}
|
|
116
|
+
// 压缩后的水位:压缩收尾 token_count 全 0(raw 不满足上面 zero-filter),
|
|
117
|
+
// 但 total_tokens 表达真实窗口;之后若出现普通 turn(input>0),
|
|
118
|
+
// 水位回归该 turn,post-compact 值失效。
|
|
119
|
+
if (compactCount > 0 && raw) {
|
|
120
|
+
if (raw.total_tokens &&
|
|
121
|
+
!raw.input_tokens &&
|
|
122
|
+
!raw.cached_input_tokens &&
|
|
123
|
+
!raw.output_tokens) {
|
|
124
|
+
compactPostContextLength = raw.total_tokens;
|
|
125
|
+
}
|
|
126
|
+
else if (raw.input_tokens > 0) {
|
|
127
|
+
compactPostContextLength = undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else if (parsed.type === 'compacted') {
|
|
133
|
+
// 顶层 compacted 事件:压缩次数 +1,压缩前水位取最近一次非零 token_count。
|
|
134
|
+
compactCount++;
|
|
135
|
+
if (lastRawUsage?.total_tokens !== undefined) {
|
|
136
|
+
compactPreContextLength = lastRawUsage.total_tokens;
|
|
104
137
|
}
|
|
105
138
|
}
|
|
106
139
|
else if (parsed.type === 'response_item' && parsed.payload) {
|
|
@@ -170,6 +203,16 @@ export function readCodexRollout(filePath) {
|
|
|
170
203
|
usage.cumulativeCacheReadTokens = cumCached;
|
|
171
204
|
usage.cumulativeCacheCreationTokens = 0; // Codex 不报告 cache creation
|
|
172
205
|
}
|
|
206
|
+
// 压缩统计:会话内出现过 compacted 事件就计数(供 /resume 与 Compact 卡
|
|
207
|
+
// 展示);仅当会话以压缩收尾时(post-compact 水位存在),contextLength
|
|
208
|
+
// 覆盖为压缩后水位并暴露压缩前水位——后续还有普通 turn 时水位回归该 turn。
|
|
209
|
+
if (compactCount > 0) {
|
|
210
|
+
usage.compactCount = compactCount;
|
|
211
|
+
if (compactPostContextLength !== undefined) {
|
|
212
|
+
usage.contextLength = compactPostContextLength;
|
|
213
|
+
usage.compactPreContextLength = compactPreContextLength;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
173
216
|
}
|
|
174
217
|
return {
|
|
175
218
|
threadId: sessionId,
|