@openclaw/acpx 2026.9.5 → 2026.9.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/README.md CHANGED
@@ -19,6 +19,57 @@ Restart the Gateway after installing or updating the plugin.
19
19
  - MCP bridge helpers for OpenClaw tools and plugin tools.
20
20
  - Static runtime assets used by the ACP process bridge.
21
21
 
22
+ ## Native agents in the model picker
23
+
24
+ Install GitHub Copilot CLI, Kilo Code, OpenCode, Pi ACP, or Qwen Code and complete its login on the Gateway host,
25
+ then refresh the model catalog. Choose one of its models to use that agent in ordinary chat.
26
+ The agent owns its credentials; OpenClaw keeps the conversation transcript and asks for approval
27
+ when the agent requests permission. Pi does not request tool approval unless an extension adds it.
28
+ The same selection works in the web app and channels.
29
+
30
+ The runtime and provider IDs are `acp-copilot`, `acp-kilocode`, `acp-opencode`, `acp-pi`, and `acp-qwen`.
31
+ Each model keeps its native ID, including any slashes. Configured ACP agent commands take
32
+ precedence over installed defaults. Catalog refresh uses installed commands and does not install
33
+ missing agents. Explicit ACP commands and bindings keep their existing agent names.
34
+
35
+ Models settings lists detected agents on the Gateway machine. Turn each native agent on or off
36
+ there, or set `plugins.entries.acpx.config.nativeAgents.<id>` to `false` (`copilot`, `kilocode`,
37
+ `opencode`, `pi`, or `qwen`). Missing flags are enabled. Disabling an agent prevents new native turns
38
+ and catalog discovery without interrupting a running turn or deleting history. Classic ACP
39
+ commands and `acp.allowedAgents` keep their existing behavior. Detection checks installed
40
+ executables; it does not prove that an agent is logged in or can serve a model.
41
+
42
+ For GitHub Copilot CLI, run `copilot login` under the Gateway's OS account before refreshing
43
+ the catalog. Copilot owns GitHub authentication, model access, and plan usage. Its explicitly
44
+ configured BYOK providers remain CLI-owned and can incur separate API charges; OpenClaw does
45
+ not select a BYOK route for it. See the
46
+ [Copilot setup and billing notes](https://docs.openclaw.ai/tools/acp-agents-setup#github-copilot-cli-in-native-chat).
47
+
48
+ Native picker runtimes run on the Gateway host and use the native app's permissions.
49
+ OpenClaw checks that execution choice before dispatching a chat turn; ACP runners do not
50
+ implement OpenClaw sandboxing or workspace-only filesystem confinement.
51
+
52
+ When optional chat restrictions cannot be enforced, an administrator can choose
53
+ **Continue for this chat** to use the native app's permissions. This grants Full Access
54
+ and turns off optional sandboxing for that chat only; agent-wide and global settings
55
+ stay unchanged. After a refused message, confirmation retries that message once.
56
+ Confirming a model selection without a pending message does not send anything.
57
+
58
+ A creator-role-required sandbox cannot be removed, and remote execution placement
59
+ is not supported. Choose a compatible runtime when those boundaries must remain.
60
+ OpenClaw's Read Only, Guarded, and Workspace permission modes are not supported
61
+ by these native runtimes.
62
+
63
+ Native tool permission requests still require their one-shot approval. Once approved,
64
+ delegated filesystem writes do not encounter a second ACPX terminal approval gate.
65
+ Classic ACP sessions keep their configured `permissionMode`. The ACP client's
66
+ delegated filesystem remains rooted at its session cwd; this is not a sandbox for
67
+ the native process's own filesystem access.
68
+
69
+ Catalog refresh closes its local connection. The native agent owns any history it creates.
70
+ Reset and deletion close the local session and prevent its reuse, including after a Gateway restart.
71
+ Native history stays with the agent; these operations do not delete it.
72
+
22
73
  ## Configure
23
74
 
24
75
  Use the ACP docs for harness-specific setup, permission modes, and model/runtime selection:
@@ -0,0 +1,138 @@
1
+ //#region extensions/acpx/src/codex-adapter.ts
2
+ const CODEX_ACP_PACKAGE = "@agentclientprotocol/codex-acp";
3
+ const CODEX_ACP_BIN = "codex-acp";
4
+ const LEGACY_CODEX_ACP_PACKAGE = "@zed-industries/codex-acp";
5
+ const OPENCLAW_CODEX_CONFIG_ARG = "--openclaw-codex-config";
6
+ //#endregion
7
+ //#region extensions/acpx/src/command-line.ts
8
+ /** Match ACPX's persisted argv identity; scalar records keep their original bytes. */
9
+ function renderAgentCommand(command) {
10
+ return typeof command === "string" ? command : command.map((part) => /^[A-Za-z0-9_@%+=:,./^~-]+$/.test(part) ? part : JSON.stringify(part)).join(" ");
11
+ }
12
+ /** Split a command string into argv-like parts using simple quote/backslash rules. */
13
+ function splitCommandParts(value) {
14
+ if (Array.isArray(value)) return value;
15
+ const windows = process.platform === "win32";
16
+ const parts = [];
17
+ let current = "";
18
+ let quote = null;
19
+ let escaping = false;
20
+ let hasPart = false;
21
+ for (const ch of value) {
22
+ if (escaping) {
23
+ current += ch;
24
+ escaping = false;
25
+ hasPart = true;
26
+ continue;
27
+ }
28
+ if (ch === "\\" && quote !== "'" && !windows) {
29
+ escaping = true;
30
+ hasPart = true;
31
+ continue;
32
+ }
33
+ if (windows && ch === "\"" && quote !== "'") {
34
+ const backslashes = current.match(/\\+$/)?.[0].length ?? 0;
35
+ current = current.slice(0, current.length - backslashes) + "\\".repeat(Math.floor(backslashes / 2));
36
+ if (backslashes % 2 === 1) {
37
+ current += "\"";
38
+ continue;
39
+ }
40
+ }
41
+ if (quote) {
42
+ if (ch === quote) quote = null;
43
+ else current += ch;
44
+ continue;
45
+ }
46
+ if (ch === "'" || ch === "\"") {
47
+ quote = ch;
48
+ hasPart = true;
49
+ continue;
50
+ }
51
+ if (/\s/.test(ch)) {
52
+ if (hasPart) {
53
+ parts.push(current);
54
+ current = "";
55
+ hasPart = false;
56
+ }
57
+ continue;
58
+ }
59
+ current += ch;
60
+ hasPart = true;
61
+ }
62
+ if (escaping) current += "\\";
63
+ if (quote) throw new Error("Invalid agent command: unterminated quote");
64
+ if (hasPart) parts.push(current);
65
+ return parts;
66
+ }
67
+ const OPENCLAW_BRIDGE_EXECUTABLE = "openclaw";
68
+ const OPENCLAW_BRIDGE_SUBCOMMAND = "acp";
69
+ function normalizeAgentName(value) {
70
+ const normalized = value?.trim().toLowerCase();
71
+ return normalized ? normalized : void 0;
72
+ }
73
+ function basename(value) {
74
+ return value.split(/[\\/]/).pop() ?? value;
75
+ }
76
+ function isEnvAssignment(value) {
77
+ return /^[A-Za-z_][A-Za-z0-9_]*=/.test(value);
78
+ }
79
+ function unwrapEnvCommand(parts) {
80
+ const command = parts.at(0);
81
+ if (!command || basename(command) !== "env") return parts;
82
+ let index = 1;
83
+ while (true) {
84
+ const part = parts.at(index);
85
+ if (!part || !isEnvAssignment(part)) break;
86
+ index += 1;
87
+ }
88
+ return parts.slice(index);
89
+ }
90
+ function matchesExecutableName(value, executableName) {
91
+ const normalized = basename(value).toLowerCase();
92
+ return normalized === executableName || normalized === `${executableName}.exe`;
93
+ }
94
+ function matchesPackageSpec(value, packageName) {
95
+ const normalized = value.trim().toLowerCase();
96
+ return normalized === packageName || normalized.startsWith(`${packageName}@`);
97
+ }
98
+ function stripModuleExtension(value) {
99
+ return value.replace(/\.[cm]?js$/i, "").toLowerCase();
100
+ }
101
+ function isAcpCommand(command, params) {
102
+ if (!command) return false;
103
+ const parts = unwrapEnvCommand(splitCommandParts(command));
104
+ if (!parts.length) return false;
105
+ if (parts.some((part) => matchesPackageSpec(part, params.packageName))) return true;
106
+ const commandName = basename(parts[0] ?? "");
107
+ if (matchesExecutableName(commandName, params.executableName)) return true;
108
+ if (!matchesExecutableName(commandName, "node")) return false;
109
+ const scriptName = stripModuleExtension(basename(parts[1] ?? ""));
110
+ return scriptName === params.executableName || scriptName === `${params.executableName}-wrapper`;
111
+ }
112
+ function isOpenClawBridgeCommand(command) {
113
+ if (!command) return false;
114
+ const parts = unwrapEnvCommand(splitCommandParts(command));
115
+ if (basename(parts[0] ?? "") === OPENCLAW_BRIDGE_EXECUTABLE) return parts[1] === OPENCLAW_BRIDGE_SUBCOMMAND;
116
+ if (basename(parts[0] ?? "") !== "node") return false;
117
+ const scriptName = basename(parts[1] ?? "");
118
+ return /^openclaw(?:\.[cm]?js)?$/i.test(scriptName) && parts[2] === OPENCLAW_BRIDGE_SUBCOMMAND;
119
+ }
120
+ function isCodexAcpCommand(command) {
121
+ return isAcpCommand(command, {
122
+ packageName: CODEX_ACP_PACKAGE,
123
+ executableName: "codex-acp"
124
+ });
125
+ }
126
+ function isClaudeAcpCommand(command) {
127
+ return isAcpCommand(command, {
128
+ packageName: "@agentclientprotocol/claude-agent-acp",
129
+ executableName: "claude-agent-acp"
130
+ });
131
+ }
132
+ function resolveAgentCommand(params) {
133
+ const normalizedAgentName = normalizeAgentName(params.agentName);
134
+ if (!normalizedAgentName) return;
135
+ return splitCommandParts(params.agentRegistry.resolve(normalizedAgentName));
136
+ }
137
+ //#endregion
138
+ export { renderAgentCommand as a, CODEX_ACP_BIN as c, OPENCLAW_CODEX_CONFIG_ARG as d, normalizeAgentName as i, CODEX_ACP_PACKAGE as l, isCodexAcpCommand as n, resolveAgentCommand as o, isOpenClawBridgeCommand as r, splitCommandParts as s, isClaudeAcpCommand as t, LEGACY_CODEX_ACP_PACKAGE as u };
@@ -1,16 +1,30 @@
1
- import { _ as splitCommandParts } from "./process-lease-B83BGiLj.mjs";
1
+ import { s as splitCommandParts } from "./command-line-CPBLOiZM.mjs";
2
2
  import { createRequire } from "node:module";
3
- import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
3
+ import { z } from "zod";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { formatPluginConfigIssue } from "openclaw/plugin-sdk/extension-shared";
8
- import { z } from "zod";
8
+ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
9
9
  //#region extensions/acpx/src/config-schema.ts
10
10
  /**
11
11
  * ACPX plugin configuration schema and public config types. Runtime setup uses
12
12
  * this file as the single source of truth for validation and defaulting.
13
13
  */
14
+ const ACPX_NATIVE_AGENT_IDS = [
15
+ "opencode",
16
+ "qwen",
17
+ "pi",
18
+ "kilocode",
19
+ "copilot"
20
+ ];
21
+ const AcpxNativeAgentsSchema = z.strictObject({
22
+ opencode: z.boolean().optional(),
23
+ qwen: z.boolean().optional(),
24
+ pi: z.boolean().optional(),
25
+ kilocode: z.boolean().optional(),
26
+ copilot: z.boolean().optional()
27
+ }).optional();
14
28
  const ACPX_PERMISSION_MODES = [
15
29
  "approve-all",
16
30
  "approve-reads",
@@ -25,6 +39,7 @@ const McpServerConfigSchema = z.object({
25
39
  });
26
40
  /** Zod schema for validating raw ACPX plugin config from OpenClaw config. */
27
41
  const AcpxPluginConfigSchema = z.strictObject({
42
+ nativeAgents: AcpxNativeAgentsSchema,
28
43
  cwd: nonEmptyTrimmedString("cwd must be a non-empty string").optional(),
29
44
  stateDir: nonEmptyTrimmedString("stateDir must be a non-empty string").optional(),
30
45
  probeAgent: nonEmptyTrimmedString("probeAgent must be a non-empty string").optional(),
@@ -181,4 +196,4 @@ function resolveAcpxPluginConfig(params) {
181
196
  };
182
197
  }
183
198
  //#endregion
184
- export { toAcpMcpServers as i, resolveAcpxPluginRoot as n, resolveOpenClawRoot as r, resolveAcpxPluginConfig as t };
199
+ export { ACPX_NATIVE_AGENT_IDS as a, toAcpMcpServers as i, resolveAcpxPluginRoot as n, AcpxNativeAgentsSchema as o, resolveOpenClawRoot as r, resolveAcpxPluginConfig as t };
@@ -0,0 +1,369 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { consumeAcpTurnStream } from "openclaw/plugin-sdk/acp-runtime";
3
+ import { clearActiveEmbeddedRun, emitAgentEvent, resolveAgentHarnessBeforePromptBuildResult, resolveBootstrapContextForRun, setActiveEmbeddedRun } from "openclaw/plugin-sdk/agent-harness-runtime";
4
+ import { SessionManager, buildSessionContext } from "openclaw/plugin-sdk/agent-sessions";
5
+ import { DEFAULT_PLUGIN_APPROVAL_TIMEOUT_MS } from "openclaw/plugin-sdk/approval-runtime";
6
+ import { toErrorObject } from "openclaw/plugin-sdk/error-runtime";
7
+ import { resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
8
+ import { appendSessionTranscriptMessageByIdentityStrict } from "openclaw/plugin-sdk/session-transcript-runtime";
9
+ //#region extensions/acpx/src/harness-attempt.ts
10
+ async function runAcpHarnessAttempt(params) {
11
+ const { input, runtime } = params;
12
+ if (!input.agentId || !input.sessionKey) throw new Error("ACP chat requires an owned OpenClaw session");
13
+ const agentId = input.agentId;
14
+ const sessionKey = input.sessionKey;
15
+ const controller = new AbortController();
16
+ const signal = AbortSignal.any([
17
+ controller.signal,
18
+ params.generationSignal,
19
+ ...input.abortSignal ? [input.abortSignal] : []
20
+ ]);
21
+ const eventGate = { open: !signal.aborted };
22
+ const stopDelivery = () => {
23
+ eventGate.open = false;
24
+ };
25
+ const assertActive = () => {
26
+ signal.throwIfAborted();
27
+ input.hostCapabilities.assertActive();
28
+ };
29
+ const transcript = {
30
+ agentId,
31
+ sessionKey,
32
+ sessionId: input.sessionId,
33
+ storePath: resolveStorePath(input.config?.session?.store, { agentId })
34
+ };
35
+ const recorder = input.userTurnTranscriptRecorder;
36
+ if (!recorder) throw new Error("ACP chat requires its admitted transcript recorder");
37
+ let handle;
38
+ let text = "";
39
+ let reasoning = "";
40
+ let started = false;
41
+ let settled = false;
42
+ let timedOut = false;
43
+ let denied = false;
44
+ let approvalFailure;
45
+ let failure;
46
+ let cancelled = false;
47
+ let terminalAnchor;
48
+ let assistant;
49
+ let assistantIdempotencyKey;
50
+ const toolMetas = [];
51
+ let messages = [];
52
+ const activeRun = {
53
+ kind: "embedded",
54
+ runId: input.runId,
55
+ toolAuthorityFingerprint: input.toolAuthorityFingerprint,
56
+ queueMessage: async () => {
57
+ throw new Error(`${params.label} does not support live message injection`);
58
+ },
59
+ isStreaming: () => started && !settled,
60
+ isAborted: () => signal.aborted,
61
+ isCompacting: () => false,
62
+ cancel: () => controller.abort(),
63
+ abort: () => controller.abort(),
64
+ sourceReplyDeliveryMode: input.sourceReplyDeliveryMode
65
+ };
66
+ let activeRegistered = false;
67
+ let timer;
68
+ assertActive();
69
+ try {
70
+ setActiveEmbeddedRun(input.sessionId, activeRun, sessionKey, input.sessionFile, agentId);
71
+ activeRegistered = true;
72
+ input.replyOperation?.attachBackend(activeRun);
73
+ signal.addEventListener("abort", stopDelivery, { once: true });
74
+ timer = setTimeout(() => {
75
+ timedOut = true;
76
+ input.onAttemptTimeout?.(/* @__PURE__ */ new Error("ACP turn timed out"));
77
+ controller.abort();
78
+ }, input.timeoutMs);
79
+ timer.unref();
80
+ const sessionContext = await SessionManager.openModelContextAsync(transcript, {
81
+ cwd: input.workspaceDir,
82
+ signal
83
+ });
84
+ const entries = sessionContext.getBranch();
85
+ messages = sessionContext.buildSessionContext().messages;
86
+ assertActive();
87
+ const target = {
88
+ agentId,
89
+ sessionKey: `agent:${agentId}:harness:${params.harnessId}:${input.sessionId}`,
90
+ agent: params.agent,
91
+ agentCommand: params.command,
92
+ mode: "persistent",
93
+ bridgeSession: {
94
+ agentId,
95
+ sessionKey,
96
+ native: true
97
+ },
98
+ cwd: input.workspaceDir
99
+ };
100
+ handle = await runtime.ensureSession(target);
101
+ assertActive();
102
+ const status = await runtime.getStatus({ handle });
103
+ assertActive();
104
+ if (status.models?.currentModelId !== input.modelId) await runtime.setModel({
105
+ handle,
106
+ model: input.modelId,
107
+ signal,
108
+ assertActive
109
+ });
110
+ assertActive();
111
+ const lastRequestId = status.lastRequestId;
112
+ const previousAssistantIndex = lastRequestId ? entries.findLastIndex((entry) => entry.type === "message" && "idempotencyKey" in entry.message && entry.message.idempotencyKey === `${lastRequestId}:acp:assistant`) : -1;
113
+ const previousUserIndex = lastRequestId && previousAssistantIndex < 0 ? entries.findLastIndex((entry) => entry.type === "message" && entry.message.role === "user" && (entry.id === lastRequestId || lastRequestId.startsWith(`${entry.id}:acp:`))) : -1;
114
+ if (lastRequestId && previousAssistantIndex < 0 && previousUserIndex < 0) throw new Error("ACP conversation history cannot be reconciled; reset this session before continuing");
115
+ await recorder.persistApproved({ expectedSessionId: input.sessionId });
116
+ assertActive();
117
+ const admission = recorder.getAdmissionReceipt();
118
+ if (recorder.isBlocked() || !recorder.hasPersisted() || !admission) throw new Error("ACP input was not admitted to its transcript");
119
+ const previousIndex = previousAssistantIndex >= 0 ? previousAssistantIndex : previousUserIndex;
120
+ const previous = buildSessionContext(entries.slice(previousIndex + 1).filter((entry) => entry.id !== admission.entryId)).messages;
121
+ const bootstrap = !lastRequestId ? await resolveBootstrapContextForRun({
122
+ workspaceDir: input.workspaceDir,
123
+ config: input.config,
124
+ sessionKey,
125
+ sessionId: input.sessionId,
126
+ agentId,
127
+ chatType: input.chatType,
128
+ contextMode: input.bootstrapContextMode,
129
+ runKind: input.bootstrapContextRunKind
130
+ }) : void 0;
131
+ const built = await resolveAgentHarnessBeforePromptBuildResult({
132
+ prompt: input.prompt,
133
+ currentInboundContext: input.currentInboundContext,
134
+ messages,
135
+ developerInstructions: [...bootstrap?.contextFiles.map((file) => `${file.path}\n${file.content}`) ?? [], input.extraSystemPrompt].filter(Boolean).join("\n\n"),
136
+ ctx: {
137
+ runId: input.runId,
138
+ agentId,
139
+ sessionId: input.sessionId,
140
+ sessionKey,
141
+ workspaceDir: input.workspaceDir,
142
+ config: input.config,
143
+ trigger: input.trigger,
144
+ modelProviderId: input.provider,
145
+ modelId: input.modelId
146
+ },
147
+ bootstrapContextRunKind: input.bootstrapContextRunKind
148
+ });
149
+ if (built.toolsAllow) throw new Error("ACP cannot enforce this prompt-hook tool restriction");
150
+ const onPermissionRequest = async (request, context) => {
151
+ try {
152
+ assertActive();
153
+ const approvalSignal = AbortSignal.any([signal, context.signal]);
154
+ const detail = JSON.stringify(request.raw.toolCall);
155
+ const supportsAllowOnce = request.raw.options.some((option) => option.kind === "allow_once");
156
+ const requestResult = await input.hostCapabilities.requestApproval({
157
+ title: `${params.label} permission request`,
158
+ description: request.raw.toolCall.title ?? "Native tool action",
159
+ detail,
160
+ signal: approvalSignal,
161
+ severity: "warning",
162
+ toolName: request.inferredKind ?? "other",
163
+ toolCallId: request.raw.toolCall.toolCallId,
164
+ allowedDecisions: supportsAllowOnce ? ["allow-once", "deny"] : ["deny"],
165
+ timeoutMs: DEFAULT_PLUGIN_APPROVAL_TIMEOUT_MS,
166
+ transportTimeoutMs: DEFAULT_PLUGIN_APPROVAL_TIMEOUT_MS + 1e4
167
+ });
168
+ const result = requestResult?.id ? await input.hostCapabilities.waitForApproval({
169
+ approvalId: requestResult.id,
170
+ timeoutMs: DEFAULT_PLUGIN_APPROVAL_TIMEOUT_MS,
171
+ transportTimeoutMs: DEFAULT_PLUGIN_APPROVAL_TIMEOUT_MS + 1e4,
172
+ signal: approvalSignal
173
+ }) : void 0;
174
+ assertActive();
175
+ approvalSignal.throwIfAborted();
176
+ const allowed = supportsAllowOnce && result?.decision === "allow-once";
177
+ denied ||= !allowed;
178
+ return { outcome: allowed ? "allow_once" : "reject_once" };
179
+ } catch (error) {
180
+ if (!signal.aborted && !context.signal.aborted) approvalFailure = toErrorObject(error, "Native approval request failed");
181
+ denied = true;
182
+ return { outcome: "cancel" };
183
+ }
184
+ };
185
+ const requestId = `${admission.entryId}:acp:${randomUUID()}`;
186
+ const turn = {
187
+ handle,
188
+ text: [
189
+ built.developerInstructions ? `Conversation instructions:\n${built.developerInstructions}` : void 0,
190
+ previous.length ? `Conversation context before this turn:\n${JSON.stringify(previous)}` : void 0,
191
+ `Current turn:\n${built.prompt}`
192
+ ].filter(Boolean).join("\n\n"),
193
+ mode: "prompt",
194
+ requestId,
195
+ signal,
196
+ assertActive,
197
+ onPermissionRequest,
198
+ ...input.images?.length ? { attachments: input.images.map((image) => ({
199
+ data: image.data,
200
+ mediaType: image.mimeType
201
+ })) } : {}
202
+ };
203
+ const outcome = await consumeAcpTurnStream({
204
+ runtime,
205
+ turn,
206
+ eventGate,
207
+ onBeforePrompt: assertActive,
208
+ onPromptStarted: () => {
209
+ started = true;
210
+ recorder.markSentToProvider?.();
211
+ input.onExecutionStarted?.();
212
+ },
213
+ onOutputEvent: async (event) => {
214
+ assertActive();
215
+ if (event.type === "text_delta") {
216
+ if (event.stream === "thought") {
217
+ reasoning += event.text;
218
+ await input.onReasoningStream?.({ text: reasoning });
219
+ } else {
220
+ if (!text) {
221
+ await input.onAssistantMessageStart?.();
222
+ assertActive();
223
+ }
224
+ text += event.text;
225
+ const update = {
226
+ stream: "assistant",
227
+ data: {
228
+ text,
229
+ delta: event.text
230
+ }
231
+ };
232
+ emitAgentEvent({
233
+ runId: input.runId,
234
+ sessionKey,
235
+ sessionId: input.sessionId,
236
+ ...update
237
+ });
238
+ await input.onAgentEvent?.(update);
239
+ assertActive();
240
+ await input.onPartialReply?.({ text });
241
+ }
242
+ } else {
243
+ const existing = event.toolCallId ? toolMetas.find((tool) => tool.toolCallId === event.toolCallId) : void 0;
244
+ const metadata = {
245
+ toolName: event.title ?? event.kind ?? "tool",
246
+ toolCallId: event.toolCallId,
247
+ meta: event.text,
248
+ isError: event.status === "failed"
249
+ };
250
+ if (existing) Object.assign(existing, metadata);
251
+ else toolMetas.push(metadata);
252
+ await input.onToolResult?.({ text: event.text });
253
+ }
254
+ }
255
+ });
256
+ if (approvalFailure) throw approvalFailure;
257
+ cancelled = outcome.terminalStatus === "cancelled";
258
+ if (!text.trim() && denied) text = `${params.label} could not complete this turn because permission was not granted.`;
259
+ else if (!text.trim() && toolMetas.some((tool) => tool.isError)) text = `${params.label} reported a failed tool operation and did not return an answer.`;
260
+ const usage = (await runtime.getStatus({ handle })).usage?.perRequest?.[requestId];
261
+ assistant = {
262
+ role: "assistant",
263
+ provider: input.provider,
264
+ model: input.modelId,
265
+ api: input.model.api,
266
+ content: [...reasoning ? [{
267
+ type: "thinking",
268
+ thinking: reasoning
269
+ }] : [], ...text ? [{
270
+ type: "text",
271
+ text
272
+ }] : []],
273
+ stopReason: cancelled ? "aborted" : "stop",
274
+ timestamp: Date.now(),
275
+ usage: {
276
+ input: usage?.inputTokens ?? 0,
277
+ output: usage?.outputTokens ?? 0,
278
+ cacheRead: usage?.cachedReadTokens ?? 0,
279
+ cacheWrite: usage?.cachedWriteTokens ?? 0,
280
+ totalTokens: usage?.totalTokens ?? 0,
281
+ cost: {
282
+ input: 0,
283
+ output: 0,
284
+ cacheRead: 0,
285
+ cacheWrite: 0,
286
+ total: 0
287
+ }
288
+ }
289
+ };
290
+ const key = `${requestId}:acp:assistant`;
291
+ const written = await appendSessionTranscriptMessageByIdentityStrict({
292
+ ...transcript,
293
+ config: input.config,
294
+ runId: input.runId,
295
+ updateMode: "inline",
296
+ message: {
297
+ ...assistant,
298
+ idempotencyKey: key
299
+ },
300
+ prepareMessageAfterIdempotencyCheck: (message) => {
301
+ input.hostCapabilities.assertActive();
302
+ return message;
303
+ }
304
+ });
305
+ if (written.kind !== "result") throw new Error("ACP assistant transcript was not committed");
306
+ assistant = written.result.message;
307
+ assistantIdempotencyKey = key;
308
+ terminalAnchor = written.result.anchor;
309
+ messages = (await SessionManager.openModelContextAsync(transcript, {
310
+ cwd: input.workspaceDir,
311
+ through: written.result.anchor
312
+ })).buildSessionContext().messages;
313
+ } catch (error) {
314
+ failure = error;
315
+ } finally {
316
+ settled = true;
317
+ clearTimeout(timer);
318
+ signal.removeEventListener("abort", stopDelivery);
319
+ if (activeRegistered) clearActiveEmbeddedRun(input.sessionId, activeRun, sessionKey, input.sessionFile);
320
+ }
321
+ return {
322
+ terminal: timedOut ? {
323
+ kind: "timeout",
324
+ phase: "prompt",
325
+ source: "runtime",
326
+ aborted: true
327
+ } : signal.aborted || cancelled ? {
328
+ kind: "aborted",
329
+ source: "external"
330
+ } : failure ? {
331
+ kind: "failed",
332
+ source: "prompt",
333
+ error: failure
334
+ } : { kind: "ok" },
335
+ sessionIdUsed: input.sessionId,
336
+ sessionFileUsed: input.sessionFile,
337
+ agentHarnessId: params.harnessId,
338
+ runtimeModelSelection: {
339
+ provider: input.provider,
340
+ model: input.modelId
341
+ },
342
+ messagesSnapshot: messages,
343
+ assistantTexts: text ? [text] : [],
344
+ lastAssistant: assistant,
345
+ currentAttemptAssistant: assistant,
346
+ ...assistantIdempotencyKey ? {
347
+ assistantTranscriptOwned: true,
348
+ assistantTranscriptIdempotencyKey: assistantIdempotencyKey,
349
+ contextEngineTerminalAnchor: terminalAnchor
350
+ } : {},
351
+ toolMetas,
352
+ didSendViaMessagingTool: false,
353
+ messagingToolSentTexts: [],
354
+ messagingToolSentMediaUrls: [],
355
+ messagingToolSentTargets: [],
356
+ cloudCodeAssistFormatError: false,
357
+ replayMetadata: {
358
+ hadPotentialSideEffects: toolMetas.length > 0,
359
+ replaySafe: !started
360
+ },
361
+ itemLifecycle: {
362
+ startedCount: toolMetas.length,
363
+ completedCount: failure ? 0 : toolMetas.length,
364
+ activeCount: 0
365
+ }
366
+ };
367
+ }
368
+ //#endregion
369
+ export { runAcpHarnessAttempt };
@@ -1,18 +1,18 @@
1
- import { c as PI_SESSION_READ_COMMAND, i as PI_LOCAL_SESSION_HOST_ID, l as PI_TERMINAL_RESUME_COMMAND, n as piSessionStore, o as PI_SESSIONS_LIST_COMMAND, r as piSessionStoreAvailable, s as PI_SESSION_ID_PATTERN, t as piAcpSessionStoreRoot } from "./pi-session-paths-EMbd4Hkz.mjs";
2
- import { resolveNodeHostExecutable } from "openclaw/plugin-sdk/node-host";
3
- import { createSessionCatalogFamily, importSessionCatalogHistory, isExternalUserText, listAdoptedSessionCatalogSessions, sessionCatalogAdoptedSessionKey } from "openclaw/plugin-sdk/session-catalog";
4
- import { isRecord, normalizeBoundedOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
1
+ import { c as PI_SESSION_READ_COMMAND, i as PI_LOCAL_SESSION_HOST_ID, l as PI_TERMINAL_RESUME_COMMAND, n as piSessionStore, o as PI_SESSIONS_LIST_COMMAND, r as piSessionStoreAvailable, s as PI_SESSION_ID_PATTERN, t as piAcpSessionStoreRoot } from "./pi-session-paths-CIvyk6KB.mjs";
2
+ import { parseDateFirstTimestampMs } from "openclaw/plugin-sdk/number-runtime";
5
3
  import { createReadStream } from "node:fs";
6
4
  import path from "node:path";
5
+ import { isRecord, normalizeBoundedOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
6
+ import { resolveNodeHostExecutable } from "openclaw/plugin-sdk/node-host";
7
+ import { createSessionCatalogFamily, importSessionCatalogHistory, isExternalUserText, listAdoptedSessionCatalogSessions, sessionCatalogAdoptedSessionKey } from "openclaw/plugin-sdk/session-catalog";
7
8
  import fs$1 from "node:fs/promises";
8
- import process from "node:process";
9
9
  import { resolveAcpSessionAvailability } from "openclaw/plugin-sdk/acp-runtime";
10
+ import process from "node:process";
10
11
  import { resolveSessionAgentIdsStrict } from "openclaw/plugin-sdk/agent-scope-runtime";
11
12
  import { sessionCatalogPaging } from "openclaw/plugin-sdk/session-catalog-paging";
12
13
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
13
14
  import { runTasksWithConcurrency } from "openclaw/plugin-sdk/concurrency-runtime";
14
15
  import { isPathStrictlyInside, readFileRangeAsync } from "openclaw/plugin-sdk/file-access-runtime";
15
- import { parseDateFirstTimestampMs } from "openclaw/plugin-sdk/number-runtime";
16
16
  //#region extensions/acpx/src/pi-session-timestamp.ts
17
17
  /** Preserve Pi JSONL's date-first string contract while accepting numeric millisecond values. */
18
18
  function parsePiSessionTimestampMs(value) {