@coda-console/codex 1.8.3 → 1.8.5

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/agent/entry.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // The Codex agent as Coda starts it (the plugin's `contributes.agents`): the locked codex-acp, run in this process with Coda's
2
2
  // patches, which fail the start once the adapter changes. Between it and Coda the entry speaks Coda's protocol for it
3
- // (waits.mjs, docs/agent-protocol.md): its session instructions become the thread's developer instructions (instructions.mjs),
3
+ // (waits.mjs, docs/agent-protocol.md), and of its own check of a call as that call's (reviews.mjs): its session instructions become the thread's developer instructions (instructions.mjs),
4
4
  // and a steer that finds no turn going is answered `promptRequired` (steering.mjs); the models of the model service chosen in
5
5
  // Coda join Codex's own (providers.mjs). What Coda wants of Codex beyond ACP is set here: the mode a session starts in, and
6
6
  // which Codex program runs, through the account bridge when another plugin chooses the account (accounts.mjs).
@@ -22,7 +22,9 @@ import { promptRequiredCodexSteering } from './steering.mjs';
22
22
  import { servedCodexModels } from './providers.mjs';
23
23
  import { input, listen, relayedCodexStreams } from './relay.mjs';
24
24
  import { CodexWaits } from './waits.mjs';
25
+ import { CodexReviews } from './reviews.mjs';
25
26
  import { explainCodexError, reportedCodexFailures } from './errors.mjs';
27
+ import { reportedCodexSleeps } from './sleep.mjs';
26
28
 
27
29
  const env = process.env;
28
30
  // A session starts read-only, unless its machine's configuration says otherwise.
@@ -47,8 +49,8 @@ if (!env.CODEX_PATH && accountSource(env) && env.CODA_PLUGIN_STATE) {
47
49
 
48
50
  const version = codexVersion();
49
51
  const waits = new CodexWaits({ agentInfo: version ? { name: 'codex', version } : null }), parse = line => { try { return JSON.parse(line); } catch { return null; } };
50
- const say = message => process.stdout.write(`${JSON.stringify(message)}\n`);
51
- listen(line => { for (const message of waits.fromAgent(parse(line))) say(explainCodexError(message)); });
52
+ const reviews = new CodexReviews(), say = message => process.stdout.write(`${JSON.stringify(message)}\n`);
53
+ listen(line => { for (const message of waits.fromAgent(parse(line))) for (const each of reviews.fromAgent(message)) say(explainCodexError(each)); });
52
54
  readLines(process.stdin).on('line', line => {
53
55
  const message = parse(line);
54
56
  if (!message || typeof message !== 'object') return;
@@ -62,7 +64,7 @@ const adapter = new URL(import.meta.resolve('@agentclientprotocol/codex-acp'));
62
64
  registerHooks({ load(url, context, nextLoad) {
63
65
  const loaded = nextLoad(url, context);
64
66
  if (url !== adapter.href) return loaded;
65
- return { ...loaded, source: reportedCodexFailures(codexOptionsAsShown(relayedCodexStreams(orderedCodexTitles(followCodexProcessExit(continuedCodexSubagents(
66
- promptRequiredCodexSteering(instructedCodexThreads(servedCodexModels(networkedCodexDevelopment(Buffer.from(loaded.source).toString('utf8'))))))))))) };
67
+ return { ...loaded, source: reportedCodexSleeps(reportedCodexFailures(codexOptionsAsShown(relayedCodexStreams(orderedCodexTitles(followCodexProcessExit(continuedCodexSubagents(
68
+ promptRequiredCodexSteering(instructedCodexThreads(servedCodexModels(networkedCodexDevelopment(Buffer.from(loaded.source).toString('utf8')))))))))))) };
67
69
  } });
68
70
  await import(adapter.href);
@@ -0,0 +1,55 @@
1
+ // Codex's own check of a call before running it (its automatic approval review), said to Coda as part of that call
2
+ // (docs/agent-protocol.md「工具调用的审核」). codex-acp makes each check a call of its own, `Guardian Review`
3
+ // (`guardian_assessment:<id>`), which names the call it checks (`targetItemId`); here it becomes `_meta.coda.review` of that
4
+ // call. A check can begin before codex-acp tells of the call: the call is told then, pending, and codex-acp's own telling of
5
+ // it later is an update of it.
6
+ const record = value => value && typeof value === 'object' && !Array.isArray(value);
7
+ const CHECK = 'guardian_assessment:';
8
+ const STATE = { inProgress: 'reviewing', approved: 'approved', denied: 'denied', aborted: 'denied', timedOut: 'denied' };
9
+ const RISKS = new Set(['low', 'medium', 'high', 'critical']);
10
+ // What the call is, as far as the check says (each kind of action codex-acp 1.12.0 names), until codex-acp tells it. ACP
11
+ // requires a call's title: one the action does not give is said plainly.
12
+ const PLACEHOLDER = {
13
+ command: a => [a.command, 'execute'],
14
+ execve: a => [(a.argv?.length ? a.argv : [a.program]).join(' '), 'execute'],
15
+ writeStdin: a => [`向进程 ${a.processId} 写入输入`, 'execute'],
16
+ applyPatch: a => [a.files?.length === 1 ? `修改 ${a.files[0]}` : `修改 ${a.files?.length ?? 0} 个文件`, 'edit'],
17
+ networkAccess: a => [`访问 ${a.target || a.host}`, 'fetch'],
18
+ mcpToolCall: a => [`MCP ${a.toolName}(${a.connectorName ?? a.server})`],
19
+ requestPermissions: a => [a.reason || '申请更多权限'],
20
+ };
21
+ const placeholder = action => {
22
+ const [title, kind] = PLACEHOLDER[action?.type]?.(action) ?? [];
23
+ return { title: typeof title === 'string' && title.trim() ? title : '待审核的操作', ...(kind ? { kind } : {}) };
24
+ };
25
+
26
+ export class CodexReviews {
27
+ constructor() {
28
+ // The calls told of, and those told here before codex-acp did, by session.
29
+ this.told = new Set(); this.early = new Set();
30
+ }
31
+ // What codex-acp says, as said to Coda: the same, but a check is said of the call it checks.
32
+ fromAgent(message) {
33
+ const u = message?.params?.update;
34
+ if (message?.method !== 'session/update' || !record(u) || !['tool_call', 'tool_call_update'].includes(u.sessionUpdate)
35
+ || typeof u.toolCallId !== 'string') return [message];
36
+ const { sessionId } = message.params, key = id => `${sessionId}\n${id}`;
37
+ const said = update => ({ ...message, params: { ...message.params, update } });
38
+ if (!u.toolCallId.startsWith(CHECK)) {
39
+ if (u.sessionUpdate !== 'tool_call') return [message];
40
+ if (this.early.delete(key(u.toolCallId))) return [said({ ...u, sessionUpdate: 'tool_call_update' })];
41
+ this.told.add(key(u.toolCallId));
42
+ return [message];
43
+ }
44
+ // The started event, then the completed one: the later is the last word.
45
+ const check = record(u.rawOutput) ? u.rawOutput : u.rawInput, target = check?.targetItemId, state = STATE[check?.review?.status];
46
+ if (typeof target !== 'string' || !target || !state) return [message];
47
+ const { rationale, riskLevel } = check.review;
48
+ const review = { state, ...(typeof rationale === 'string' && rationale.trim() ? { reason: rationale.slice(0, 4000) } : {}),
49
+ ...(RISKS.has(riskLevel) ? { risk: riskLevel } : {}) };
50
+ const meta = { toolCallId: target, _meta: { coda: { review } } };
51
+ if (this.told.has(key(target)) || this.early.has(key(target))) return [said({ sessionUpdate: 'tool_call_update', ...meta })];
52
+ this.early.add(key(target)); this.told.add(key(target));
53
+ return [said({ sessionUpdate: 'tool_call', ...meta, ...placeholder(check.action), status: 'pending' })];
54
+ }
55
+ }
@@ -0,0 +1,22 @@
1
+ // Codex's interruptible clock.sleep is a native display item, not a shell command or an AIR async task.
2
+ // Keep it in the adapter's normal session routing, including child sessions. waits.mjs turns this private metadata
3
+ // into a Coda background wait; the public ACP update stays valid while crossing codex-acp's schema.
4
+ export function codexSleepUpdate(item, state) {
5
+ return { sessionUpdate: 'session_info_update', _meta: { coda: { sleep: { id: item.id, durationMs: item.durationMs, state } } } };
6
+ }
7
+
8
+ export function reportedCodexSleeps(source) {
9
+ for (const [activity, state] of [['Started', 'waiting'], ['Completed', 'done']]) {
10
+ const before = ` return this.subagents.legacyActivity${activity}(event.item);\n case "sleep":`;
11
+ if (source.split(before).length !== 2) throw new Error('codex-acp sleep patch no longer matches the locked adapter');
12
+ source = source.replace(before, `${before}\n return codexSleepUpdate(event.item, "${state}");`);
13
+ }
14
+ return `import { codexSleepUpdate } from ${JSON.stringify(import.meta.url)};\n${source.replace(/^#![^\n]*\n/, '')}`;
15
+ }
16
+
17
+ export function sleepTitle(durationMs) {
18
+ for (const [unit, label] of [[3600000, '小时'], [60000, '分钟'], [1000, '秒']]) {
19
+ if (durationMs >= unit && durationMs % unit === 0) return `等待 ${durationMs / unit} ${label}`;
20
+ }
21
+ return `等待 ${durationMs / 1000} 秒`;
22
+ }
package/agent/waits.mjs CHANGED
@@ -4,6 +4,7 @@
4
4
  // Coda: every session it names to Coda is `X`, and each run of it a wait of its own. Coda's session instructions are checked
5
5
  // here and handed to the patched codex-acp in the request's `_meta` (instructions.mjs).
6
6
  import { INSTRUCTIONS, sessionInstructions } from './instructions.mjs';
7
+ import { sleepTitle } from './sleep.mjs';
7
8
 
8
9
  const record = value => value && typeof value === 'object' && !Array.isArray(value);
9
10
  const string = value => typeof value === 'string' && value.length > 0;
@@ -16,7 +17,7 @@ export class CodexWaits {
16
17
  // `agentInfo`: the Codex it runs, said in place of codex-acp's own name and version.
17
18
  constructor({ agentInfo = null } = {}) {
18
19
  // The root session; Coda's requests whose answers are read here, by id; the waits said, by id.
19
- this.root = null; this.asked = new Map(); this.waits = new Map(); this.agentInfo = agentInfo;
20
+ this.root = null; this.asked = new Map(); this.waits = new Map(); this.sleeps = new Map(); this.agentInfo = agentInfo;
20
21
  }
21
22
  // What Coda says: `agent`, what codex-acp is told; `coda`, what Coda is answered here.
22
23
  fromCoda(message) {
@@ -37,6 +38,7 @@ export class CodexWaits {
37
38
  }
38
39
  if (message.method === 'session/new') this.asked.set(message.id, 'session');
39
40
  else if (message.method === 'session/load' && string(message.params?.sessionId)) this.root = message.params.sessionId;
41
+ else if (message.method === 'session/prompt') this.asked.set(message.id, { prompt: message.params.sessionId });
40
42
  return { agent: [message], coda: [] };
41
43
  }
42
44
  // A command is stopped through the session that keeps it, the root; a subagent by cancelling its session.
@@ -52,12 +54,31 @@ export class CodexWaits {
52
54
  // What codex-acp says, as said to Coda.
53
55
  fromAgent(message) {
54
56
  if (!record(message)) return [];
55
- if ('id' in message && !('method' in message)) return [this.answer(message)];
57
+ if ('id' in message && !('method' in message)) {
58
+ const sessionId = this.asked.get(message.id)?.prompt;
59
+ // Cancelling or failing a turn can skip the sleep's completed item. End its waits before ending the prompt.
60
+ const finished = sessionId ? this.finishSleeps(base(sessionId), message.error ? 'failed' : message.result?.stopReason === 'cancelled' ? 'cancelled' : 'done') : [];
61
+ return [...finished, this.answer(message)];
62
+ }
56
63
  if (record(message.params) && string(message.params.sessionId)) message = { ...message, params: { ...message.params, sessionId: base(message.params.sessionId) } };
57
64
  if (message.method !== 'session/update' || !record(message.params?.update)) return [message];
58
65
  const { sessionId, update: u } = message.params;
59
66
  const said = params => [{ jsonrpc: '2.0', method: '_coda/wait', params: { sessionId, ...params } }];
60
67
  switch (u.sessionUpdate) {
68
+ case 'session_info_update': {
69
+ const sleep = u._meta?.coda?.sleep;
70
+ if (!sleep) return [message];
71
+ const waitId = `sleep:${sleep.id}`;
72
+ if (sleep.state === 'waiting') {
73
+ if (!this.sleeps.has(sessionId)) this.sleeps.set(sessionId, new Set());
74
+ this.sleeps.get(sessionId).add(waitId);
75
+ return said({ waitId, kind: 'task', state: 'waiting', title: sleepTitle(sleep.durationMs),
76
+ detail: '等待结束后继续;发送消息并选择「立即插话」可提前继续。', stoppable: false });
77
+ }
78
+ if (!this.sleeps.get(sessionId)?.delete(waitId)) return [];
79
+ if (!this.sleeps.get(sessionId).size) this.sleeps.delete(sessionId);
80
+ return said({ waitId, state: 'done' });
81
+ }
61
82
  case 'subagent_spawned': {
62
83
  if (!string(u.subagentSessionId)) return [];
63
84
  const stoppable = u.capabilities?.cancel === true;
@@ -66,7 +87,10 @@ export class CodexWaits {
66
87
  childSessionId: base(u.subagentSessionId), stoppable });
67
88
  }
68
89
  case 'subagent_state_update':
69
- return string(u.subagentSessionId) && SUBAGENT_END[u.state] ? said({ waitId: u.subagentSessionId, state: SUBAGENT_END[u.state] }) : [];
90
+ return string(u.subagentSessionId) && SUBAGENT_END[u.state] ? [
91
+ ...this.finishSleeps(base(u.subagentSessionId), SUBAGENT_END[u.state]),
92
+ ...said({ waitId: u.subagentSessionId, state: SUBAGENT_END[u.state] }),
93
+ ] : [];
70
94
  case 'async_task_spawned': {
71
95
  if (!string(u.asyncTaskId)) return [];
72
96
  const stoppable = u.canStop === true;
@@ -86,6 +110,11 @@ export class CodexWaits {
86
110
  }
87
111
  return [message];
88
112
  }
113
+ finishSleeps(sessionId, state) {
114
+ const pending = this.sleeps.get(sessionId) ?? [];
115
+ this.sleeps.delete(sessionId);
116
+ return [...pending].map(waitId => ({ jsonrpc: '2.0', method: '_coda/wait', params: { sessionId, waitId, state } }));
117
+ }
89
118
  answer(message) {
90
119
  const asked = this.asked.get(message.id);
91
120
  if (!asked) return message;
package/package.json CHANGED
@@ -2,8 +2,8 @@
2
2
  "name": "@coda-console/codex",
3
3
  "id": "codex",
4
4
  "displayName": "Codex",
5
- "description": "Codex Agent:Coda 自带的入口运行 codex-acp,报告子 Agent 与后台命令。额度与登录由账号来源插件接管;未接入账号来源时使用 Codex 自己的登录面板。",
6
- "version": "1.8.3",
5
+ "description": "Codex Agent:Coda 自带的入口运行 codex-acp,报告子 Agent、后台命令与命令的自动审核。额度与登录由账号来源插件接管;未接入账号来源时使用 Codex 自己的登录面板。",
6
+ "version": "1.8.5",
7
7
  "categories": ["Agent"],
8
8
  "dependencies": {
9
9
  "@agentclientprotocol/codex-acp": "1.12.0",