@cjhyy/code-shell-capability-coding 0.8.2 → 0.8.4

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Optional coding capability pack for `@cjhyy/code-shell-core`. It owns
4
4
  CodeShell's coding policy and implementations: the `terminal-coding` preset,
5
- coding prompt, Git/worktree behavior, LSP, ApplyPatch, NotebookEdit, Brief,
5
+ coding prompt, Git/worktree behavior, LSP, ApplyPatch, NotebookEdit,
6
6
  review/quota helpers, and external coding-agent adapters.
7
7
 
8
8
  The package is deliberately separate from core. A service building a customer
@@ -52,8 +52,10 @@ import { probeClaudeCli } from "@cjhyy/code-shell-capability-coding/orchestratio
52
52
  ```
53
53
 
54
54
  These are subpaths of the same npm package, not independently versioned
55
- packages. LSP, ApplyPatch, Brief, NotebookEdit, and other compatibility exports
56
- remain available from the root.
55
+ packages. LSP, ApplyPatch, NotebookEdit, and other compatibility exports remain
56
+ available from the root. The legacy `briefTool` formatter is also retained as a
57
+ root compatibility export, but it is no longer registered with the default
58
+ coding preset: user-facing Markdown should be returned as normal assistant text.
57
59
 
58
60
  ## Boundary
59
61
 
@@ -34,6 +34,8 @@ export declare class ClaudeEventTranslator {
34
34
  /** Accumulates `input_json_delta` per tool block so args land as one object. */
35
35
  private readonly toolInput;
36
36
  constructor(options: ClaudeEventTranslatorOptions);
37
+ /** Reset turn-scoped state while preserving the durable Claude session id. */
38
+ beginTurn(): void;
37
39
  /** Claude session id, learned from the `system/init` line. */
38
40
  runtimeSessionId?: string;
39
41
  /**
@@ -31,6 +31,11 @@ export class ClaudeEventTranslator {
31
31
  this.options = options;
32
32
  this.codeshellServer = options.codeshellServerName ?? "codeshell_tools";
33
33
  }
34
+ /** Reset turn-scoped state while preserving the durable Claude session id. */
35
+ beginTurn() {
36
+ this.terminal = false;
37
+ this.toolInput.clear();
38
+ }
34
39
  /** Claude session id, learned from the `system/init` line. */
35
40
  runtimeSessionId;
36
41
  /**
@@ -188,12 +193,32 @@ export class ClaudeEventTranslator {
188
193
  if (this.terminal)
189
194
  return [];
190
195
  this.terminal = true;
191
- return [
192
- {
193
- type: "turn_complete",
194
- reason: terminalReasonFor(str(message.subtype), message.is_error === true),
195
- },
196
- ];
196
+ const events = [];
197
+ const usage = asRecord(message.usage);
198
+ const number = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
199
+ const promptTokens = number(usage?.input_tokens);
200
+ if (promptTokens !== undefined) {
201
+ const completionTokens = number(usage?.output_tokens);
202
+ const cacheReadTokens = number(usage?.cache_read_input_tokens);
203
+ const cacheCreationTokens = number(usage?.cache_creation_input_tokens);
204
+ events.push({
205
+ type: "usage_update",
206
+ promptTokens,
207
+ promptTokensSource: "provider_usage",
208
+ promptTokensConfidence: "high",
209
+ ...(completionTokens !== undefined ? { completionTokens } : {}),
210
+ ...(cacheReadTokens !== undefined ? { cacheReadTokens } : {}),
211
+ ...(cacheCreationTokens !== undefined ? { cacheCreationTokens } : {}),
212
+ });
213
+ }
214
+ const reason = terminalReasonFor(str(message.subtype), message.is_error === true);
215
+ if (reason === "model_error") {
216
+ const detail = str(message.result) ?? str(message.error);
217
+ if (detail)
218
+ events.push({ type: "error", error: detail });
219
+ }
220
+ events.push({ type: "turn_complete", reason });
221
+ return events;
197
222
  }
198
223
  isCodeshellHostTool(toolName) {
199
224
  return toolName.startsWith(`mcp__${this.codeshellServer}__`);
@@ -1,5 +1,6 @@
1
1
  import type { StreamEvent } from "@cjhyy/code-shell-core/extension";
2
2
  import type { McpBridgeHandle } from "../shared/mcp-bridge.js";
3
+ import { type ExternalRuntimeTurnInput } from "../turn-input.js";
3
4
  export interface ClaudeRuntimeOptions {
4
5
  cwd: string;
5
6
  /** CodeShell business session id — the authorization subject. */
@@ -12,6 +13,8 @@ export interface ClaudeRuntimeOptions {
12
13
  /** Extra args, inserted before the runtime's own. */
13
14
  extraArgs?: readonly string[];
14
15
  model?: string;
16
+ resumeRuntimeSessionId?: string;
17
+ initialContext?: string;
15
18
  serverName?: string;
16
19
  log?: (event: string, data: Record<string, unknown>) => void;
17
20
  }
@@ -30,6 +33,8 @@ export declare class ClaudeCodeRuntime {
30
33
  private child?;
31
34
  private claudeSessionId?;
32
35
  private closed;
36
+ private firstTurn;
37
+ private terminalSeen;
33
38
  constructor(options: ClaudeRuntimeOptions, hooks?: ClaudeRuntimeHooks);
34
39
  /** Claude session id, once the first turn has reported it. Resume key only. */
35
40
  get runtimeSessionId(): string | undefined;
@@ -41,8 +46,9 @@ export declare class ClaudeCodeRuntime {
41
46
  * positional prompt after it is swallowed as another config value (measured),
42
47
  * and a prompt on the command line would also be visible in `ps`.
43
48
  */
44
- send(text: string): Promise<ClaudeTurnHandle>;
49
+ send(input: ExternalRuntimeTurnInput): Promise<ClaudeTurnHandle>;
45
50
  private onLine;
51
+ private emit;
46
52
  /**
47
53
  * Interrupt the active turn.
48
54
  *
@@ -19,6 +19,7 @@ import { createInterface } from "node:readline";
19
19
  import { ClaudeEventTranslator } from "./event-translator.js";
20
20
  import { claudeBridgeArgs, CLAUDE_MCP_SERVER_NAME } from "./mcp-config.js";
21
21
  import { buildRuntimeSpawnEnv } from "../shared/spawn-env.js";
22
+ import { textWithAttachmentReferences } from "../turn-input.js";
22
23
  export class ClaudeCodeRuntime {
23
24
  options;
24
25
  hooks;
@@ -28,6 +29,8 @@ export class ClaudeCodeRuntime {
28
29
  child;
29
30
  claudeSessionId;
30
31
  closed = false;
32
+ firstTurn = true;
33
+ terminalSeen = false;
31
34
  constructor(options, hooks = {}) {
32
35
  this.options = options;
33
36
  this.hooks = hooks;
@@ -36,6 +39,7 @@ export class ClaudeCodeRuntime {
36
39
  sessionId: options.businessSessionId,
37
40
  codeshellServerName: options.serverName ?? CLAUDE_MCP_SERVER_NAME,
38
41
  });
42
+ this.claudeSessionId = options.resumeRuntimeSessionId;
39
43
  }
40
44
  /** Claude session id, once the first turn has reported it. Resume key only. */
41
45
  get runtimeSessionId() {
@@ -49,11 +53,13 @@ export class ClaudeCodeRuntime {
49
53
  * positional prompt after it is swallowed as another config value (measured),
50
54
  * and a prompt on the command line would also be visible in `ps`.
51
55
  */
52
- async send(text) {
56
+ async send(input) {
53
57
  if (this.closed)
54
58
  throw new Error("ClaudeCodeRuntime is closed");
55
59
  if (this.child)
56
60
  throw new Error("a turn is already running");
61
+ this.terminalSeen = false;
62
+ this.translator.beginTurn();
57
63
  const wiring = claudeBridgeArgs({
58
64
  bridge: this.options.bridge,
59
65
  exposedToolNames: this.options.exposedToolNames,
@@ -94,19 +100,34 @@ export class ClaudeCodeRuntime {
94
100
  if (trimmed)
95
101
  this.log("claude.stderr", { bytes: trimmed.length });
96
102
  });
103
+ let text = textWithAttachmentReferences(input);
104
+ if (this.firstTurn && !this.options.resumeRuntimeSessionId && this.options.initialContext) {
105
+ text = `${this.options.initialContext}\n\n<current_user_request>\n${text}\n</current_user_request>`;
106
+ }
107
+ this.firstTurn = false;
97
108
  child.stdin.end(text);
98
109
  const done = new Promise((resolve) => {
99
- const finish = () => {
110
+ let finished = false;
111
+ const finish = (code, error) => {
112
+ if (finished)
113
+ return;
114
+ finished = true;
100
115
  lines.close();
101
116
  // Clean up the config file that carried the bearer token.
102
117
  wiring.cleanup();
103
118
  this.child = undefined;
119
+ if (!this.terminalSeen) {
120
+ const detail = error?.message ??
121
+ `Claude Code exited without a terminal result (code ${code ?? "unknown"})`;
122
+ this.emit({ type: "error", error: detail });
123
+ this.emit({ type: "turn_complete", reason: "model_error" });
124
+ }
104
125
  resolve();
105
126
  };
106
- child.once("exit", finish);
127
+ child.once("exit", (code) => finish(code));
107
128
  child.once("error", (error) => {
108
129
  this.log("claude.spawn_failed", { error: error.message.slice(0, 200) });
109
- finish();
130
+ finish(undefined, error);
110
131
  });
111
132
  });
112
133
  return { done };
@@ -124,14 +145,9 @@ export class ClaudeCodeRuntime {
124
145
  return;
125
146
  }
126
147
  for (const event of this.translator.translate(parsed)) {
127
- try {
128
- this.hooks.onEvent?.(event);
129
- }
130
- catch (error) {
131
- this.log("claude.event_handler_failed", {
132
- error: error instanceof Error ? error.name : "unknown",
133
- });
134
- }
148
+ if (event.type === "turn_complete")
149
+ this.terminalSeen = true;
150
+ this.emit(event);
135
151
  }
136
152
  if (!this.claudeSessionId && this.translator.runtimeSessionId) {
137
153
  this.claudeSessionId = this.translator.runtimeSessionId;
@@ -141,6 +157,16 @@ export class ClaudeCodeRuntime {
141
157
  });
142
158
  }
143
159
  }
160
+ emit(event) {
161
+ try {
162
+ this.hooks.onEvent?.(event);
163
+ }
164
+ catch (error) {
165
+ this.log("claude.event_handler_failed", {
166
+ error: error instanceof Error ? error.name : "unknown",
167
+ });
168
+ }
169
+ }
144
170
  /**
145
171
  * Interrupt the active turn.
146
172
  *
@@ -56,6 +56,8 @@ export declare class CodexEventTranslator {
56
56
  private onTurnCompleted;
57
57
  private onError;
58
58
  private onAgentDelta;
59
+ private onReasoningDelta;
60
+ private onTokenUsage;
59
61
  /**
60
62
  * A CodeShell Host Tool call must not produce a card here — `ToolExecutor`
61
63
  * already emits one, and two unsynchronised sources for one operation is
@@ -2,6 +2,18 @@
2
2
  const CODESHELL_MCP_SERVER = "codeshell_tools";
3
3
  /** Cap on remembered finished turns — a session is long-lived, the set is not. */
4
4
  const MAX_TOMBSTONES = 256;
5
+ /** Thread items that represent observable runtime work rather than prose/state. */
6
+ const TOOL_ITEM_TYPES = new Set([
7
+ "commandExecution",
8
+ "fileChange",
9
+ "mcpToolCall",
10
+ "dynamicToolCall",
11
+ "collabAgentToolCall",
12
+ "webSearch",
13
+ "imageView",
14
+ "sleep",
15
+ "imageGeneration",
16
+ ]);
5
17
  function asRecord(value) {
6
18
  return value && typeof value === "object" && !Array.isArray(value)
7
19
  ? value
@@ -69,13 +81,17 @@ export class CodexEventTranslator {
69
81
  return this.onError(params);
70
82
  case "item/agentMessage/delta":
71
83
  return this.onAgentDelta(params);
84
+ case "item/reasoning/summaryTextDelta":
85
+ case "item/reasoning/textDelta":
86
+ return this.onReasoningDelta(params);
87
+ case "thread/tokenUsage/updated":
88
+ return this.onTokenUsage(params);
72
89
  case "item/started":
73
90
  return this.onItemStarted(params);
74
91
  case "item/completed":
75
92
  return this.onItemCompleted(params);
76
93
  default:
77
- // Everything else (token usage, rate limits, plan updates, MCP status…)
78
- // is either handled elsewhere or deliberately not surfaced.
94
+ // Rate limits, plan updates and MCP status are not chat events.
79
95
  return [];
80
96
  }
81
97
  }
@@ -118,7 +134,18 @@ export class CodexEventTranslator {
118
134
  if (turnId)
119
135
  this.remember(turnId);
120
136
  this.activeTurnId = undefined;
121
- return [{ type: "turn_complete", reason: terminalReasonFor(str(turn?.status)) }];
137
+ const reason = terminalReasonFor(str(turn?.status));
138
+ if (reason === "model_error") {
139
+ const error = asRecord(turn?.error);
140
+ const detail = str(error?.message) ?? str(error?.additionalDetails);
141
+ if (detail) {
142
+ return [
143
+ { type: "error", error: detail },
144
+ { type: "turn_complete", reason },
145
+ ];
146
+ }
147
+ }
148
+ return [{ type: "turn_complete", reason }];
122
149
  }
123
150
  onError(params) {
124
151
  // A retryable error is not terminal. Reporting completion here would close
@@ -132,7 +159,12 @@ export class CodexEventTranslator {
132
159
  if (turnId)
133
160
  this.remember(turnId);
134
161
  this.activeTurnId = undefined;
135
- return [{ type: "turn_complete", reason: "model_error" }];
162
+ const error = asRecord(params.error);
163
+ const message = str(error?.message) ?? str(error?.additionalDetails) ?? "Codex turn failed";
164
+ return [
165
+ { type: "error", error: message },
166
+ { type: "turn_complete", reason: "model_error" },
167
+ ];
136
168
  }
137
169
  onAgentDelta(params) {
138
170
  if (this.isStale(str(params.turnId)))
@@ -140,6 +172,50 @@ export class CodexEventTranslator {
140
172
  const delta = str(params.delta);
141
173
  return delta ? [{ type: "text_delta", text: delta }] : [];
142
174
  }
175
+ onReasoningDelta(params) {
176
+ if (this.isStale(str(params.turnId)))
177
+ return [];
178
+ const delta = str(params.delta);
179
+ return delta ? [{ type: "thinking_delta", text: delta }] : [];
180
+ }
181
+ onTokenUsage(params) {
182
+ if (this.isStale(str(params.turnId)))
183
+ return [];
184
+ const usage = asRecord(params.tokenUsage);
185
+ const total = asRecord(usage?.total);
186
+ const last = asRecord(usage?.last);
187
+ const number = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
188
+ const promptTokens = number(last?.inputTokens);
189
+ if (promptTokens === undefined)
190
+ return [];
191
+ const cacheReadTokens = number(last?.cachedInputTokens);
192
+ const cumulativePromptTokens = number(total?.inputTokens);
193
+ const cumulativeCacheReadTokens = number(total?.cachedInputTokens);
194
+ const cacheCreationTokens = number(last?.cacheWriteInputTokens);
195
+ const cumulativeCacheCreationTokens = number(total?.cacheWriteInputTokens);
196
+ const completionTokens = number(last?.outputTokens);
197
+ const cumulativeCompletionTokens = number(total?.outputTokens);
198
+ return [
199
+ {
200
+ type: "usage_update",
201
+ promptTokens,
202
+ promptTokensSource: "provider_usage",
203
+ promptTokensConfidence: "high",
204
+ ...(cacheReadTokens !== undefined ? { cacheReadTokens } : {}),
205
+ ...(cacheCreationTokens !== undefined ? { cacheCreationTokens } : {}),
206
+ singleTurnPromptTokens: promptTokens,
207
+ ...(cacheReadTokens !== undefined ? { singleTurnCacheReadTokens: cacheReadTokens } : {}),
208
+ ...(cacheCreationTokens !== undefined
209
+ ? { singleTurnCacheCreationTokens: cacheCreationTokens }
210
+ : {}),
211
+ ...(cumulativePromptTokens !== undefined ? { cumulativePromptTokens } : {}),
212
+ ...(cumulativeCacheReadTokens !== undefined ? { cumulativeCacheReadTokens } : {}),
213
+ ...(cumulativeCacheCreationTokens !== undefined ? { cumulativeCacheCreationTokens } : {}),
214
+ ...(completionTokens !== undefined ? { completionTokens } : {}),
215
+ ...(cumulativeCompletionTokens !== undefined ? { cumulativeCompletionTokens } : {}),
216
+ },
217
+ ];
218
+ }
143
219
  /**
144
220
  * A CodeShell Host Tool call must not produce a card here — `ToolExecutor`
145
221
  * already emits one, and two unsynchronised sources for one operation is
@@ -161,6 +237,8 @@ export class CodexEventTranslator {
161
237
  const type = str(item?.type);
162
238
  if (!item || !id || !type)
163
239
  return [];
240
+ if (!TOOL_ITEM_TYPES.has(type))
241
+ return [];
164
242
  if (this.isCodeshellHostTool(item))
165
243
  return [];
166
244
  const { id: _id, type: _type, ...args } = item;
@@ -174,13 +252,33 @@ export class CodexEventTranslator {
174
252
  const type = str(item?.type);
175
253
  if (!item || !id || !type)
176
254
  return [];
255
+ if (!TOOL_ITEM_TYPES.has(type))
256
+ return [];
177
257
  if (this.isCodeshellHostTool(item))
178
258
  return [];
179
- const output = str(item.aggregatedOutput) ?? str(item.output) ?? str(item.text) ?? str(item.result);
259
+ const rawOutput = item.aggregatedOutput ?? item.output ?? item.text ?? item.result ?? item.changes;
260
+ let output;
261
+ if (typeof rawOutput === "string")
262
+ output = rawOutput;
263
+ else if (rawOutput !== undefined && rawOutput !== null) {
264
+ try {
265
+ output = JSON.stringify(rawOutput);
266
+ }
267
+ catch {
268
+ output = String(rawOutput);
269
+ }
270
+ }
271
+ const error = asRecord(item.error);
272
+ const errorMessage = str(error?.message) ?? str(item.error);
180
273
  return [
181
274
  {
182
275
  type: "tool_result",
183
- result: { id, toolName: type, ...(output !== undefined ? { result: output } : {}) },
276
+ result: {
277
+ id,
278
+ toolName: type,
279
+ ...(output !== undefined ? { result: output } : {}),
280
+ ...(errorMessage ? { error: errorMessage, isError: true } : {}),
281
+ },
184
282
  },
185
283
  ];
186
284
  }
@@ -18,6 +18,7 @@
18
18
  import type { StreamEvent } from "@cjhyy/code-shell-core/extension";
19
19
  import { type AppServerClientOptions } from "./app-server-client.js";
20
20
  import { type McpBridgeHandle } from "../shared/mcp-bridge.js";
21
+ import { type ExternalRuntimeTurnInput } from "../turn-input.js";
21
22
  export interface CodexRuntimeOptions {
22
23
  cwd: string;
23
24
  /** CodeShell business session id — the authorization subject, never the thread id. */
@@ -31,6 +32,9 @@ export interface CodexRuntimeOptions {
31
32
  */
32
33
  bridgeServerName?: string;
33
34
  model?: string;
35
+ resumeRuntimeSessionId?: string;
36
+ initialContext?: string;
37
+ developerInstructions?: string;
34
38
  /** Codex sandbox mode. Kebab-case per protocol (`workspace-write`, …). */
35
39
  sandbox?: string;
36
40
  /** Codex approval policy. Also kebab-case. */
@@ -59,6 +63,11 @@ export interface CodexRuntimeHooks {
59
63
  method: string;
60
64
  params: unknown;
61
65
  }) => Promise<NativeApprovalDecision> | NativeApprovalDecision;
66
+ /** Answer Codex's request_user_input tool through the owning CodeShell UI. */
67
+ onUserInput?: (request: {
68
+ method: string;
69
+ params: unknown;
70
+ }) => Promise<unknown> | unknown;
62
71
  }
63
72
  export declare class CodexRuntime {
64
73
  private readonly options;
@@ -69,6 +78,8 @@ export declare class CodexRuntime {
69
78
  private translator?;
70
79
  private threadId?;
71
80
  private started;
81
+ private resumed;
82
+ private firstTurn;
72
83
  private activeTurn?;
73
84
  constructor(options: CodexRuntimeOptions, hooks?: CodexRuntimeHooks);
74
85
  /** Codex thread id, once the thread exists. Protocol routing only — never the
@@ -90,7 +101,7 @@ export declare class CodexRuntime {
90
101
  * tombstones are what keep a late `turn/started` from reactivating a session
91
102
  * that already reported terminal.
92
103
  */
93
- send(text: string): Promise<CodexTurnHandle>;
104
+ send(input: ExternalRuntimeTurnInput): Promise<CodexTurnHandle>;
94
105
  /**
95
106
  * Interrupt the active turn.
96
107
  *
@@ -2,6 +2,7 @@ import { CodexAppServerClient } from "./app-server-client.js";
2
2
  import { CodexEventTranslator } from "./event-translator.js";
3
3
  import { buildRuntimeSpawnEnv } from "../shared/spawn-env.js";
4
4
  import { codexBridgeConfigArgs } from "../shared/mcp-bridge.js";
5
+ import { textWithAttachmentReferences } from "../turn-input.js";
5
6
  /** A thread/start or turn/start that hangs is worse than one that fails. */
6
7
  const CRITICAL_RPC_TIMEOUT_MS = 60_000;
7
8
  /** Interrupt is a fail-safe; an unbounded wait silently defeats it. */
@@ -15,6 +16,8 @@ export class CodexRuntime {
15
16
  translator;
16
17
  threadId;
17
18
  started = false;
19
+ resumed = false;
20
+ firstTurn = true;
18
21
  activeTurn;
19
22
  constructor(options, hooks = {}) {
20
23
  this.options = options;
@@ -57,15 +60,41 @@ export class CodexRuntime {
57
60
  clientInfo: { name: "codeshell", title: "CodeShell", version: "1" },
58
61
  capabilities: { experimentalApi: true },
59
62
  }, CRITICAL_RPC_TIMEOUT_MS);
60
- const thread = (await this.client.request("thread/start", {
63
+ let thread;
64
+ if (this.options.resumeRuntimeSessionId) {
65
+ try {
66
+ thread = (await this.client.request("thread/resume", {
67
+ threadId: this.options.resumeRuntimeSessionId,
68
+ cwd: this.options.cwd,
69
+ ...(this.options.model ? { model: this.options.model } : {}),
70
+ ...(this.options.sandbox ? { sandbox: this.options.sandbox } : {}),
71
+ ...(this.options.approvalPolicy ? { approvalPolicy: this.options.approvalPolicy } : {}),
72
+ ...(this.options.developerInstructions
73
+ ? { developerInstructions: this.options.developerInstructions }
74
+ : {}),
75
+ }, CRITICAL_RPC_TIMEOUT_MS));
76
+ this.resumed = true;
77
+ }
78
+ catch (error) {
79
+ this.log("runtime.thread_resume_failed", {
80
+ businessSessionId: this.options.businessSessionId,
81
+ error: error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200),
82
+ });
83
+ }
84
+ }
85
+ thread ??= (await this.client.request("thread/start", {
61
86
  cwd: this.options.cwd,
62
87
  ...(this.options.model ? { model: this.options.model } : {}),
63
88
  ...(this.options.sandbox ? { sandbox: this.options.sandbox } : {}),
64
89
  ...(this.options.approvalPolicy ? { approvalPolicy: this.options.approvalPolicy } : {}),
90
+ ...(this.options.developerInstructions
91
+ ? { developerInstructions: this.options.developerInstructions }
92
+ : {}),
65
93
  }, CRITICAL_RPC_TIMEOUT_MS));
66
94
  const threadId = thread?.thread?.id;
67
- if (!threadId)
68
- throw new Error("thread/start returned no thread id");
95
+ if (!threadId) {
96
+ throw new Error(`${this.resumed ? "thread/resume" : "thread/start"} returned no thread id`);
97
+ }
69
98
  this.threadId = threadId;
70
99
  this.translator = new CodexEventTranslator({
71
100
  threadId,
@@ -74,6 +103,7 @@ export class CodexRuntime {
74
103
  this.log("runtime.thread_started", {
75
104
  businessSessionId: this.options.businessSessionId,
76
105
  threadIdPrefix: threadId.slice(0, 8),
106
+ resumed: this.resumed,
77
107
  });
78
108
  }
79
109
  /**
@@ -84,16 +114,31 @@ export class CodexRuntime {
84
114
  * tombstones are what keep a late `turn/started` from reactivating a session
85
115
  * that already reported terminal.
86
116
  */
87
- async send(text) {
117
+ async send(input) {
88
118
  if (!this.threadId)
89
119
  throw new Error("CodexRuntime.send() before start()");
90
120
  let resolveDone;
91
121
  const done = new Promise((resolve) => (resolveDone = resolve));
92
122
  this.activeTurn = { resolve: resolveDone };
123
+ let text = textWithAttachmentReferences(input);
124
+ if (this.firstTurn && !this.resumed && this.options.initialContext) {
125
+ text = `${this.options.initialContext}\n\n<current_user_request>\n${text}\n</current_user_request>`;
126
+ }
127
+ this.firstTurn = false;
128
+ const imageInputs = (input.attachments ?? [])
129
+ .filter((attachment) => attachment.kind === "image" || attachment.mime?.toLowerCase().startsWith("image/"))
130
+ .map((attachment) => ({
131
+ type: "localImage",
132
+ path: attachment.path,
133
+ ...(attachment.detail === "low" || attachment.detail === "high"
134
+ ? { detail: attachment.detail }
135
+ : {}),
136
+ }));
93
137
  const response = (await this.client.request("turn/start", {
94
138
  threadId: this.threadId,
139
+ ...(input.clientMessageId ? { clientUserMessageId: input.clientMessageId } : {}),
95
140
  // `text_elements` is required by the protocol even when empty.
96
- input: [{ type: "text", text, text_elements: [] }],
141
+ input: [{ type: "text", text, text_elements: [] }, ...imageInputs],
97
142
  }, CRITICAL_RPC_TIMEOUT_MS));
98
143
  const turnId = response?.turn?.id;
99
144
  if (this.activeTurn)
@@ -171,6 +216,11 @@ export class CodexRuntime {
171
216
  if (method === "mcpServer/elicitation/request") {
172
217
  return this.answerMcpElicitation(params);
173
218
  }
219
+ if (method === "item/tool/requestUserInput") {
220
+ return this.hooks.onUserInput
221
+ ? await this.hooks.onUserInput({ method, params })
222
+ : { answers: {} };
223
+ }
174
224
  if (!method.includes("requestApproval")) {
175
225
  // Some other server request (user input, …). Leave unhandled so the client
176
226
  // answers method-not-found rather than inventing consent.
@@ -20,6 +20,8 @@ export { SessionContextStore } from "./shared/session-context-store.js";
20
20
  export type { ResolveRequest, SessionContextResult, SessionContextMissReason, ToolHostRef, } from "./shared/session-context-store.js";
21
21
  export { buildRuntimeSpawnEnv } from "./shared/spawn-env.js";
22
22
  export type { RuntimeSpawnEnvOptions } from "./shared/spawn-env.js";
23
+ export { textWithAttachmentReferences } from "./turn-input.js";
24
+ export type { ExternalRuntimeAttachment, ExternalRuntimeTurnInput } from "./turn-input.js";
23
25
  export { CodexEventTranslator } from "./codex/event-translator.js";
24
26
  export { CodexAppServerClient } from "./codex/app-server-client.js";
25
27
  export type { AppServerClientOptions } from "./codex/app-server-client.js";
@@ -17,6 +17,7 @@
17
17
  export { CODEX_MCP_TOKEN_ENV_VAR, codexBridgeConfigArgs, startLoopbackMcpBridge, threadIdFromMeta, } from "./shared/mcp-bridge.js";
18
18
  export { SessionContextStore } from "./shared/session-context-store.js";
19
19
  export { buildRuntimeSpawnEnv } from "./shared/spawn-env.js";
20
+ export { textWithAttachmentReferences } from "./turn-input.js";
20
21
  export { CodexEventTranslator } from "./codex/event-translator.js";
21
22
  export { CodexAppServerClient } from "./codex/app-server-client.js";
22
23
  export { CodexRuntime } from "./codex/runtime.js";
@@ -23,6 +23,7 @@ import type { PermissionRule, ToolDefinition } from "@cjhyy/code-shell-core/exte
23
23
  import { createSessionToolHost, type ExternalToolExposurePolicy, type ToolVisibilityInputs } from "@cjhyy/code-shell-core/extension";
24
24
  import { type CodexRuntimeHooks, type CodexRuntimeOptions } from "./codex/runtime.js";
25
25
  import { type ClaudeRuntimeHooks } from "./claude-code/runtime.js";
26
+ import type { ExternalRuntimeTurnInput } from "./turn-input.js";
26
27
  export type ExternalRuntimeKind = "codex" | "claude-code";
27
28
  /**
28
29
  * Everything the host must decide. Deliberately no defaults for the
@@ -52,6 +53,12 @@ export interface ExternalRuntimeSessionOptions {
52
53
  contextOverrides?: Parameters<typeof createSessionToolHost>[0]["contextOverrides"];
53
54
  settingsScope?: Parameters<typeof createSessionToolHost>[0]["settingsScope"];
54
55
  model?: string;
56
+ /** Runtime thread/session id recovered from durable Desktop state. */
57
+ resumeRuntimeSessionId?: string;
58
+ /** Context replayed only when resume is unavailable and a fresh thread starts. */
59
+ initialContext?: string;
60
+ /** Stable host guidance applied to new and resumed runtime threads. */
61
+ developerInstructions?: string;
55
62
  /** Codex only. Kebab-case per protocol. */
56
63
  sandbox?: string;
57
64
  /** Codex only. Kebab-case per protocol. */
@@ -76,7 +83,7 @@ export interface ExternalRuntimeSession {
76
83
  readonly runtimeSessionId: string | undefined;
77
84
  /** Tools actually exposed, after the allowlist and visibility guards. */
78
85
  listTools(): readonly ToolDefinition[];
79
- send(text: string): Promise<{
86
+ send(input: ExternalRuntimeTurnInput): Promise<{
80
87
  done: Promise<void>;
81
88
  }>;
82
89
  interrupt(): Promise<void>;
@@ -66,6 +66,13 @@ export async function startExternalRuntimeSession(options) {
66
66
  businessSessionId: options.businessSessionId,
67
67
  bridge,
68
68
  ...(options.model ? { model: options.model } : {}),
69
+ ...(options.resumeRuntimeSessionId
70
+ ? { resumeRuntimeSessionId: options.resumeRuntimeSessionId }
71
+ : {}),
72
+ ...(options.initialContext ? { initialContext: options.initialContext } : {}),
73
+ ...(options.developerInstructions
74
+ ? { developerInstructions: options.developerInstructions }
75
+ : {}),
69
76
  ...(options.sandbox ? { sandbox: options.sandbox } : {}),
70
77
  ...(options.approvalPolicy ? { approvalPolicy: options.approvalPolicy } : {}),
71
78
  ...(options.codexClient ? { client: options.codexClient } : {}),
@@ -81,6 +88,10 @@ export async function startExternalRuntimeSession(options) {
81
88
  bridge,
82
89
  exposedToolNames,
83
90
  ...(options.model ? { model: options.model } : {}),
91
+ ...(options.resumeRuntimeSessionId
92
+ ? { resumeRuntimeSessionId: options.resumeRuntimeSessionId }
93
+ : {}),
94
+ ...(options.initialContext ? { initialContext: options.initialContext } : {}),
84
95
  ...(options.claudeExtraArgs ? { extraArgs: options.claudeExtraArgs } : {}),
85
96
  ...(options.claudeCommand ? { command: options.claudeCommand } : {}),
86
97
  log,
@@ -103,7 +114,7 @@ export async function startExternalRuntimeSession(options) {
103
114
  return activeRuntime.runtimeSessionId;
104
115
  },
105
116
  listTools: () => activeHost.listTools(),
106
- send: (text) => activeRuntime.send(text),
117
+ send: (input) => activeRuntime.send(input),
107
118
  interrupt: () => activeRuntime.interrupt(),
108
119
  close,
109
120
  };
@@ -0,0 +1,13 @@
1
+ /** Transport-neutral user input for Codex / Claude Code runtimes. */
2
+ export interface ExternalRuntimeAttachment {
3
+ path: string;
4
+ kind?: "image" | "file" | "directory";
5
+ mime?: string;
6
+ detail?: "low" | "standard" | "high";
7
+ }
8
+ export interface ExternalRuntimeTurnInput {
9
+ text: string;
10
+ clientMessageId?: string;
11
+ attachments?: readonly ExternalRuntimeAttachment[];
12
+ }
13
+ export declare function textWithAttachmentReferences(input: ExternalRuntimeTurnInput): string;
@@ -0,0 +1,15 @@
1
+ export function textWithAttachmentReferences(input) {
2
+ const references = (input.attachments ?? [])
3
+ .filter((attachment) => attachment.path.trim())
4
+ .map((attachment) => `- ${attachment.kind ?? "file"}: ${attachment.path}`);
5
+ if (references.length === 0)
6
+ return input.text;
7
+ return [
8
+ input.text,
9
+ "",
10
+ "<codeshell_attachments>",
11
+ "The user explicitly attached these local paths. Inspect them when relevant:",
12
+ ...references,
13
+ "</codeshell_attachments>",
14
+ ].join("\n");
15
+ }
@@ -1,7 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { BUILTIN_AGENT_PRESETS, BUILTIN_TOOLS, derivePresetExposure, } from "@cjhyy/code-shell-core/extension";
4
- import { briefTool, briefToolDef } from "./tools/brief.js";
5
4
  import { lspTool, lspToolDef } from "./tools/lsp.js";
6
5
  import { notebookEditTool, notebookEditToolDef } from "./tools/notebook-edit.js";
7
6
  import { applyPatchTool, applyPatchToolDef } from "./tools/apply-patch/index.js";
@@ -16,6 +15,9 @@ function defineTool(definition, execute, exposure) {
16
15
  return { definition, execute, exposure };
17
16
  }
18
17
  export const CODING_TOOLS = [
18
+ // `briefTool` remains a root compatibility export, but is deliberately not
19
+ // model-exposed here: its Markdown return value is a tool result, not a
20
+ // user-facing assistant message (especially important for headless runs).
19
21
  defineTool({
20
22
  ...driveAgentToolDef,
21
23
  source: "builtin",
@@ -92,16 +94,6 @@ export const CODING_TOOLS = [
92
94
  },
93
95
  },
94
96
  }, applyPatchTool, { presetTags: ["terminal-coding"] }),
95
- defineTool({
96
- ...briefToolDef,
97
- source: "builtin",
98
- permissionDefault: "allow",
99
- isReadOnly: true,
100
- isConcurrencySafe: true,
101
- }, briefTool, {
102
- presetTags: ["terminal-coding"],
103
- defaultPermissionRules: [{ tool: "Brief", decision: "allow" }],
104
- }),
105
97
  defineTool({
106
98
  ...notebookEditToolDef,
107
99
  source: "builtin",
@@ -1,6 +1,12 @@
1
1
  /**
2
- * BriefTool send structured messages with markdown support.
2
+ * Legacy Brief formatter.
3
+ *
4
+ * Kept as a root compatibility export for existing programmatic consumers. It
5
+ * is intentionally not registered in the default coding capability because a
6
+ * tool result is not a user-facing assistant message.
3
7
  */
4
8
  import type { ToolDefinition } from "@cjhyy/code-shell-core/extension";
9
+ /** @deprecated Return user-facing Markdown as normal assistant text instead. */
5
10
  export declare const briefToolDef: ToolDefinition;
11
+ /** @deprecated Return user-facing Markdown as normal assistant text instead. */
6
12
  export declare function briefTool(args: Record<string, unknown>): Promise<string>;
@@ -1,6 +1,11 @@
1
1
  /**
2
- * BriefTool send structured messages with markdown support.
2
+ * Legacy Brief formatter.
3
+ *
4
+ * Kept as a root compatibility export for existing programmatic consumers. It
5
+ * is intentionally not registered in the default coding capability because a
6
+ * tool result is not a user-facing assistant message.
3
7
  */
8
+ /** @deprecated Return user-facing Markdown as normal assistant text instead. */
4
9
  export const briefToolDef = {
5
10
  name: "Brief",
6
11
  description: "Send a structured brief/summary message. Useful for providing concise status updates, " +
@@ -25,6 +30,7 @@ export const briefToolDef = {
25
30
  required: ["content"],
26
31
  },
27
32
  };
33
+ /** @deprecated Return user-facing Markdown as normal assistant text instead. */
28
34
  export async function briefTool(args) {
29
35
  const title = args.title;
30
36
  const content = args.content;
@@ -107,6 +107,9 @@ export const driveAgentToolDef = {
107
107
  required: ["prompt"],
108
108
  },
109
109
  };
110
+ function isExternalRuntimeContext(ctx) {
111
+ return (ctx?.externalRuntime === true);
112
+ }
110
113
  const defaultRunner = (opts) => {
111
114
  const { adapter, command } = CLI_ADAPTERS[opts.cli];
112
115
  return runAgentOnce(adapter, {
@@ -601,7 +604,7 @@ function trackBackgroundRun(params) {
601
604
  return { jobId, ...(warning ? { warning } : {}) };
602
605
  }
603
606
  async function waitForForegroundOrHandoff(run, handoffMs) {
604
- if (handoffMs < 0) {
607
+ if (!Number.isFinite(handoffMs) || handoffMs < 0) {
605
608
  return { kind: "completed", result: await run };
606
609
  }
607
610
  let timer;
@@ -651,9 +654,14 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
651
654
  args.permissionMode === "acceptEdits" ||
652
655
  args.permissionMode === "bypassPermissions"
653
656
  ? args.permissionMode
654
- : "bypassPermissions";
657
+ : isExternalRuntimeContext(ctx)
658
+ ? "default"
659
+ : "bypassPermissions";
655
660
  const isWritableRun = permissionMode !== "default";
656
- const background = args.background !== false;
661
+ // External runtimes do not participate in the native Engine's wake-up loop.
662
+ // A detached result would be queued for an Engine that is not driving this
663
+ // turn, so keep delegation attached to the parent runtime.
664
+ const background = isExternalRuntimeContext(ctx) ? false : args.background !== false;
657
665
  const cliName = cli === "codex" ? "Codex" : "Claude Code";
658
666
  if (background && !isValidSessionId(ctx?.sessionId)) {
659
667
  return `Error: cannot start a background ${cliName} job without a session — its result notification would be dropped. Retry with background:false, or ensure the tool runs inside a session.`;
@@ -801,7 +809,9 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
801
809
  permissionMode,
802
810
  imagePaths,
803
811
  };
804
- const foregroundHandoffMs = options.foregroundHandoffMs ?? DRIVE_AGENT_FOREGROUND_HANDOFF_MS;
812
+ const foregroundHandoffMs = isExternalRuntimeContext(ctx)
813
+ ? Number.POSITIVE_INFINITY
814
+ : (options.foregroundHandoffMs ?? DRIVE_AGENT_FOREGROUND_HANDOFF_MS);
805
815
  if (background) {
806
816
  // Fail loud on a missing sessionId: a background job whose completion
807
817
  // notification can't be routed (enqueue drops invalid/empty sessionId)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjhyy/code-shell-capability-coding",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Coding capability pack for the generic code-shell agent core.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -39,7 +39,7 @@
39
39
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
40
40
  },
41
41
  "dependencies": {
42
- "@cjhyy/code-shell-core": "0.8.2"
42
+ "@cjhyy/code-shell-core": "0.8.4"
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=20.10"