@openclaw/copilot 2026.5.28
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 +28 -0
- package/dist/attempt-DMegR4ua.js +1667 -0
- package/dist/doctor-contract-api.js +30 -0
- package/dist/harness-Blgz_qk3.js +473 -0
- package/dist/harness.js +2 -0
- package/dist/index.js +33 -0
- package/dist/runtime-C2rPhifm.js +341 -0
- package/npm-shrinkwrap.json +160 -0
- package/openclaw.plugin.json +39 -0
- package/package.json +57 -0
|
@@ -0,0 +1,1667 @@
|
|
|
1
|
+
import { n as createInfiniteSessionConfig, r as resolveCopilotAuth } from "./harness-Blgz_qk3.js";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fsp from "node:fs/promises";
|
|
5
|
+
import { acquireSessionWriteLock, appendSessionTranscriptMessage, applyEmbeddedAttemptToolsAllow, buildEmbeddedAttemptToolRunContext, detectAndLoadAgentHarnessPromptImages, emitSessionTranscriptUpdate, getPluginToolMeta, isSubagentSessionKey, resolveAttemptFsWorkspaceOnly, resolveAttemptSpawnWorkspaceDir, resolveBootstrapContextForRun, resolveEmbeddedAttemptToolConstructionPlan, resolveModelAuthMode, resolveSandboxContext, resolveSessionAgentIds, resolveSessionWriteLockAcquireTimeoutMs, resolveUserPath, runAgentHarnessBeforeMessageWriteHook } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
6
|
+
//#region extensions/copilot/src/dual-write-transcripts.ts
|
|
7
|
+
/**
|
|
8
|
+
* Mirrors the AgentMessages produced by the copilot agent runtime into the
|
|
9
|
+
* OpenClaw audit transcript that sits next to (but is distinct from) the
|
|
10
|
+
* SDK's own session storage.
|
|
11
|
+
*
|
|
12
|
+
* The OpenClaw shell (src/agents/command/attempt-execution.ts) already
|
|
13
|
+
* writes the user prompt and the terminal assistant text into the
|
|
14
|
+
* transcript at the end of each attempt. That is the bare minimum to
|
|
15
|
+
* keep `/history` working. It does NOT capture tool calls, tool
|
|
16
|
+
* results, or intermediate assistant turns — those live only in the
|
|
17
|
+
* SDK's own session file.
|
|
18
|
+
*
|
|
19
|
+
* For audit/compliance and for the codex-parity guarantees we promised
|
|
20
|
+
* in the proposal, we mirror the full `messagesSnapshot` (user +
|
|
21
|
+
* assistant + toolResult) into the OpenClaw transcript via the same
|
|
22
|
+
* plugin-sdk primitives that the codex extension uses
|
|
23
|
+
* (extensions/codex/src/app-server/transcript-mirror.ts). Both writers
|
|
24
|
+
* cooperate via idempotency-key dedupe: each mirrored entry carries a
|
|
25
|
+
* stable `${idempotencyScope}:${identity}` key, and we skip any key
|
|
26
|
+
* already present in the transcript on disk before appending. Both
|
|
27
|
+
* attempt-execution's untagged entries (no idempotencyKey) and our
|
|
28
|
+
* tagged mirror entries can coexist; attempt-execution dedupes its own
|
|
29
|
+
* final-assistant append via `embeddedAssistantGapFill` content match.
|
|
30
|
+
*
|
|
31
|
+
* Failures (lock contention, fs errors, etc.) are swallowed by the
|
|
32
|
+
* caller-side `dualWriteCopilotTranscriptBestEffort` wrapper used
|
|
33
|
+
* in attempt.ts so they cannot break the attempt; this module itself
|
|
34
|
+
* throws on infrastructure failure so callers can choose policy.
|
|
35
|
+
*/
|
|
36
|
+
const MIRROR_IDENTITY_META_KEY = "mirrorIdentity";
|
|
37
|
+
/**
|
|
38
|
+
* Tag a message with a stable logical identity for mirror dedupe.
|
|
39
|
+
* Callers should use a value that is invariant for the same logical
|
|
40
|
+
* message across re-emits (e.g. `${sdkSessionId}:assistant:${turnIndex}`)
|
|
41
|
+
* but distinct for genuinely-distinct messages. When present this
|
|
42
|
+
* identity replaces the role/content fingerprint in the idempotency
|
|
43
|
+
* key, so the dedupe survives caller-scope rotation without collapsing
|
|
44
|
+
* distinct same-content turns. Symmetric to
|
|
45
|
+
* `attachCodexMirrorIdentity` in the codex extension.
|
|
46
|
+
*/
|
|
47
|
+
function attachCopilotMirrorIdentity(message, identity) {
|
|
48
|
+
const record = message;
|
|
49
|
+
const existing = record["__openclaw"];
|
|
50
|
+
const baseMeta = existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {};
|
|
51
|
+
return {
|
|
52
|
+
...record,
|
|
53
|
+
__openclaw: {
|
|
54
|
+
...baseMeta,
|
|
55
|
+
[MIRROR_IDENTITY_META_KEY]: identity
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function readMirrorIdentity(message) {
|
|
60
|
+
const meta = message["__openclaw"];
|
|
61
|
+
if (!meta || typeof meta !== "object" || Array.isArray(meta)) return;
|
|
62
|
+
const id = meta[MIRROR_IDENTITY_META_KEY];
|
|
63
|
+
return typeof id === "string" && id.length > 0 ? id : void 0;
|
|
64
|
+
}
|
|
65
|
+
function fingerprintMirrorMessageContent(message) {
|
|
66
|
+
const payload = JSON.stringify({
|
|
67
|
+
role: message.role,
|
|
68
|
+
content: message.content
|
|
69
|
+
});
|
|
70
|
+
return createHash("sha256").update(payload).digest("hex").slice(0, 16);
|
|
71
|
+
}
|
|
72
|
+
function buildMirrorDedupeIdentity(message) {
|
|
73
|
+
const explicit = readMirrorIdentity(message);
|
|
74
|
+
if (explicit) return explicit;
|
|
75
|
+
return `${message.role}:${fingerprintMirrorMessageContent(message)}`;
|
|
76
|
+
}
|
|
77
|
+
async function mirrorCopilotTranscript(params) {
|
|
78
|
+
const messages = params.messages.filter((message) => message.role === "user" || message.role === "assistant" || message.role === "toolResult");
|
|
79
|
+
if (messages.length === 0) return;
|
|
80
|
+
const lock = await acquireSessionWriteLock({
|
|
81
|
+
sessionFile: params.sessionFile,
|
|
82
|
+
timeoutMs: resolveSessionWriteLockAcquireTimeoutMs(params.config)
|
|
83
|
+
});
|
|
84
|
+
try {
|
|
85
|
+
const existingIdempotencyKeys = await readTranscriptIdempotencyKeys(params.sessionFile);
|
|
86
|
+
for (const message of messages) {
|
|
87
|
+
const dedupeIdentity = buildMirrorDedupeIdentity(message);
|
|
88
|
+
const idempotencyKey = params.idempotencyScope ? `${params.idempotencyScope}:${dedupeIdentity}` : void 0;
|
|
89
|
+
if (idempotencyKey && existingIdempotencyKeys.has(idempotencyKey)) continue;
|
|
90
|
+
const nextMessage = runAgentHarnessBeforeMessageWriteHook({
|
|
91
|
+
message: {
|
|
92
|
+
...message,
|
|
93
|
+
...idempotencyKey ? { idempotencyKey } : {}
|
|
94
|
+
},
|
|
95
|
+
agentId: params.agentId,
|
|
96
|
+
sessionKey: params.sessionKey
|
|
97
|
+
});
|
|
98
|
+
if (!nextMessage) continue;
|
|
99
|
+
const messageToAppend = idempotencyKey ? {
|
|
100
|
+
...nextMessage,
|
|
101
|
+
idempotencyKey
|
|
102
|
+
} : nextMessage;
|
|
103
|
+
await appendSessionTranscriptMessage({
|
|
104
|
+
transcriptPath: params.sessionFile,
|
|
105
|
+
message: messageToAppend,
|
|
106
|
+
config: params.config
|
|
107
|
+
});
|
|
108
|
+
if (idempotencyKey) existingIdempotencyKeys.add(idempotencyKey);
|
|
109
|
+
}
|
|
110
|
+
} finally {
|
|
111
|
+
await lock.release();
|
|
112
|
+
}
|
|
113
|
+
if (params.sessionKey) emitSessionTranscriptUpdate({
|
|
114
|
+
sessionFile: params.sessionFile,
|
|
115
|
+
sessionKey: params.sessionKey
|
|
116
|
+
});
|
|
117
|
+
else emitSessionTranscriptUpdate(params.sessionFile);
|
|
118
|
+
}
|
|
119
|
+
async function readTranscriptIdempotencyKeys(sessionFile) {
|
|
120
|
+
const keys = /* @__PURE__ */ new Set();
|
|
121
|
+
let raw;
|
|
122
|
+
try {
|
|
123
|
+
raw = await fsp.readFile(sessionFile, "utf8");
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (error.code !== "ENOENT") throw error;
|
|
126
|
+
return keys;
|
|
127
|
+
}
|
|
128
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
129
|
+
if (!line.trim()) continue;
|
|
130
|
+
try {
|
|
131
|
+
const parsed = JSON.parse(line);
|
|
132
|
+
if (typeof parsed.message?.idempotencyKey === "string") keys.add(parsed.message.idempotencyKey);
|
|
133
|
+
} catch {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return keys;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Caller-side wrapper that swallows mirror failures. attempt.ts uses
|
|
141
|
+
* this so that a transient transcript-mirror failure (lock contention,
|
|
142
|
+
* disk full, etc.) never breaks an otherwise-successful attempt. The
|
|
143
|
+
* SDK's own session file remains the source of truth in that case;
|
|
144
|
+
* the OpenClaw audit trail just misses the intermediate messages for
|
|
145
|
+
* this turn.
|
|
146
|
+
*/
|
|
147
|
+
async function dualWriteCopilotTranscriptBestEffort(params) {
|
|
148
|
+
try {
|
|
149
|
+
await mirrorCopilotTranscript(params);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.warn("[copilot-attempt] dual-write transcript mirror failed", error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region extensions/copilot/src/usage-bridge.ts
|
|
156
|
+
function isCopilotUsageSource(data) {
|
|
157
|
+
return typeof data === "object" && data !== null;
|
|
158
|
+
}
|
|
159
|
+
function buildZeroCost() {
|
|
160
|
+
return {
|
|
161
|
+
cacheRead: 0,
|
|
162
|
+
cacheWrite: 0,
|
|
163
|
+
input: 0,
|
|
164
|
+
output: 0,
|
|
165
|
+
total: 0
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function coerceTokenCount(value) {
|
|
169
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : void 0;
|
|
170
|
+
}
|
|
171
|
+
function normalizeCopilotUsage(data) {
|
|
172
|
+
if (!isCopilotUsageSource(data)) return;
|
|
173
|
+
const input = coerceTokenCount(data.inputTokens);
|
|
174
|
+
const output = coerceTokenCount(data.outputTokens);
|
|
175
|
+
const cacheRead = coerceTokenCount(data.cacheReadTokens);
|
|
176
|
+
const cacheWrite = coerceTokenCount(data.cacheWriteTokens);
|
|
177
|
+
return {
|
|
178
|
+
cacheRead,
|
|
179
|
+
cacheWrite,
|
|
180
|
+
input,
|
|
181
|
+
output,
|
|
182
|
+
total: (input ?? 0) + (output ?? 0) + (cacheRead ?? 0) + (cacheWrite ?? 0)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function buildCopilotAssistantUsage(params) {
|
|
186
|
+
const usage = params.usage ?? normalizeCopilotUsage({ outputTokens: params.fallbackOutputTokens });
|
|
187
|
+
return {
|
|
188
|
+
cacheRead: usage?.cacheRead ?? 0,
|
|
189
|
+
cacheWrite: usage?.cacheWrite ?? 0,
|
|
190
|
+
cost: buildZeroCost(),
|
|
191
|
+
input: usage?.input ?? 0,
|
|
192
|
+
output: usage?.output ?? 0,
|
|
193
|
+
totalTokens: usage?.total ?? 0
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region extensions/copilot/src/event-bridge.ts
|
|
198
|
+
function attachEventBridge(session, options) {
|
|
199
|
+
const messageOrder = [];
|
|
200
|
+
const messagesById = /* @__PURE__ */ new Map();
|
|
201
|
+
const reasoningOrder = [];
|
|
202
|
+
const reasoningById = /* @__PURE__ */ new Map();
|
|
203
|
+
let lastAssistantEvent;
|
|
204
|
+
let usage;
|
|
205
|
+
let streamError;
|
|
206
|
+
const toolMetas = [];
|
|
207
|
+
const toolNamesByCallId = /* @__PURE__ */ new Map();
|
|
208
|
+
let startedCount = 0;
|
|
209
|
+
let completedCount = 0;
|
|
210
|
+
let deltaQueue = Promise.resolve();
|
|
211
|
+
let deltaChain = Promise.resolve();
|
|
212
|
+
let firstDeltaError;
|
|
213
|
+
let detached = false;
|
|
214
|
+
const unsubscribeFns = [];
|
|
215
|
+
registerListener(session, unsubscribeFns, "assistant.message_delta", (event) => {
|
|
216
|
+
const messageId = readString$2(event.data.messageId) ?? "assistant-message";
|
|
217
|
+
const delta = event.data.deltaContent;
|
|
218
|
+
if (!delta) return;
|
|
219
|
+
const entry = ensureMessageAccumulator(messagesById, messageOrder, messageId);
|
|
220
|
+
entry.text += delta;
|
|
221
|
+
const onAssistantDelta = options.onAssistantDelta;
|
|
222
|
+
if (!onAssistantDelta) return;
|
|
223
|
+
const payload = {
|
|
224
|
+
delta,
|
|
225
|
+
sessionId: options.getSdkSessionId(),
|
|
226
|
+
text: entry.text,
|
|
227
|
+
usage
|
|
228
|
+
};
|
|
229
|
+
deltaQueue = deltaQueue.then(() => onAssistantDelta(payload), () => onAssistantDelta(payload)).catch((error) => {
|
|
230
|
+
firstDeltaError ??= error;
|
|
231
|
+
});
|
|
232
|
+
deltaChain = deltaQueue.then(() => {
|
|
233
|
+
if (firstDeltaError !== void 0) throw firstDeltaError;
|
|
234
|
+
});
|
|
235
|
+
deltaChain.catch(() => void 0);
|
|
236
|
+
});
|
|
237
|
+
registerListener(session, unsubscribeFns, "assistant.reasoning_delta", (event) => {
|
|
238
|
+
const reasoningId = readString$2(event.data.reasoningId) ?? "assistant-reasoning";
|
|
239
|
+
const delta = event.data.deltaContent;
|
|
240
|
+
if (!delta) return;
|
|
241
|
+
if (!reasoningById.has(reasoningId)) {
|
|
242
|
+
reasoningById.set(reasoningId, "");
|
|
243
|
+
reasoningOrder.push(reasoningId);
|
|
244
|
+
}
|
|
245
|
+
reasoningById.set(reasoningId, `${reasoningById.get(reasoningId) ?? ""}${delta}`);
|
|
246
|
+
});
|
|
247
|
+
registerListener(session, unsubscribeFns, "assistant.message", (event) => {
|
|
248
|
+
lastAssistantEvent = event;
|
|
249
|
+
const entry = ensureMessageAccumulator(messagesById, messageOrder, event.data.messageId);
|
|
250
|
+
if (typeof event.data.content === "string" && event.data.content.length >= entry.text.length) entry.text = event.data.content;
|
|
251
|
+
});
|
|
252
|
+
registerListener(session, unsubscribeFns, "assistant.usage", (event) => {
|
|
253
|
+
usage = normalizeCopilotUsage(event.data);
|
|
254
|
+
});
|
|
255
|
+
registerListener(session, unsubscribeFns, "tool.execution_start", (event) => {
|
|
256
|
+
startedCount += 1;
|
|
257
|
+
toolNamesByCallId.set(event.data.toolCallId, event.data.toolName);
|
|
258
|
+
toolMetas.push({ toolName: event.data.toolName });
|
|
259
|
+
});
|
|
260
|
+
registerListener(session, unsubscribeFns, "tool.execution_complete", (event) => {
|
|
261
|
+
completedCount += 1;
|
|
262
|
+
const toolName = toolNamesByCallId.get(event.data.toolCallId);
|
|
263
|
+
const meta = event.data.success ? event.data.result?.detailedContent ?? event.data.result?.content : event.data.error?.message;
|
|
264
|
+
if (toolName) toolMetas.push({
|
|
265
|
+
meta,
|
|
266
|
+
toolName
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
registerListener(session, unsubscribeFns, "session.error", (event) => {
|
|
270
|
+
if (!options.isAborted()) streamError = createPromptError$1(event.data.errorCode ?? event.data.errorType, event.data.message);
|
|
271
|
+
});
|
|
272
|
+
registerListener(session, unsubscribeFns, "abort", (event) => {
|
|
273
|
+
if (!options.isAborted()) streamError = createPromptError$1("session_aborted", `[copilot-attempt] session aborted: ${event.data.reason}`);
|
|
274
|
+
});
|
|
275
|
+
return {
|
|
276
|
+
recordSendResult(result) {
|
|
277
|
+
if (!isAssistantMessageEvent(result)) return false;
|
|
278
|
+
lastAssistantEvent = result;
|
|
279
|
+
return true;
|
|
280
|
+
},
|
|
281
|
+
awaitDeltaChain() {
|
|
282
|
+
return deltaChain;
|
|
283
|
+
},
|
|
284
|
+
snapshot() {
|
|
285
|
+
return {
|
|
286
|
+
assistantTexts: finalizeAssistantTexts(messageOrder, messagesById, lastAssistantEvent),
|
|
287
|
+
completedCount,
|
|
288
|
+
lastAssistantEvent,
|
|
289
|
+
startedCount,
|
|
290
|
+
streamError,
|
|
291
|
+
toolMetas: toolMetas.map((toolMeta) => Object.assign({}, toolMeta)),
|
|
292
|
+
usage: usage ? { ...usage } : void 0
|
|
293
|
+
};
|
|
294
|
+
},
|
|
295
|
+
buildAssistantMessage(args) {
|
|
296
|
+
return buildAssistantMessage({
|
|
297
|
+
event: lastAssistantEvent,
|
|
298
|
+
modelRef: args.modelRef,
|
|
299
|
+
now: args.now,
|
|
300
|
+
reasoningById,
|
|
301
|
+
reasoningOrder,
|
|
302
|
+
usage,
|
|
303
|
+
assistantTexts: finalizeAssistantTexts(messageOrder, messagesById, lastAssistantEvent)
|
|
304
|
+
});
|
|
305
|
+
},
|
|
306
|
+
finalizeAssistantTexts() {
|
|
307
|
+
return finalizeAssistantTexts(messageOrder, messagesById, lastAssistantEvent);
|
|
308
|
+
},
|
|
309
|
+
detach() {
|
|
310
|
+
if (detached) return;
|
|
311
|
+
detached = true;
|
|
312
|
+
for (const unsubscribe of [...unsubscribeFns].toReversed()) try {
|
|
313
|
+
unsubscribe();
|
|
314
|
+
} catch {}
|
|
315
|
+
unsubscribeFns.length = 0;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function buildAssistantMessage(params) {
|
|
320
|
+
const event = params.event;
|
|
321
|
+
const text = event ? event.data.content || params.assistantTexts[params.assistantTexts.length - 1] || "" : "";
|
|
322
|
+
const reasoningText = event?.data.reasoningText ?? joinReasoning(params.reasoningOrder, params.reasoningById);
|
|
323
|
+
const toolRequests = event?.data.toolRequests ?? [];
|
|
324
|
+
if (!text && !reasoningText && toolRequests.length === 0) return;
|
|
325
|
+
const content = [];
|
|
326
|
+
if (reasoningText) content.push({
|
|
327
|
+
thinking: reasoningText,
|
|
328
|
+
type: "thinking"
|
|
329
|
+
});
|
|
330
|
+
if (text) content.push({
|
|
331
|
+
text,
|
|
332
|
+
type: "text"
|
|
333
|
+
});
|
|
334
|
+
for (const request of toolRequests) content.push({
|
|
335
|
+
arguments: request.arguments ?? {},
|
|
336
|
+
id: request.toolCallId,
|
|
337
|
+
name: request.name,
|
|
338
|
+
type: "toolCall"
|
|
339
|
+
});
|
|
340
|
+
return {
|
|
341
|
+
api: params.modelRef.api ?? "openai-responses",
|
|
342
|
+
content,
|
|
343
|
+
model: event?.data.model ?? params.modelRef.id,
|
|
344
|
+
provider: params.modelRef.provider,
|
|
345
|
+
role: "assistant",
|
|
346
|
+
stopReason: toolRequests.length > 0 ? "toolUse" : "stop",
|
|
347
|
+
timestamp: params.now(),
|
|
348
|
+
usage: buildCopilotAssistantUsage({
|
|
349
|
+
fallbackOutputTokens: event?.data.outputTokens,
|
|
350
|
+
usage: params.usage
|
|
351
|
+
})
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function createPromptError$1(code, message, cause) {
|
|
355
|
+
const error = new Error(message);
|
|
356
|
+
error.code = code;
|
|
357
|
+
if (cause !== void 0) error.cause = cause;
|
|
358
|
+
return error;
|
|
359
|
+
}
|
|
360
|
+
function ensureMessageAccumulator(messagesById, messageOrder, messageId) {
|
|
361
|
+
let entry = messagesById.get(messageId);
|
|
362
|
+
if (!entry) {
|
|
363
|
+
entry = {
|
|
364
|
+
messageId,
|
|
365
|
+
text: ""
|
|
366
|
+
};
|
|
367
|
+
messagesById.set(messageId, entry);
|
|
368
|
+
messageOrder.push(messageId);
|
|
369
|
+
}
|
|
370
|
+
return entry;
|
|
371
|
+
}
|
|
372
|
+
function finalizeAssistantTexts(messageOrder, messagesById, event) {
|
|
373
|
+
const texts = messageOrder.map((messageId) => messagesById.get(messageId)?.text ?? "").filter((text) => text.length > 0);
|
|
374
|
+
if (texts.length > 0) return texts;
|
|
375
|
+
if (event?.data.content) return [event.data.content];
|
|
376
|
+
return [];
|
|
377
|
+
}
|
|
378
|
+
function isAssistantMessageEvent(event) {
|
|
379
|
+
return event?.type === "assistant.message";
|
|
380
|
+
}
|
|
381
|
+
function joinReasoning(order, reasoningById) {
|
|
382
|
+
return order.map((reasoningId) => reasoningById.get(reasoningId) ?? "").join("");
|
|
383
|
+
}
|
|
384
|
+
function readString$2(value) {
|
|
385
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
386
|
+
}
|
|
387
|
+
function registerListener(session, unsubscribeFns, eventType, handler) {
|
|
388
|
+
const maybeUnsubscribe = session.on(eventType, handler);
|
|
389
|
+
if (typeof maybeUnsubscribe === "function") {
|
|
390
|
+
unsubscribeFns.push(maybeUnsubscribe);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
unsubscribeFns.push(() => {
|
|
394
|
+
session.off?.(eventType, handler);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region extensions/copilot/src/hooks-bridge.ts
|
|
399
|
+
const DEFAULT_HOOK_ERROR_HANDLER = ({ hookName, error }) => {
|
|
400
|
+
console.warn(`[copilot hooks-bridge] ${hookName} handler threw:`, error);
|
|
401
|
+
};
|
|
402
|
+
/**
|
|
403
|
+
* Wrap a host handler in an error-isolating envelope so it cannot
|
|
404
|
+
* throw out into the SDK. Returns `undefined` (no opinion) when the
|
|
405
|
+
* host handler throws, so the SDK falls back to its default behaviour
|
|
406
|
+
* for that hook.
|
|
407
|
+
*/
|
|
408
|
+
function isolate(hookName, handler, onError) {
|
|
409
|
+
if (!handler) return;
|
|
410
|
+
return async (...args) => {
|
|
411
|
+
try {
|
|
412
|
+
return await handler(...args);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
try {
|
|
415
|
+
onError({
|
|
416
|
+
hookName,
|
|
417
|
+
error
|
|
418
|
+
});
|
|
419
|
+
} catch {}
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Build an SDK-shaped `SessionHooks` object from a host-supplied
|
|
426
|
+
* `CopilotHooksConfig`. Returns `undefined` when no handlers were
|
|
427
|
+
* supplied so the SDK skips the hook subsystem entirely.
|
|
428
|
+
*/
|
|
429
|
+
function createHooksBridge(config) {
|
|
430
|
+
if (!config) return;
|
|
431
|
+
const onError = config.onHookError ?? DEFAULT_HOOK_ERROR_HANDLER;
|
|
432
|
+
const hooks = {};
|
|
433
|
+
const pre = isolate("onPreToolUse", config.onPreToolUse, onError);
|
|
434
|
+
const post = isolate("onPostToolUse", config.onPostToolUse, onError);
|
|
435
|
+
const userPrompt = isolate("onUserPromptSubmitted", config.onUserPromptSubmitted, onError);
|
|
436
|
+
const sessionStart = isolate("onSessionStart", config.onSessionStart, onError);
|
|
437
|
+
const sessionEnd = isolate("onSessionEnd", config.onSessionEnd, onError);
|
|
438
|
+
const errorOccurred = isolate("onErrorOccurred", config.onErrorOccurred, onError);
|
|
439
|
+
if (pre) hooks.onPreToolUse = pre;
|
|
440
|
+
if (post) hooks.onPostToolUse = post;
|
|
441
|
+
if (userPrompt) hooks.onUserPromptSubmitted = userPrompt;
|
|
442
|
+
if (sessionStart) hooks.onSessionStart = sessionStart;
|
|
443
|
+
if (sessionEnd) hooks.onSessionEnd = sessionEnd;
|
|
444
|
+
if (errorOccurred) hooks.onErrorOccurred = errorOccurred;
|
|
445
|
+
if (Object.keys(hooks).length === 0) return;
|
|
446
|
+
return hooks;
|
|
447
|
+
}
|
|
448
|
+
//#endregion
|
|
449
|
+
//#region extensions/copilot/src/permission-bridge.ts
|
|
450
|
+
/** Built-in fail-closed default. Mirrors the pre-bridge attempt.ts stub. */
|
|
451
|
+
const REJECT_ALL_FEEDBACK = "copilot agent runtime: no permission policy installed (fail-closed default)";
|
|
452
|
+
const rejectAllPolicy = () => ({
|
|
453
|
+
kind: "reject",
|
|
454
|
+
feedback: REJECT_ALL_FEEDBACK
|
|
455
|
+
});
|
|
456
|
+
/**
|
|
457
|
+
* Adapt a `CopilotPermissionPolicy` to the SDK's
|
|
458
|
+
* `PermissionHandler` shape. The returned handler always resolves
|
|
459
|
+
* (never rejects), defaulting to fail-closed when the policy returns
|
|
460
|
+
* undefined or throws.
|
|
461
|
+
*/
|
|
462
|
+
function createPermissionBridge(policy = rejectAllPolicy) {
|
|
463
|
+
return async (request, invocation) => {
|
|
464
|
+
const ctx = {
|
|
465
|
+
request,
|
|
466
|
+
sessionId: invocation.sessionId
|
|
467
|
+
};
|
|
468
|
+
try {
|
|
469
|
+
const result = await policy(ctx);
|
|
470
|
+
if (result !== void 0) return result;
|
|
471
|
+
} catch (error) {
|
|
472
|
+
return {
|
|
473
|
+
kind: "reject",
|
|
474
|
+
feedback: `copilot permission policy threw: ${formatError(error)}`
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
kind: "reject",
|
|
479
|
+
feedback: REJECT_ALL_FEEDBACK
|
|
480
|
+
};
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function formatError(error) {
|
|
484
|
+
if (error instanceof Error) return error.message;
|
|
485
|
+
try {
|
|
486
|
+
return JSON.stringify(error);
|
|
487
|
+
} catch {
|
|
488
|
+
return String(error);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region extensions/copilot/src/replay-shim.ts
|
|
493
|
+
function normalizeSdkSessionId(value) {
|
|
494
|
+
if (typeof value !== "string") return;
|
|
495
|
+
const trimmed = value.trim();
|
|
496
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Pure pre-call decision: should attempt.ts call resumeSession or
|
|
500
|
+
* createSession?
|
|
501
|
+
*
|
|
502
|
+
* Rules:
|
|
503
|
+
* - No input → create (no-replay-state)
|
|
504
|
+
* - No (trimmed) sdkSessionId → create (no-sdk-session-id)
|
|
505
|
+
* - sdkSessionId + replayInvalid=true → create (replay-invalid),
|
|
506
|
+
* downgradedFromResume=true
|
|
507
|
+
* - sdkSessionId + replayInvalid=false → resume
|
|
508
|
+
*/
|
|
509
|
+
function decideReplayAction(input) {
|
|
510
|
+
if (!input) return {
|
|
511
|
+
action: "create",
|
|
512
|
+
downgradedFromResume: false,
|
|
513
|
+
downgradeReason: "no-replay-state"
|
|
514
|
+
};
|
|
515
|
+
const sdkSessionId = normalizeSdkSessionId(input.sdkSessionId);
|
|
516
|
+
if (!sdkSessionId) return {
|
|
517
|
+
action: "create",
|
|
518
|
+
downgradedFromResume: false,
|
|
519
|
+
downgradeReason: "no-sdk-session-id"
|
|
520
|
+
};
|
|
521
|
+
if (input.replayInvalid === true) return {
|
|
522
|
+
action: "create",
|
|
523
|
+
downgradedFromResume: true,
|
|
524
|
+
downgradeReason: "replay-invalid"
|
|
525
|
+
};
|
|
526
|
+
return {
|
|
527
|
+
action: "resume",
|
|
528
|
+
sdkSessionId,
|
|
529
|
+
downgradedFromResume: false
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
const MISSING_SESSION_CODES = new Set([
|
|
533
|
+
"SESSION_NOT_FOUND",
|
|
534
|
+
"session_not_found",
|
|
535
|
+
"NotFound",
|
|
536
|
+
"ENOENT"
|
|
537
|
+
]);
|
|
538
|
+
const MISSING_SESSION_MESSAGE_PATTERNS = [
|
|
539
|
+
/\bsession not found\b/i,
|
|
540
|
+
/\bsession .* not found\b/i,
|
|
541
|
+
/\bunknown session id\b/i,
|
|
542
|
+
/\bsession id .* (does not exist|not found)\b/i,
|
|
543
|
+
/\bsession .* does not exist\b/i,
|
|
544
|
+
/\bno such session\b/i
|
|
545
|
+
];
|
|
546
|
+
function readErrorField(error, key) {
|
|
547
|
+
if (!error || typeof error !== "object") return;
|
|
548
|
+
return error[key];
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Post-call: classify a resumeSession() failure so attempt.ts can
|
|
552
|
+
* decide whether to downgrade silently to createSession.
|
|
553
|
+
*
|
|
554
|
+
* Conservative: only treats clearly session-gone signals as recoverable.
|
|
555
|
+
* Structured signals (status === 404, recognised code strings) are
|
|
556
|
+
* checked first; message matching is a fallback because SDK error
|
|
557
|
+
* messages are not part of the typed contract.
|
|
558
|
+
*
|
|
559
|
+
* Everything else (transport errors, auth failures, generic Error) is
|
|
560
|
+
* unrecoverable and should surface to the outer attempt.ts try/catch
|
|
561
|
+
* which converts it to a prompt error.
|
|
562
|
+
*/
|
|
563
|
+
function classifyResumeFailure(error) {
|
|
564
|
+
if (error === void 0 || error === null) return {
|
|
565
|
+
recoverable: false,
|
|
566
|
+
kind: "unknown"
|
|
567
|
+
};
|
|
568
|
+
if (readErrorField(error, "status") === 404) return {
|
|
569
|
+
recoverable: true,
|
|
570
|
+
kind: "missing"
|
|
571
|
+
};
|
|
572
|
+
if (readErrorField(error, "statusCode") === 404) return {
|
|
573
|
+
recoverable: true,
|
|
574
|
+
kind: "missing"
|
|
575
|
+
};
|
|
576
|
+
const code = readErrorField(error, "code");
|
|
577
|
+
if (typeof code === "string" && MISSING_SESSION_CODES.has(code)) return {
|
|
578
|
+
recoverable: true,
|
|
579
|
+
kind: "missing"
|
|
580
|
+
};
|
|
581
|
+
const message = error instanceof Error ? error.message : typeof error === "object" ? typeof error.message === "string" ? error.message : void 0 : void 0;
|
|
582
|
+
if (typeof message === "string") {
|
|
583
|
+
for (const pattern of MISSING_SESSION_MESSAGE_PATTERNS) if (pattern.test(message)) return {
|
|
584
|
+
recoverable: true,
|
|
585
|
+
kind: "missing"
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
recoverable: false,
|
|
590
|
+
kind: "unknown"
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Compute the `EmbeddedRunReplayMetadata` to attach to the attempt
|
|
595
|
+
* result. Worst-case-wins:
|
|
596
|
+
*
|
|
597
|
+
* hadPotentialSideEffects = priorHadPotentialSideEffects OR timedOut
|
|
598
|
+
* OR thisAttemptHadPotentialSideEffects
|
|
599
|
+
* (timeout means we cannot prove the prompt was not partially
|
|
600
|
+
* committed server-side; treat as side-effecting so the
|
|
601
|
+
* orchestrator will not blindly re-issue the same prompt).
|
|
602
|
+
*
|
|
603
|
+
* replaySafe = NOT (
|
|
604
|
+
* priorReplayInvalid
|
|
605
|
+
* OR thisAttemptDowngradedFromResume
|
|
606
|
+
* OR thisAttemptResumeFailureRecovered
|
|
607
|
+
* OR hadPotentialSideEffects
|
|
608
|
+
* )
|
|
609
|
+
*
|
|
610
|
+
* Matches the parity rule in
|
|
611
|
+
* `src/agents/pi-embedded-runner/replay-state.ts#replayMetadataFromState`.
|
|
612
|
+
*/
|
|
613
|
+
function computeReplayMetadata(input) {
|
|
614
|
+
const priorReplayInvalid = input.priorReplayInvalid === true;
|
|
615
|
+
const priorHadPotentialSideEffects = input.priorHadPotentialSideEffects === true;
|
|
616
|
+
const timedOut = input.thisAttemptTimedOut === true;
|
|
617
|
+
const thisAttemptHadPotentialSideEffects = input.thisAttemptHadPotentialSideEffects === true;
|
|
618
|
+
const downgraded = input.thisAttemptDowngradedFromResume === true;
|
|
619
|
+
const recovered = input.thisAttemptResumeFailureRecovered === true;
|
|
620
|
+
const hadPotentialSideEffects = priorHadPotentialSideEffects || timedOut || thisAttemptHadPotentialSideEffects;
|
|
621
|
+
return {
|
|
622
|
+
hadPotentialSideEffects,
|
|
623
|
+
replaySafe: !(priorReplayInvalid || downgraded || recovered || hadPotentialSideEffects)
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
const COPILOT_REPLAY_SAFE_READ_ONLY_TOOL_NAMES = new Set([
|
|
627
|
+
"get",
|
|
628
|
+
"file_read",
|
|
629
|
+
"glob",
|
|
630
|
+
"grep",
|
|
631
|
+
"inspect",
|
|
632
|
+
"list",
|
|
633
|
+
"ls",
|
|
634
|
+
"memory_get",
|
|
635
|
+
"memory_search",
|
|
636
|
+
"probe",
|
|
637
|
+
"query",
|
|
638
|
+
"read",
|
|
639
|
+
"search",
|
|
640
|
+
"sessions_history",
|
|
641
|
+
"sessions_list",
|
|
642
|
+
"status",
|
|
643
|
+
"tool_search",
|
|
644
|
+
"update_plan",
|
|
645
|
+
"view",
|
|
646
|
+
"web_fetch",
|
|
647
|
+
"web_search"
|
|
648
|
+
]);
|
|
649
|
+
function copilotToolMetasHavePotentialSideEffects(toolMetas) {
|
|
650
|
+
return (toolMetas ?? []).some((entry) => entry.asyncStarted === true || !isReplaySafeReadOnlyToolName(entry.toolName));
|
|
651
|
+
}
|
|
652
|
+
function isReplaySafeReadOnlyToolName(toolName) {
|
|
653
|
+
const normalized = toolName.trim().toLowerCase();
|
|
654
|
+
return COPILOT_REPLAY_SAFE_READ_ONLY_TOOL_NAMES.has(normalized);
|
|
655
|
+
}
|
|
656
|
+
//#endregion
|
|
657
|
+
//#region extensions/copilot/src/tool-bridge.ts
|
|
658
|
+
const SUPPORTED_TOOL_PROVIDERS = new Set(["github-copilot"]);
|
|
659
|
+
const BASE_COPILOT_CODING_TOOL_NAMES = new Set([
|
|
660
|
+
"edit",
|
|
661
|
+
"read",
|
|
662
|
+
"write"
|
|
663
|
+
]);
|
|
664
|
+
const SHELL_COPILOT_CODING_TOOL_NAMES = new Set([
|
|
665
|
+
"apply_patch",
|
|
666
|
+
"exec",
|
|
667
|
+
"process"
|
|
668
|
+
]);
|
|
669
|
+
function supportsModelTools(modelProvider) {
|
|
670
|
+
return SUPPORTED_TOOL_PROVIDERS.has(modelProvider);
|
|
671
|
+
}
|
|
672
|
+
async function createCopilotToolBridge(input) {
|
|
673
|
+
if (!supportsModelTools(input.modelProvider)) return {
|
|
674
|
+
sdkTools: [],
|
|
675
|
+
sourceTools: []
|
|
676
|
+
};
|
|
677
|
+
const attemptParams = input.attemptParams ?? {};
|
|
678
|
+
const toolPlan = resolveEmbeddedAttemptToolConstructionPlan({
|
|
679
|
+
disableTools: attemptParams.disableTools,
|
|
680
|
+
forceMessageTool: shouldForceCopilotMessageTool(attemptParams),
|
|
681
|
+
isRawModelRun: isCopilotRawModelRun(attemptParams),
|
|
682
|
+
toolsAllow: attemptParams.toolsAllow
|
|
683
|
+
});
|
|
684
|
+
const effectiveToolPlan = hasNonWildcardGlobAllowlist(toolPlan.runtimeToolAllowlist) ? {
|
|
685
|
+
...toolPlan,
|
|
686
|
+
codingToolConstructionPlan: {
|
|
687
|
+
includeBaseCodingTools: true,
|
|
688
|
+
includeChannelTools: true,
|
|
689
|
+
includeOpenClawTools: true,
|
|
690
|
+
includePluginTools: true,
|
|
691
|
+
includeShellTools: true
|
|
692
|
+
},
|
|
693
|
+
constructTools: true,
|
|
694
|
+
includeCoreTools: true
|
|
695
|
+
} : toolPlan;
|
|
696
|
+
if (!effectiveToolPlan.constructTools) return {
|
|
697
|
+
sdkTools: [],
|
|
698
|
+
sourceTools: []
|
|
699
|
+
};
|
|
700
|
+
const createOpenClawCodingTools = input.createOpenClawCodingTools ?? (await import("openclaw/plugin-sdk/agent-harness")).createOpenClawCodingTools;
|
|
701
|
+
const toolOptions = buildOpenClawCodingToolsOptions(input, effectiveToolPlan);
|
|
702
|
+
let sourceTools;
|
|
703
|
+
try {
|
|
704
|
+
sourceTools = await createOpenClawCodingTools(toolOptions);
|
|
705
|
+
} catch (error) {
|
|
706
|
+
throw createError(`[copilot-tool-bridge] createOpenClawCodingTools failed: ${toError$1(error).message}`, error);
|
|
707
|
+
}
|
|
708
|
+
if (!Array.isArray(sourceTools)) throw new Error("[copilot-tool-bridge] createOpenClawCodingTools must return an array of tools");
|
|
709
|
+
const filteredTools = filterCopilotToolsForAllowlist(filterCopilotToolsForConstructionPlan(sourceTools, effectiveToolPlan.codingToolConstructionPlan), effectiveToolPlan.runtimeToolAllowlist);
|
|
710
|
+
const duplicateNames = findDuplicateToolNames(filteredTools);
|
|
711
|
+
if (duplicateNames.length > 0) throw new Error(`[copilot-tool-bridge] duplicate tool names: ${duplicateNames.join(", ")}`);
|
|
712
|
+
return {
|
|
713
|
+
sdkTools: filteredTools.map((sourceTool) => convertOpenClawToolToSdkTool(sourceTool, {
|
|
714
|
+
abortSignal: input.abortSignal,
|
|
715
|
+
beforeExecute: input.beforeExecute
|
|
716
|
+
})),
|
|
717
|
+
sourceTools: filteredTools
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Builds the full `createOpenClawCodingTools` options bag mirroring the
|
|
722
|
+
* PI in-tree call at `src/agents/pi-embedded-runner/run/attempt.ts:1029-1117`.
|
|
723
|
+
*
|
|
724
|
+
* Why PI parity matters: bridged OpenClaw tools register with the SDK
|
|
725
|
+
* as `overridesBuiltInTool: true, skipPermission: true` (see
|
|
726
|
+
* `convertOpenClawToolToSdkTool` below). That means the wrapped-tool
|
|
727
|
+
* enforcement layer
|
|
728
|
+
* (`src/agents/pi-tools.before-tool-call.ts → wrapToolWithBeforeToolCallHook`)
|
|
729
|
+
* is the single gate for permission, owner-only allowlists, loop
|
|
730
|
+
* detection, trusted-plugin policies, and two-phase plugin approvals.
|
|
731
|
+
* That layer reads its context from the fields forwarded here; missing
|
|
732
|
+
* fields silently degrade policy decisions. See docs/plugins/copilot.md.
|
|
733
|
+
*
|
|
734
|
+
* The shared embedded-runner tool plan is forwarded so the bridge does
|
|
735
|
+
* not construct broad tool families only to filter them later. That
|
|
736
|
+
* preserves PI allowlist semantics such as `write` not materializing
|
|
737
|
+
* `apply_patch`.
|
|
738
|
+
* Sandbox is forwarded via the explicit `sandbox` field on
|
|
739
|
+
* {@link CopilotToolBridgeInput}; callers resolve it via
|
|
740
|
+
* `resolveSandboxContext` before constructing the bridge.
|
|
741
|
+
*/
|
|
742
|
+
function buildOpenClawCodingToolsOptions(input, toolPlan) {
|
|
743
|
+
const a = input.attemptParams ?? {};
|
|
744
|
+
const sandboxSessionKey = a.sandboxSessionKey?.trim() || a.sessionKey?.trim() || input.sessionKey || input.sessionId;
|
|
745
|
+
const liveSessionKey = a.sessionKey ?? input.sessionKey;
|
|
746
|
+
const runSessionKey = liveSessionKey && liveSessionKey !== sandboxSessionKey ? liveSessionKey : void 0;
|
|
747
|
+
const workspaceDir = input.workspaceDir ?? a.workspaceDir;
|
|
748
|
+
const cwd = input.cwd ?? a.cwd;
|
|
749
|
+
const agentDir = input.agentDir ?? a.agentDir;
|
|
750
|
+
const sandbox = input.sandbox ?? void 0;
|
|
751
|
+
const spawnWorkspaceDir = input.spawnWorkspaceDir ?? (workspaceDir ? resolveAttemptSpawnWorkspaceDir({
|
|
752
|
+
sandbox,
|
|
753
|
+
resolvedWorkspace: workspaceDir
|
|
754
|
+
}) : void 0);
|
|
755
|
+
const model = a.model;
|
|
756
|
+
const modelHasVision = Array.isArray(model?.input) && model.input.includes("image");
|
|
757
|
+
const modelCompat = model && typeof model === "object" && "compat" in model && model.compat && typeof model.compat === "object" ? model.compat : void 0;
|
|
758
|
+
return {
|
|
759
|
+
agentId: input.agentId,
|
|
760
|
+
...buildEmbeddedAttemptToolRunContext({
|
|
761
|
+
trigger: a.trigger,
|
|
762
|
+
jobId: a.jobId,
|
|
763
|
+
memoryFlushWritePath: a.memoryFlushWritePath,
|
|
764
|
+
toolsAllow: a.toolsAllow
|
|
765
|
+
}),
|
|
766
|
+
exec: {
|
|
767
|
+
...a.execOverrides,
|
|
768
|
+
elevated: a.bashElevated
|
|
769
|
+
},
|
|
770
|
+
messageProvider: a.messageProvider ?? a.messageChannel,
|
|
771
|
+
agentAccountId: a.agentAccountId,
|
|
772
|
+
messageTo: a.messageTo,
|
|
773
|
+
messageThreadId: a.messageThreadId,
|
|
774
|
+
groupId: a.groupId,
|
|
775
|
+
groupChannel: a.groupChannel,
|
|
776
|
+
groupSpace: a.groupSpace,
|
|
777
|
+
memberRoleIds: a.memberRoleIds,
|
|
778
|
+
spawnedBy: a.spawnedBy,
|
|
779
|
+
senderId: a.senderId,
|
|
780
|
+
senderName: a.senderName,
|
|
781
|
+
senderUsername: a.senderUsername,
|
|
782
|
+
senderE164: a.senderE164,
|
|
783
|
+
senderIsOwner: a.senderIsOwner,
|
|
784
|
+
allowGatewaySubagentBinding: a.allowGatewaySubagentBinding,
|
|
785
|
+
sessionKey: sandboxSessionKey,
|
|
786
|
+
runSessionKey,
|
|
787
|
+
sessionId: input.sessionId,
|
|
788
|
+
runId: a.runId,
|
|
789
|
+
agentDir,
|
|
790
|
+
workspaceDir,
|
|
791
|
+
cwd,
|
|
792
|
+
sandbox,
|
|
793
|
+
spawnWorkspaceDir,
|
|
794
|
+
config: a.config,
|
|
795
|
+
abortSignal: input.abortSignal,
|
|
796
|
+
modelProvider: input.modelProvider,
|
|
797
|
+
modelId: input.modelId,
|
|
798
|
+
includeCoreTools: toolPlan.includeCoreTools,
|
|
799
|
+
runtimeToolAllowlist: toolPlan.runtimeToolAllowlist,
|
|
800
|
+
toolConstructionPlan: toolPlan.codingToolConstructionPlan,
|
|
801
|
+
modelCompat,
|
|
802
|
+
modelApi: model?.api,
|
|
803
|
+
modelContextWindowTokens: model?.contextWindow,
|
|
804
|
+
modelAuthMode: resolveModelAuthMode(input.modelProvider, a.config, void 0, { workspaceDir }),
|
|
805
|
+
currentChannelId: a.currentChannelId,
|
|
806
|
+
currentThreadTs: a.currentThreadTs,
|
|
807
|
+
currentMessageId: a.currentMessageId,
|
|
808
|
+
replyToMode: a.replyToMode,
|
|
809
|
+
hasRepliedRef: a.hasRepliedRef,
|
|
810
|
+
modelHasVision,
|
|
811
|
+
requireExplicitMessageTarget: a.requireExplicitMessageTarget ?? isSubagentSessionKey(liveSessionKey),
|
|
812
|
+
sourceReplyDeliveryMode: a.sourceReplyDeliveryMode,
|
|
813
|
+
disableMessageTool: a.disableMessageTool,
|
|
814
|
+
forceMessageTool: a.forceMessageTool,
|
|
815
|
+
enableHeartbeatTool: a.enableHeartbeatTool,
|
|
816
|
+
forceHeartbeatTool: a.forceHeartbeatTool,
|
|
817
|
+
authProfileStore: a.toolAuthProfileStore ?? a.authProfileStore,
|
|
818
|
+
onToolOutcome: a.onToolOutcome,
|
|
819
|
+
onYield: (message) => {
|
|
820
|
+
try {
|
|
821
|
+
input.onYieldDetected?.(message);
|
|
822
|
+
} catch (error) {
|
|
823
|
+
console.warn("[copilot-tool-bridge] onYieldDetected handler threw; continuing", error);
|
|
824
|
+
}
|
|
825
|
+
(input.sessionRef?.current)?.abort?.();
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
function convertOpenClawToolToSdkTool(sourceTool, ctx) {
|
|
830
|
+
if (typeof sourceTool.name !== "string" || sourceTool.name.trim().length === 0) throw new Error("[copilot-tool-bridge] tool name must be a non-empty string");
|
|
831
|
+
if (typeof sourceTool.execute !== "function") throw new Error(`[copilot-tool-bridge] tool '${sourceTool.name}' must define an execute function`);
|
|
832
|
+
let sequentialLock = Promise.resolve();
|
|
833
|
+
const executeOnce = async (args, invocation) => {
|
|
834
|
+
if (ctx.abortSignal?.aborted) {
|
|
835
|
+
const error = /* @__PURE__ */ new Error("[copilot-tool-bridge] aborted before execution");
|
|
836
|
+
return createFailureResult(error.message, error);
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
await ctx.beforeExecute?.({
|
|
840
|
+
args,
|
|
841
|
+
invocation,
|
|
842
|
+
sourceTool,
|
|
843
|
+
toolCallId: invocation.toolCallId,
|
|
844
|
+
toolName: sourceTool.name
|
|
845
|
+
});
|
|
846
|
+
} catch (error) {
|
|
847
|
+
return createFailureResult(`[copilot-tool-bridge] beforeExecute failed for tool '${sourceTool.name}': ${toError$1(error).message}`, error);
|
|
848
|
+
}
|
|
849
|
+
let preparedArgs = args;
|
|
850
|
+
try {
|
|
851
|
+
preparedArgs = sourceTool.prepareArguments ? sourceTool.prepareArguments(args) : args;
|
|
852
|
+
} catch (error) {
|
|
853
|
+
return createFailureResult(`[copilot-tool-bridge] prepareArguments failed for tool '${sourceTool.name}': ${toError$1(error).message}`, error);
|
|
854
|
+
}
|
|
855
|
+
let result;
|
|
856
|
+
try {
|
|
857
|
+
result = await sourceTool.execute(invocation.toolCallId, preparedArgs, ctx.abortSignal, void 0);
|
|
858
|
+
} catch (error) {
|
|
859
|
+
return createFailureResult(`[copilot-tool-bridge] tool '${sourceTool.name}' failed: ${toError$1(error).message}`, error);
|
|
860
|
+
}
|
|
861
|
+
return agentToolResultToSdk(result);
|
|
862
|
+
};
|
|
863
|
+
const handler = sourceTool.executionMode === "sequential" ? (args, invocation) => {
|
|
864
|
+
const run = sequentialLock.then(() => executeOnce(args, invocation), () => executeOnce(args, invocation));
|
|
865
|
+
sequentialLock = run.then(() => void 0, () => void 0);
|
|
866
|
+
return run;
|
|
867
|
+
} : executeOnce;
|
|
868
|
+
return {
|
|
869
|
+
description: sourceTool.description,
|
|
870
|
+
handler,
|
|
871
|
+
name: sourceTool.name,
|
|
872
|
+
overridesBuiltInTool: true,
|
|
873
|
+
parameters: sourceTool.parameters,
|
|
874
|
+
skipPermission: true
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
function agentToolResultToSdk(result) {
|
|
878
|
+
const content = result?.content;
|
|
879
|
+
if (content == null) return createSuccessResult("");
|
|
880
|
+
if (!Array.isArray(content)) return createUnsupportedContentFailure(typeof content);
|
|
881
|
+
const textParts = [];
|
|
882
|
+
const binaryResults = [];
|
|
883
|
+
for (const block of content) {
|
|
884
|
+
if (!block || typeof block !== "object") return createUnsupportedContentFailure(typeof block);
|
|
885
|
+
const kind = readString$1(block.type);
|
|
886
|
+
if (kind === "text") {
|
|
887
|
+
const text = readString$1(block.text, { allowEmpty: true });
|
|
888
|
+
if (text === void 0) return createUnsupportedContentFailure(kind);
|
|
889
|
+
textParts.push(text);
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (kind === "image") {
|
|
893
|
+
const base64Data = readString$1(block.data);
|
|
894
|
+
const mimeType = readString$1(block.mimeType);
|
|
895
|
+
if (!base64Data || !mimeType) return createUnsupportedContentFailure(kind);
|
|
896
|
+
binaryResults.push({
|
|
897
|
+
base64Data,
|
|
898
|
+
data: base64Data,
|
|
899
|
+
mimeType,
|
|
900
|
+
type: "image"
|
|
901
|
+
});
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
return createUnsupportedContentFailure(kind ?? typeof block);
|
|
905
|
+
}
|
|
906
|
+
return {
|
|
907
|
+
...binaryResults.length > 0 ? { binaryResultsForLlm: binaryResults } : {},
|
|
908
|
+
resultType: "success",
|
|
909
|
+
textResultForLlm: textParts.join("\n")
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
function createUnsupportedContentFailure(kind) {
|
|
913
|
+
const message = `[copilot-tool-bridge] unsupported AgentToolResult content shape: ${kind}`;
|
|
914
|
+
return createFailureResult(message, new Error(message));
|
|
915
|
+
}
|
|
916
|
+
function createSuccessResult(textResultForLlm) {
|
|
917
|
+
return {
|
|
918
|
+
resultType: "success",
|
|
919
|
+
textResultForLlm
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
function createFailureResult(message, error) {
|
|
923
|
+
return {
|
|
924
|
+
error: toError$1(error).message,
|
|
925
|
+
resultType: "failure",
|
|
926
|
+
textResultForLlm: message
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function createError(message, cause) {
|
|
930
|
+
const error = new Error(message);
|
|
931
|
+
error.cause = cause;
|
|
932
|
+
return error;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Returns true when the attempt was launched as a raw-model run, which
|
|
936
|
+
* suppresses tool construction in PI
|
|
937
|
+
* (`src/agents/pi-embedded-runner/run/attempt.ts:1305-1310` and
|
|
938
|
+
* `attempt-tool-construction-plan.ts:165-184`). A run is raw when the
|
|
939
|
+
* caller explicitly sets `modelRun: true` or asks for no system prompt
|
|
940
|
+
* via `promptMode: "none"`.
|
|
941
|
+
*/
|
|
942
|
+
function isCopilotRawModelRun(params) {
|
|
943
|
+
return params.modelRun === true || params.promptMode === "none";
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Mirrors PI's `shouldForceMessageTool` semantics: a message tool is
|
|
947
|
+
* forced when the caller asked for it explicitly or when the source
|
|
948
|
+
* reply delivery mode is `message_tool_only`, but never when
|
|
949
|
+
* `disableMessageTool` is set (the suppress flag always wins). Compare
|
|
950
|
+
* `src/agents/pi-embedded-runner/run/attempt.ts:1361-1366` and the
|
|
951
|
+
* codex equivalent at
|
|
952
|
+
* `extensions/codex/src/app-server/run-attempt.ts:4253-4258`.
|
|
953
|
+
*/
|
|
954
|
+
function shouldForceCopilotMessageTool(params) {
|
|
955
|
+
if (params.disableMessageTool === true) return false;
|
|
956
|
+
return params.forceMessageTool === true || params.sourceReplyDeliveryMode === "message_tool_only";
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Mirrors PI's `applyEmbeddedAttemptToolsAllow`
|
|
960
|
+
* (`src/agents/embedded-agent-runner/run/attempt-tool-construction-plan.ts`)
|
|
961
|
+
* so final filtering keeps aliases, groups, plugin policies, and glob
|
|
962
|
+
* semantics identical to the in-tree embedded runner.
|
|
963
|
+
*/
|
|
964
|
+
function filterCopilotToolsForAllowlist(tools, toolsAllow) {
|
|
965
|
+
return applyEmbeddedAttemptToolsAllow(tools, toolsAllow, { toolMeta: (tool) => getPluginToolMeta(tool) ?? readInlinePluginToolMeta(tool) });
|
|
966
|
+
}
|
|
967
|
+
function filterCopilotToolsForConstructionPlan(tools, plan) {
|
|
968
|
+
if (plan.includeBaseCodingTools && plan.includeShellTools) return tools;
|
|
969
|
+
return tools.filter((tool) => {
|
|
970
|
+
if (!plan.includeBaseCodingTools && BASE_COPILOT_CODING_TOOL_NAMES.has(tool.name)) return false;
|
|
971
|
+
if (!plan.includeShellTools && SHELL_COPILOT_CODING_TOOL_NAMES.has(tool.name)) return false;
|
|
972
|
+
return true;
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
function hasNonWildcardGlobAllowlist(toolsAllow) {
|
|
976
|
+
return (toolsAllow ?? []).some((entry) => {
|
|
977
|
+
const trimmed = entry.trim();
|
|
978
|
+
return trimmed !== "*" && trimmed.includes("*");
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
function readInlinePluginToolMeta(tool) {
|
|
982
|
+
const pluginId = tool.pluginId;
|
|
983
|
+
return typeof pluginId === "string" && pluginId.trim() ? { pluginId } : void 0;
|
|
984
|
+
}
|
|
985
|
+
function findDuplicateToolNames(sourceTools) {
|
|
986
|
+
const counts = /* @__PURE__ */ new Map();
|
|
987
|
+
for (const sourceTool of sourceTools) {
|
|
988
|
+
if (typeof sourceTool.name !== "string" || sourceTool.name.length === 0) continue;
|
|
989
|
+
counts.set(sourceTool.name, (counts.get(sourceTool.name) ?? 0) + 1);
|
|
990
|
+
}
|
|
991
|
+
return [...counts.entries()].filter(([, count]) => count > 1).map(([name]) => name).toSorted();
|
|
992
|
+
}
|
|
993
|
+
function readString$1(value, options = {}) {
|
|
994
|
+
if (typeof value !== "string") return;
|
|
995
|
+
if (options.allowEmpty || value.length > 0) return value;
|
|
996
|
+
}
|
|
997
|
+
function toError$1(error) {
|
|
998
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
999
|
+
}
|
|
1000
|
+
//#endregion
|
|
1001
|
+
//#region extensions/copilot/src/workspace-bootstrap.ts
|
|
1002
|
+
const COPILOT_NATIVE_PROJECT_DOC_BASENAMES = new Set(["agents.md"]);
|
|
1003
|
+
const COPILOT_BOOTSTRAP_CONTEXT_ORDER = new Map([
|
|
1004
|
+
["soul.md", 10],
|
|
1005
|
+
["identity.md", 20],
|
|
1006
|
+
["heartbeat.md", 30],
|
|
1007
|
+
["bootstrap.md", 40],
|
|
1008
|
+
["tools.md", 50],
|
|
1009
|
+
["user.md", 60],
|
|
1010
|
+
["memory.md", 70]
|
|
1011
|
+
]);
|
|
1012
|
+
/**
|
|
1013
|
+
* Loads OpenClaw workspace bootstrap files (IDENTITY.md, SOUL.md,
|
|
1014
|
+
* HEARTBEAT.md, USER.md, TOOLS.md, BOOTSTRAP.md, MEMORY.md, ...) using
|
|
1015
|
+
* the shared core helper PI and codex both use, then renders them as a
|
|
1016
|
+
* single string suitable for `SessionConfig.systemMessage.content` on
|
|
1017
|
+
* the Copilot SDK.
|
|
1018
|
+
*
|
|
1019
|
+
* Returns `instructions: undefined` when there are no relevant files
|
|
1020
|
+
* (after filtering out SDK-native docs) so the caller can omit the
|
|
1021
|
+
* `systemMessage` field entirely rather than passing an empty string.
|
|
1022
|
+
*
|
|
1023
|
+
* Mirrors codex's `buildCodexWorkspaceBootstrapContext` /
|
|
1024
|
+
* `renderCodexWorkspaceBootstrapInstructions` pair
|
|
1025
|
+
* (`extensions/codex/src/app-server/run-attempt.ts:2877,3047`). The
|
|
1026
|
+
* shape divergence — codex returns instructions inside the same object
|
|
1027
|
+
* as bootstrapFiles+contextFiles for its developerInstructions field;
|
|
1028
|
+
* copilot exposes the rendered string for SDK `systemMessage` — is the
|
|
1029
|
+
* intended difference between the two runtimes' system-prompt
|
|
1030
|
+
* surfaces.
|
|
1031
|
+
*/
|
|
1032
|
+
async function resolveCopilotWorkspaceBootstrapContext(params) {
|
|
1033
|
+
const { attempt } = params;
|
|
1034
|
+
const workspaceDir = readResolvedWorkspacePath(attempt.workspaceDir);
|
|
1035
|
+
if (!workspaceDir) return {
|
|
1036
|
+
bootstrapFiles: [],
|
|
1037
|
+
contextFiles: []
|
|
1038
|
+
};
|
|
1039
|
+
try {
|
|
1040
|
+
const bootstrapContext = await resolveBootstrapContextForRun({
|
|
1041
|
+
workspaceDir,
|
|
1042
|
+
config: attempt.config,
|
|
1043
|
+
sessionKey: readNonEmptyString(attempt.sessionKey),
|
|
1044
|
+
sessionId: readNonEmptyString(attempt.sessionId),
|
|
1045
|
+
agentId: readNonEmptyString(attempt.agentId),
|
|
1046
|
+
warn: params.warn,
|
|
1047
|
+
contextMode: attempt.bootstrapContextMode,
|
|
1048
|
+
runKind: attempt.bootstrapContextRunKind
|
|
1049
|
+
});
|
|
1050
|
+
const contextFiles = remapCopilotBootstrapContextFiles({
|
|
1051
|
+
files: bootstrapContext.contextFiles,
|
|
1052
|
+
sourceWorkspaceDir: workspaceDir,
|
|
1053
|
+
targetWorkspaceDir: readResolvedWorkspacePath(params.effectiveWorkspaceDir) ?? workspaceDir
|
|
1054
|
+
});
|
|
1055
|
+
return {
|
|
1056
|
+
bootstrapFiles: bootstrapContext.bootstrapFiles,
|
|
1057
|
+
contextFiles,
|
|
1058
|
+
instructions: renderCopilotWorkspaceBootstrapInstructions(contextFiles)
|
|
1059
|
+
};
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
params.warn?.(`[copilot-attempt] failed to load workspace bootstrap instructions: ${error instanceof Error ? error.message : String(error)}`);
|
|
1062
|
+
return {
|
|
1063
|
+
bootstrapFiles: [],
|
|
1064
|
+
contextFiles: []
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Rewrites context-file paths from a source workspace root to a
|
|
1070
|
+
* target workspace root, mirroring PI's
|
|
1071
|
+
* `remapInjectedContextFilesToWorkspace`
|
|
1072
|
+
* (`src/agents/pi-embedded-runner/run/attempt.ts:603`). Files whose
|
|
1073
|
+
* resolved relative path escapes the source workspace (parent
|
|
1074
|
+
* traversal or absolute) are left untouched so we never pretend a
|
|
1075
|
+
* file lives inside the sandbox when it does not. Exported for unit
|
|
1076
|
+
* tests; intentionally local to the Copilot extension (codex keeps
|
|
1077
|
+
* similar helpers extension-local rather than importing from PI).
|
|
1078
|
+
*/
|
|
1079
|
+
function remapCopilotBootstrapContextFiles(params) {
|
|
1080
|
+
if (params.sourceWorkspaceDir === params.targetWorkspaceDir) return params.files;
|
|
1081
|
+
return params.files.map((file) => {
|
|
1082
|
+
const relative = path.relative(params.sourceWorkspaceDir, file.path);
|
|
1083
|
+
if (!isRelativePathInsideOrEqual(relative)) return file;
|
|
1084
|
+
return {
|
|
1085
|
+
...file,
|
|
1086
|
+
path: relative === "" ? params.targetWorkspaceDir : path.join(params.targetWorkspaceDir, relative)
|
|
1087
|
+
};
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
function isRelativePathInsideOrEqual(relativePath) {
|
|
1091
|
+
return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${path.sep}`) && !path.isAbsolute(relativePath);
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Renders bootstrap context files into a single string for
|
|
1095
|
+
* `SessionConfig.systemMessage.content` (append mode). Returns
|
|
1096
|
+
* `undefined` when no relevant files remain after filtering, so the
|
|
1097
|
+
* caller can skip setting `systemMessage` altogether.
|
|
1098
|
+
*
|
|
1099
|
+
* Files whose basename matches a doc the Copilot SDK already loads
|
|
1100
|
+
* natively (see {@link COPILOT_NATIVE_PROJECT_DOC_BASENAMES}) are
|
|
1101
|
+
* dropped to avoid duplication with SDK-managed sections.
|
|
1102
|
+
*/
|
|
1103
|
+
function renderCopilotWorkspaceBootstrapInstructions(contextFiles) {
|
|
1104
|
+
const files = contextFiles.filter((file) => {
|
|
1105
|
+
const baseName = getCopilotContextFileBasename(file.path);
|
|
1106
|
+
return baseName.length > 0 && !COPILOT_NATIVE_PROJECT_DOC_BASENAMES.has(baseName);
|
|
1107
|
+
}).toSorted(compareCopilotContextFiles);
|
|
1108
|
+
if (files.length === 0) return;
|
|
1109
|
+
const hasSoulFile = files.some((file) => getCopilotContextFileBasename(file.path) === "soul.md");
|
|
1110
|
+
const lines = [
|
|
1111
|
+
"OpenClaw loaded these user-editable workspace files. Treat them as project/user context. The Copilot SDK loads AGENTS.md natively from its instruction directories, so AGENTS.md is not repeated here.",
|
|
1112
|
+
"",
|
|
1113
|
+
"# Project Context",
|
|
1114
|
+
"",
|
|
1115
|
+
"The following project context files have been loaded:"
|
|
1116
|
+
];
|
|
1117
|
+
if (hasSoulFile) lines.push("SOUL.md: persona/tone. Follow it unless higher-priority instructions override.");
|
|
1118
|
+
lines.push("");
|
|
1119
|
+
for (const file of files) lines.push(`## ${file.path}`, "", file.content, "");
|
|
1120
|
+
return lines.join("\n").trim();
|
|
1121
|
+
}
|
|
1122
|
+
function compareCopilotContextFiles(left, right) {
|
|
1123
|
+
const leftBase = getCopilotContextFileBasename(left.path);
|
|
1124
|
+
const rightBase = getCopilotContextFileBasename(right.path);
|
|
1125
|
+
const leftOrder = COPILOT_BOOTSTRAP_CONTEXT_ORDER.get(leftBase) ?? Number.MAX_SAFE_INTEGER;
|
|
1126
|
+
const rightOrder = COPILOT_BOOTSTRAP_CONTEXT_ORDER.get(rightBase) ?? Number.MAX_SAFE_INTEGER;
|
|
1127
|
+
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
|
|
1128
|
+
const leftPath = normalizeCopilotContextFilePath(left.path);
|
|
1129
|
+
const rightPath = normalizeCopilotContextFilePath(right.path);
|
|
1130
|
+
if (leftPath < rightPath) return -1;
|
|
1131
|
+
if (leftPath > rightPath) return 1;
|
|
1132
|
+
return 0;
|
|
1133
|
+
}
|
|
1134
|
+
function normalizeCopilotContextFilePath(filePath) {
|
|
1135
|
+
return filePath.trim().replaceAll("\\", "/").toLowerCase();
|
|
1136
|
+
}
|
|
1137
|
+
function getCopilotContextFileBasename(filePath) {
|
|
1138
|
+
return normalizeCopilotContextFilePath(filePath).split("/").pop() ?? "";
|
|
1139
|
+
}
|
|
1140
|
+
function readNonEmptyString(value) {
|
|
1141
|
+
return typeof value === "string" && value.trim().length > 0 ? value : void 0;
|
|
1142
|
+
}
|
|
1143
|
+
function readResolvedWorkspacePath(value) {
|
|
1144
|
+
const raw = readNonEmptyString(value);
|
|
1145
|
+
if (!raw) return;
|
|
1146
|
+
if (process.platform !== "win32" && /^[A-Za-z]:[\\/]/.test(raw)) return raw.trim();
|
|
1147
|
+
return resolveUserPath(raw);
|
|
1148
|
+
}
|
|
1149
|
+
//#endregion
|
|
1150
|
+
//#region extensions/copilot/src/attempt.ts
|
|
1151
|
+
const SUPPORTED_PROVIDERS = new Set(["github-copilot"]);
|
|
1152
|
+
async function runCopilotAttempt(params, deps) {
|
|
1153
|
+
const now = deps.now ?? Date.now;
|
|
1154
|
+
const input = params;
|
|
1155
|
+
const createToolBridge = deps.createToolBridge ?? createCopilotToolBridge;
|
|
1156
|
+
const messages = getMessagesSnapshotInput(input);
|
|
1157
|
+
if (params.abortSignal?.aborted) return createResult(input, {
|
|
1158
|
+
aborted: true,
|
|
1159
|
+
externalAbort: true,
|
|
1160
|
+
messagesSnapshot: messages,
|
|
1161
|
+
now,
|
|
1162
|
+
promptError: void 0,
|
|
1163
|
+
sdkSessionId: void 0,
|
|
1164
|
+
sessionIdUsed: input.sessionId
|
|
1165
|
+
});
|
|
1166
|
+
const modelRef = resolveModelRef(input);
|
|
1167
|
+
if (!SUPPORTED_PROVIDERS.has(modelRef.provider)) return createResult(input, {
|
|
1168
|
+
messagesSnapshot: messages,
|
|
1169
|
+
now,
|
|
1170
|
+
promptError: createPromptError("model_not_supported", `[copilot-attempt] provider ${modelRef.provider} is not supported at MVP (subscription Copilot models only; BYOK arrives via byok-mapping-skeleton)`),
|
|
1171
|
+
sdkSessionId: void 0,
|
|
1172
|
+
sessionIdUsed: input.sessionId
|
|
1173
|
+
});
|
|
1174
|
+
let abortRequested = false;
|
|
1175
|
+
let aborted = false;
|
|
1176
|
+
let externalAbort = false;
|
|
1177
|
+
let settled = false;
|
|
1178
|
+
let sentTurnStarted = false;
|
|
1179
|
+
let timedOut = false;
|
|
1180
|
+
let promptError;
|
|
1181
|
+
let sdkSessionId;
|
|
1182
|
+
let sessionIdUsed = input.sessionId;
|
|
1183
|
+
let disconnectError;
|
|
1184
|
+
let handle;
|
|
1185
|
+
let session;
|
|
1186
|
+
let bridge;
|
|
1187
|
+
let releaseError;
|
|
1188
|
+
let downgradedFromResume = false;
|
|
1189
|
+
let resumeFailureRecovered = false;
|
|
1190
|
+
let yieldDetected = false;
|
|
1191
|
+
const onAbort = () => {
|
|
1192
|
+
abortRequested = true;
|
|
1193
|
+
externalAbort = true;
|
|
1194
|
+
aborted = true;
|
|
1195
|
+
if (settled || !sentTurnStarted || !session) return;
|
|
1196
|
+
session.abort().catch(() => void 0);
|
|
1197
|
+
};
|
|
1198
|
+
params.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
1199
|
+
const resolvedWorkspaceForSandbox = readResolvedAttemptPath(input.workspaceDir) ?? readResolvedAttemptPath(input.cwd);
|
|
1200
|
+
const sandboxSessionKey = readString(input.sandboxSessionKey) ?? readString(input.sessionKey) ?? readString(input.sessionId);
|
|
1201
|
+
const resolveSandbox = deps.resolveSandboxContextOverride ?? resolveSandboxContext;
|
|
1202
|
+
let sandbox = null;
|
|
1203
|
+
let effectiveWorkspaceDir = resolvedWorkspaceForSandbox;
|
|
1204
|
+
if (resolvedWorkspaceForSandbox) try {
|
|
1205
|
+
sandbox = await resolveSandbox({
|
|
1206
|
+
config: input.config,
|
|
1207
|
+
sessionKey: sandboxSessionKey,
|
|
1208
|
+
workspaceDir: resolvedWorkspaceForSandbox
|
|
1209
|
+
});
|
|
1210
|
+
effectiveWorkspaceDir = sandbox?.enabled ? sandbox.workspaceAccess === "rw" ? resolvedWorkspaceForSandbox : sandbox.workspaceDir : resolvedWorkspaceForSandbox;
|
|
1211
|
+
if (sandbox?.enabled && effectiveWorkspaceDir && effectiveWorkspaceDir !== resolvedWorkspaceForSandbox) await fsp.mkdir(effectiveWorkspaceDir, { recursive: true });
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
settled = true;
|
|
1214
|
+
params.abortSignal?.removeEventListener("abort", onAbort);
|
|
1215
|
+
if (abortRequested || params.abortSignal?.aborted) return createResult(input, {
|
|
1216
|
+
aborted: true,
|
|
1217
|
+
externalAbort: true,
|
|
1218
|
+
messagesSnapshot: messages,
|
|
1219
|
+
now,
|
|
1220
|
+
promptError: void 0,
|
|
1221
|
+
sdkSessionId: void 0,
|
|
1222
|
+
sessionIdUsed: input.sessionId
|
|
1223
|
+
});
|
|
1224
|
+
return createResult(input, {
|
|
1225
|
+
messagesSnapshot: messages,
|
|
1226
|
+
now,
|
|
1227
|
+
promptError: createPromptError("sandbox_resolution_failure", `[copilot-attempt] sandbox resolution failed: ${toError(error).message}`, error),
|
|
1228
|
+
sdkSessionId: void 0,
|
|
1229
|
+
sessionIdUsed: input.sessionId
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
const requestedCwd = readResolvedAttemptPath(input.cwd);
|
|
1233
|
+
if (sandbox?.enabled && requestedCwd && requestedCwd !== resolvedWorkspaceForSandbox) {
|
|
1234
|
+
settled = true;
|
|
1235
|
+
params.abortSignal?.removeEventListener("abort", onAbort);
|
|
1236
|
+
return createResult(input, {
|
|
1237
|
+
messagesSnapshot: messages,
|
|
1238
|
+
now,
|
|
1239
|
+
promptError: createPromptError("sandbox_cwd_override_unsupported", "[copilot-attempt] cwd override is not supported for sandboxed Copilot runs; omit cwd or use the agent workspace as cwd"),
|
|
1240
|
+
sdkSessionId: void 0,
|
|
1241
|
+
sessionIdUsed: input.sessionId
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
const effectiveCwd = sandbox?.enabled ? effectiveWorkspaceDir : requestedCwd ?? effectiveWorkspaceDir;
|
|
1245
|
+
const { sessionAgentId } = resolveSessionAgentIds({
|
|
1246
|
+
sessionKey: readString(input.sessionKey),
|
|
1247
|
+
config: input.config,
|
|
1248
|
+
agentId: readString(params.agentId)
|
|
1249
|
+
});
|
|
1250
|
+
const effectiveFsWorkspaceOnly = resolveAttemptFsWorkspaceOnly({
|
|
1251
|
+
config: input.config,
|
|
1252
|
+
sessionAgentId
|
|
1253
|
+
});
|
|
1254
|
+
const sandboxAwareSpawnWorkspaceDir = resolvedWorkspaceForSandbox ? resolveAttemptSpawnWorkspaceDir({
|
|
1255
|
+
sandbox,
|
|
1256
|
+
resolvedWorkspace: resolvedWorkspaceForSandbox
|
|
1257
|
+
}) : void 0;
|
|
1258
|
+
const poolAcquire = resolvePoolAcquire(input);
|
|
1259
|
+
const sessionRef = { current: void 0 };
|
|
1260
|
+
try {
|
|
1261
|
+
let sdkTools;
|
|
1262
|
+
try {
|
|
1263
|
+
sdkTools = (await createToolBridge({
|
|
1264
|
+
modelProvider: modelRef.provider,
|
|
1265
|
+
modelId: modelRef.id,
|
|
1266
|
+
agentId: readString(params.agentId) ?? "copilot",
|
|
1267
|
+
sessionId: readString(input.sessionId) ?? "copilot-session",
|
|
1268
|
+
sessionKey: readString(input.sessionKey),
|
|
1269
|
+
agentDir: readString(input.agentDir),
|
|
1270
|
+
workspaceDir: effectiveWorkspaceDir,
|
|
1271
|
+
cwd: effectiveCwd,
|
|
1272
|
+
sandbox,
|
|
1273
|
+
spawnWorkspaceDir: sandboxAwareSpawnWorkspaceDir,
|
|
1274
|
+
abortSignal: params.abortSignal,
|
|
1275
|
+
attemptParams: input,
|
|
1276
|
+
sessionRef,
|
|
1277
|
+
onYieldDetected: () => {
|
|
1278
|
+
yieldDetected = true;
|
|
1279
|
+
}
|
|
1280
|
+
})).sdkTools;
|
|
1281
|
+
} catch (error) {
|
|
1282
|
+
return createResult(input, {
|
|
1283
|
+
messagesSnapshot: messages,
|
|
1284
|
+
now,
|
|
1285
|
+
promptError: createPromptError("tool_bridge_failure", `[copilot-attempt] tool-bridge construction failed: ${toError(error).message}`, error),
|
|
1286
|
+
sdkSessionId: void 0,
|
|
1287
|
+
sessionIdUsed: input.sessionId
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
handle = await deps.pool.acquire(poolAcquire.key, poolAcquire.options);
|
|
1291
|
+
const client = handle.client;
|
|
1292
|
+
const workspaceBootstrap = await resolveCopilotWorkspaceBootstrapContext({
|
|
1293
|
+
attempt: input,
|
|
1294
|
+
effectiveWorkspaceDir,
|
|
1295
|
+
warn: (message) => console.warn(message)
|
|
1296
|
+
});
|
|
1297
|
+
const sessionConfig = createSessionConfig(input, modelRef.id, sdkTools, poolAcquire.auth, workspaceBootstrap.instructions, effectiveWorkspaceDir, effectiveCwd);
|
|
1298
|
+
const replayDecision = decideReplayAction({
|
|
1299
|
+
sdkSessionId: input.initialReplayState?.sdkSessionId,
|
|
1300
|
+
replayInvalid: input.initialReplayState?.replayInvalid
|
|
1301
|
+
});
|
|
1302
|
+
downgradedFromResume = replayDecision.downgradedFromResume;
|
|
1303
|
+
const resumeSessionId = replayDecision.action === "resume" ? replayDecision.sdkSessionId : void 0;
|
|
1304
|
+
if (resumeSessionId) try {
|
|
1305
|
+
session = await client.resumeSession(resumeSessionId, {
|
|
1306
|
+
...sessionConfig,
|
|
1307
|
+
continuePendingWork: false
|
|
1308
|
+
});
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
if (!classifyResumeFailure(error).recoverable) throw error;
|
|
1311
|
+
resumeFailureRecovered = true;
|
|
1312
|
+
session = await client.createSession(sessionConfig);
|
|
1313
|
+
}
|
|
1314
|
+
else session = await client.createSession(sessionConfig);
|
|
1315
|
+
sessionRef.current = session;
|
|
1316
|
+
sdkSessionId = readSessionId(session) ?? (resumeFailureRecovered ? void 0 : resumeSessionId);
|
|
1317
|
+
sessionIdUsed = sdkSessionId ?? input.sessionId;
|
|
1318
|
+
if (sdkSessionId && deps.onSessionEstablished) try {
|
|
1319
|
+
deps.onSessionEstablished({
|
|
1320
|
+
sdkSessionId,
|
|
1321
|
+
pooledClient: handle
|
|
1322
|
+
});
|
|
1323
|
+
} catch {}
|
|
1324
|
+
bridge = attachEventBridge(session, {
|
|
1325
|
+
onAssistantDelta: input.onAssistantDelta,
|
|
1326
|
+
getSdkSessionId: () => sdkSessionId,
|
|
1327
|
+
isAborted: () => aborted
|
|
1328
|
+
});
|
|
1329
|
+
const messageOptions = await createMessageOptions(input, {
|
|
1330
|
+
effectiveCwd,
|
|
1331
|
+
effectiveWorkspaceDir,
|
|
1332
|
+
sandbox,
|
|
1333
|
+
workspaceOnly: effectiveFsWorkspaceOnly
|
|
1334
|
+
});
|
|
1335
|
+
if (abortRequested || params.abortSignal?.aborted) {
|
|
1336
|
+
aborted = true;
|
|
1337
|
+
externalAbort = true;
|
|
1338
|
+
} else {
|
|
1339
|
+
sentTurnStarted = true;
|
|
1340
|
+
const result = await session.sendAndWait(messageOptions, input.timeoutMs);
|
|
1341
|
+
await bridge.awaitDeltaChain();
|
|
1342
|
+
if (!bridge.recordSendResult(result) && !aborted) timedOut = true;
|
|
1343
|
+
const snap = bridge.snapshot();
|
|
1344
|
+
if (!promptError && !timedOut && !aborted && snap.streamError) promptError = snap.streamError;
|
|
1345
|
+
}
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
if (!aborted) if (isSdkSendAndWaitTimeoutError(error)) {
|
|
1348
|
+
timedOut = true;
|
|
1349
|
+
try {
|
|
1350
|
+
await bridge?.awaitDeltaChain();
|
|
1351
|
+
} catch {}
|
|
1352
|
+
} else promptError = toError(error);
|
|
1353
|
+
} finally {
|
|
1354
|
+
settled = true;
|
|
1355
|
+
bridge?.detach();
|
|
1356
|
+
params.abortSignal?.removeEventListener("abort", onAbort);
|
|
1357
|
+
if (session) try {
|
|
1358
|
+
await session.disconnect();
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
disconnectError = toError(error);
|
|
1361
|
+
if (!promptError && !timedOut) promptError = disconnectError;
|
|
1362
|
+
}
|
|
1363
|
+
if (handle) try {
|
|
1364
|
+
await deps.pool.release(handle);
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
const releaseFailure = toError(error);
|
|
1367
|
+
if (promptError) console.warn("[copilot-attempt] pool.release failed after primary error", releaseFailure);
|
|
1368
|
+
else releaseError = releaseFailure;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
if (releaseError) throw releaseError;
|
|
1372
|
+
const snap = bridge?.snapshot();
|
|
1373
|
+
const assistantTexts = bridge?.finalizeAssistantTexts() ?? [];
|
|
1374
|
+
const lastAssistant = bridge?.buildAssistantMessage({
|
|
1375
|
+
modelRef,
|
|
1376
|
+
now
|
|
1377
|
+
});
|
|
1378
|
+
const syntheticUserText = readString(input.transcriptPrompt) ?? readString(input.prompt);
|
|
1379
|
+
const tailUserText = readTailUserText(messages);
|
|
1380
|
+
const syntheticUser = syntheticUserText && syntheticUserText !== tailUserText ? attachCopilotMirrorIdentity({
|
|
1381
|
+
role: "user",
|
|
1382
|
+
content: syntheticUserText,
|
|
1383
|
+
timestamp: now()
|
|
1384
|
+
}, `${input.runId}:prompt`) : void 0;
|
|
1385
|
+
const taggedLastAssistant = lastAssistant ? attachCopilotMirrorIdentity(lastAssistant, `${input.runId}:assistant:final`) : void 0;
|
|
1386
|
+
const messagesSnapshot = [
|
|
1387
|
+
...messages,
|
|
1388
|
+
...syntheticUser ? [syntheticUser] : [],
|
|
1389
|
+
...taggedLastAssistant ? [taggedLastAssistant] : []
|
|
1390
|
+
];
|
|
1391
|
+
const sessionFileForMirror = readString(input.sessionFile);
|
|
1392
|
+
const sessionIdForScope = sessionIdUsed ?? readString(input.sessionId);
|
|
1393
|
+
if (sessionFileForMirror && messagesSnapshot.length > 0) {
|
|
1394
|
+
const taggedMessages = messagesSnapshot.map((message, index) => {
|
|
1395
|
+
if (message.role !== "user" && message.role !== "assistant" && message.role !== "toolResult") return message;
|
|
1396
|
+
if (hasMirrorIdentity(message)) return message;
|
|
1397
|
+
return attachCopilotMirrorIdentity(message, `${sdkSessionId ?? sessionIdForScope ?? "attempt"}:${message.role}:${index}`);
|
|
1398
|
+
});
|
|
1399
|
+
await dualWriteCopilotTranscriptBestEffort({
|
|
1400
|
+
sessionFile: sessionFileForMirror,
|
|
1401
|
+
sessionKey: readString(input.sessionKey),
|
|
1402
|
+
agentId: readString(input.agentId),
|
|
1403
|
+
messages: taggedMessages,
|
|
1404
|
+
idempotencyScope: sessionIdForScope ? `copilot:${sessionIdForScope}` : void 0,
|
|
1405
|
+
config: input.config
|
|
1406
|
+
}).catch((mirrorError) => {
|
|
1407
|
+
console.warn("[copilot-attempt] dual-write transcript wrapper rejected unexpectedly", mirrorError);
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
return createResult(input, {
|
|
1411
|
+
aborted,
|
|
1412
|
+
assistantTexts,
|
|
1413
|
+
currentAttemptAssistant: lastAssistant,
|
|
1414
|
+
downgradedFromResume,
|
|
1415
|
+
externalAbort,
|
|
1416
|
+
itemLifecycle: {
|
|
1417
|
+
activeCount: Math.max((snap?.startedCount ?? 0) - (snap?.completedCount ?? 0), 0),
|
|
1418
|
+
completedCount: snap?.completedCount ?? 0,
|
|
1419
|
+
startedCount: snap?.startedCount ?? 0
|
|
1420
|
+
},
|
|
1421
|
+
lastAssistant,
|
|
1422
|
+
messagesSnapshot,
|
|
1423
|
+
now,
|
|
1424
|
+
promptError,
|
|
1425
|
+
resumeFailureRecovered,
|
|
1426
|
+
sdkSessionId,
|
|
1427
|
+
sessionIdUsed,
|
|
1428
|
+
timedOut,
|
|
1429
|
+
toolMetas: snap ? [...snap.toolMetas] : [],
|
|
1430
|
+
usage: snap?.usage,
|
|
1431
|
+
yieldDetected
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
function createResult(params, state) {
|
|
1435
|
+
const promptError = state.promptError;
|
|
1436
|
+
const timedOut = state.timedOut === true;
|
|
1437
|
+
const toolMetas = state.toolMetas ?? [];
|
|
1438
|
+
const replayMetadata = computeReplayMetadata({
|
|
1439
|
+
priorReplayInvalid: params.initialReplayState?.replayInvalid,
|
|
1440
|
+
priorHadPotentialSideEffects: params.initialReplayState?.hadPotentialSideEffects,
|
|
1441
|
+
thisAttemptTimedOut: timedOut,
|
|
1442
|
+
thisAttemptHadPotentialSideEffects: copilotToolMetasHavePotentialSideEffects(toolMetas),
|
|
1443
|
+
thisAttemptDowngradedFromResume: state.downgradedFromResume,
|
|
1444
|
+
thisAttemptResumeFailureRecovered: state.resumeFailureRecovered
|
|
1445
|
+
});
|
|
1446
|
+
return {
|
|
1447
|
+
aborted: state.aborted === true,
|
|
1448
|
+
...state.sdkSessionId ? { sdkSessionId: state.sdkSessionId } : {},
|
|
1449
|
+
assistantTexts: state.assistantTexts ?? [],
|
|
1450
|
+
attemptUsage: state.usage,
|
|
1451
|
+
cloudCodeAssistFormatError: false,
|
|
1452
|
+
currentAttemptAssistant: state.currentAttemptAssistant,
|
|
1453
|
+
didSendViaMessagingTool: false,
|
|
1454
|
+
externalAbort: state.externalAbort === true,
|
|
1455
|
+
idleTimedOut: false,
|
|
1456
|
+
itemLifecycle: state.itemLifecycle ?? {
|
|
1457
|
+
activeCount: 0,
|
|
1458
|
+
completedCount: 0,
|
|
1459
|
+
startedCount: 0
|
|
1460
|
+
},
|
|
1461
|
+
lastAssistant: state.lastAssistant,
|
|
1462
|
+
messagesSnapshot: state.messagesSnapshot,
|
|
1463
|
+
messagingToolSentMediaUrls: [],
|
|
1464
|
+
messagingToolSentTargets: [],
|
|
1465
|
+
messagingToolSentTexts: [],
|
|
1466
|
+
promptError,
|
|
1467
|
+
promptErrorSource: promptError ? "prompt" : null,
|
|
1468
|
+
replayMetadata,
|
|
1469
|
+
sessionFileUsed: readString(params.sessionFile),
|
|
1470
|
+
sessionIdUsed: state.sessionIdUsed ?? readString(params.sessionId) ?? "copilot-session",
|
|
1471
|
+
timedOut,
|
|
1472
|
+
timedOutDuringCompaction: false,
|
|
1473
|
+
toolMetas,
|
|
1474
|
+
yieldDetected: state.yieldDetected === true
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
function createPromptError(code, message, cause) {
|
|
1478
|
+
const error = new Error(message);
|
|
1479
|
+
error.code = code;
|
|
1480
|
+
if (cause !== void 0) error.cause = cause;
|
|
1481
|
+
return error;
|
|
1482
|
+
}
|
|
1483
|
+
function createSessionConfig(params, sdkModelId, sdkTools, resolvedAuth, workspaceBootstrapInstructions, effectiveWorkspaceDir, effectiveCwd) {
|
|
1484
|
+
const permissionPolicy = params.permissionPolicy ?? rejectAllPolicy;
|
|
1485
|
+
const hooks = createHooksBridge(params.hooksConfig);
|
|
1486
|
+
const infiniteSessions = createInfiniteSessionConfig(params.infiniteSessionConfig);
|
|
1487
|
+
const systemMessageContent = createSystemMessageContent(params, workspaceBootstrapInstructions);
|
|
1488
|
+
return {
|
|
1489
|
+
model: sdkModelId,
|
|
1490
|
+
onPermissionRequest: createPermissionBridge(permissionPolicy),
|
|
1491
|
+
...hooks ? { hooks } : {},
|
|
1492
|
+
...typeof params.enableSessionTelemetry === "boolean" ? { enableSessionTelemetry: params.enableSessionTelemetry } : {},
|
|
1493
|
+
...infiniteSessions ? { infiniteSessions } : {},
|
|
1494
|
+
reasoningEffort: params.reasoningEffort,
|
|
1495
|
+
tools: sdkTools,
|
|
1496
|
+
availableTools: sdkTools.map((tool) => tool.name),
|
|
1497
|
+
workingDirectory: effectiveCwd ?? effectiveWorkspaceDir ?? readResolvedAttemptPath(params.workspaceDir),
|
|
1498
|
+
...effectiveWorkspaceDir && effectiveCwd && effectiveCwd !== effectiveWorkspaceDir ? { instructionDirectories: [effectiveWorkspaceDir] } : {},
|
|
1499
|
+
...resolvedAuth.authMode === "gitHubToken" && resolvedAuth.gitHubToken ? { gitHubToken: resolvedAuth.gitHubToken } : {},
|
|
1500
|
+
...systemMessageContent ? { systemMessage: {
|
|
1501
|
+
mode: "append",
|
|
1502
|
+
content: systemMessageContent
|
|
1503
|
+
} } : {}
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
async function createMessageOptions(params, context) {
|
|
1507
|
+
const attachments = createPromptImageAttachments(await resolvePromptImages(params, context));
|
|
1508
|
+
return attachments.length > 0 ? {
|
|
1509
|
+
prompt: params.prompt,
|
|
1510
|
+
attachments
|
|
1511
|
+
} : { prompt: params.prompt };
|
|
1512
|
+
}
|
|
1513
|
+
function createPromptImageAttachments(images) {
|
|
1514
|
+
return images.flatMap((image, index) => {
|
|
1515
|
+
if (!image || typeof image !== "object" || image.type !== "image" || typeof image.data !== "string" || typeof image.mimeType !== "string") return [];
|
|
1516
|
+
return [{
|
|
1517
|
+
type: "blob",
|
|
1518
|
+
data: image.data,
|
|
1519
|
+
mimeType: image.mimeType,
|
|
1520
|
+
displayName: `prompt-image-${index + 1}`
|
|
1521
|
+
}];
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
async function resolvePromptImages(params, context) {
|
|
1525
|
+
const workspaceDir = context.effectiveCwd ?? context.effectiveWorkspaceDir ?? readResolvedAttemptPath(params.cwd) ?? readResolvedAttemptPath(params.workspaceDir);
|
|
1526
|
+
if (!workspaceDir) return [];
|
|
1527
|
+
const localRoots = context.workspaceOnly && context.effectiveWorkspaceDir ? [context.effectiveWorkspaceDir] : void 0;
|
|
1528
|
+
return (await detectAndLoadAgentHarnessPromptImages({
|
|
1529
|
+
prompt: params.prompt,
|
|
1530
|
+
workspaceDir,
|
|
1531
|
+
model: resolveImageCapabilityModel(params),
|
|
1532
|
+
existingImages: Array.isArray(params.images) ? params.images : void 0,
|
|
1533
|
+
imageOrder: Array.isArray(params.imageOrder) ? params.imageOrder : void 0,
|
|
1534
|
+
config: params.config,
|
|
1535
|
+
workspaceOnly: context.workspaceOnly,
|
|
1536
|
+
localRoots,
|
|
1537
|
+
sandbox: context.sandbox?.enabled && context.sandbox.fsBridge ? {
|
|
1538
|
+
root: context.sandbox.workspaceDir,
|
|
1539
|
+
bridge: context.sandbox.fsBridge
|
|
1540
|
+
} : void 0
|
|
1541
|
+
})).images;
|
|
1542
|
+
}
|
|
1543
|
+
function resolveImageCapabilityModel(params) {
|
|
1544
|
+
const model = params.model;
|
|
1545
|
+
if (model && typeof model === "object" && Array.isArray(model.input)) return { input: model.input };
|
|
1546
|
+
return { input: ["image"] };
|
|
1547
|
+
}
|
|
1548
|
+
function createSystemMessageContent(params, workspaceBootstrapInstructions) {
|
|
1549
|
+
const sections = [];
|
|
1550
|
+
const bootstrap = workspaceBootstrapInstructions?.trim();
|
|
1551
|
+
if (bootstrap) sections.push(bootstrap);
|
|
1552
|
+
const extraSystemPrompt = readString(params.extraSystemPrompt)?.trim();
|
|
1553
|
+
if (extraSystemPrompt && !isRawCopilotModelRun(params)) {
|
|
1554
|
+
const contextHeader = params.promptMode === "minimal" ? "## Subagent Context" : "## Group Chat Context";
|
|
1555
|
+
sections.push(`${contextHeader}\n${extraSystemPrompt}`);
|
|
1556
|
+
}
|
|
1557
|
+
return sections.length > 0 ? sections.join("\n\n") : void 0;
|
|
1558
|
+
}
|
|
1559
|
+
function isRawCopilotModelRun(params) {
|
|
1560
|
+
return params.modelRun === true || params.promptMode === "none";
|
|
1561
|
+
}
|
|
1562
|
+
function getMessagesSnapshotInput(params) {
|
|
1563
|
+
return Array.isArray(params.messages) ? [...params.messages] : [];
|
|
1564
|
+
}
|
|
1565
|
+
function readTailUserText(messages) {
|
|
1566
|
+
const tail = messages[messages.length - 1];
|
|
1567
|
+
if (!tail || tail.role !== "user") return;
|
|
1568
|
+
const content = tail.content;
|
|
1569
|
+
if (typeof content === "string") return content;
|
|
1570
|
+
if (Array.isArray(content)) {
|
|
1571
|
+
for (const part of content) if (part && typeof part === "object" && part.type === "text") {
|
|
1572
|
+
const text = part.text;
|
|
1573
|
+
if (typeof text === "string" && text.length > 0) return text;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
function hasMirrorIdentity(message) {
|
|
1578
|
+
const meta = message["__openclaw"];
|
|
1579
|
+
if (!meta || typeof meta !== "object" || Array.isArray(meta)) return false;
|
|
1580
|
+
const id = meta.mirrorIdentity;
|
|
1581
|
+
return typeof id === "string" && id.length > 0;
|
|
1582
|
+
}
|
|
1583
|
+
function readSessionId(session) {
|
|
1584
|
+
if (!session) return;
|
|
1585
|
+
return readString(session.sessionId) ?? readString(session.id);
|
|
1586
|
+
}
|
|
1587
|
+
function readString(value) {
|
|
1588
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1589
|
+
}
|
|
1590
|
+
function readResolvedAttemptPath(value) {
|
|
1591
|
+
const raw = readString(value)?.trim();
|
|
1592
|
+
if (!raw) return;
|
|
1593
|
+
if (process.platform !== "win32" && /^[A-Za-z]:[\\/]/.test(raw)) return raw;
|
|
1594
|
+
return resolveUserPath(raw);
|
|
1595
|
+
}
|
|
1596
|
+
function resolveModelRef(params) {
|
|
1597
|
+
const rawModel = params.model;
|
|
1598
|
+
if (rawModel && typeof rawModel === "object") return {
|
|
1599
|
+
api: readString(rawModel.api),
|
|
1600
|
+
id: readString(rawModel.id) ?? readString(params.modelId) ?? "unknown-model",
|
|
1601
|
+
provider: readString(rawModel.provider) ?? readString(params.provider) ?? "unknown-provider"
|
|
1602
|
+
};
|
|
1603
|
+
return {
|
|
1604
|
+
id: readString(typeof rawModel === "string" ? rawModel : void 0) ?? readString(params.modelId) ?? "unknown-model",
|
|
1605
|
+
provider: readString(params.provider) ?? "unknown-provider"
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
function resolvePoolAcquire(params) {
|
|
1609
|
+
const resolved = resolveCopilotAuth({
|
|
1610
|
+
agentId: readString(params.agentId),
|
|
1611
|
+
agentDir: readString(params.agentDir),
|
|
1612
|
+
workspaceDir: readString(params.workspaceDir),
|
|
1613
|
+
copilotHome: readString(params.copilotHome),
|
|
1614
|
+
auth: params.auth,
|
|
1615
|
+
resolvedApiKey: readString(params.resolvedApiKey),
|
|
1616
|
+
authProfileId: readString(params.authProfileId),
|
|
1617
|
+
profileVersion: readString(params.profileVersion)
|
|
1618
|
+
});
|
|
1619
|
+
return {
|
|
1620
|
+
key: {
|
|
1621
|
+
agentId: resolved.agentId,
|
|
1622
|
+
authMode: resolved.authMode,
|
|
1623
|
+
...resolved.authMode === "gitHubToken" ? {
|
|
1624
|
+
authProfileId: resolved.authProfileId,
|
|
1625
|
+
authProfileVersion: resolved.authProfileVersion
|
|
1626
|
+
} : {},
|
|
1627
|
+
copilotHome: resolved.copilotHome
|
|
1628
|
+
},
|
|
1629
|
+
options: {
|
|
1630
|
+
copilotHome: resolved.copilotHome,
|
|
1631
|
+
cwd: readString(params.cwd) ?? readString(params.workspaceDir),
|
|
1632
|
+
gitHubToken: resolved.authMode === "gitHubToken" ? resolved.gitHubToken : void 0,
|
|
1633
|
+
useLoggedInUser: resolved.authMode === "useLoggedInUser"
|
|
1634
|
+
},
|
|
1635
|
+
auth: resolved
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
function toError(error) {
|
|
1639
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Detect the @github/copilot-sdk `session.sendAndWait` timeout
|
|
1643
|
+
* rejection shape. The SDK's `sendAndWait` races the internal
|
|
1644
|
+
* `session.idle` event against a timer; when the timer fires first
|
|
1645
|
+
* it REJECTS the promise with
|
|
1646
|
+
* `new Error(`Timeout after ${effectiveTimeout}ms waiting for
|
|
1647
|
+
* session.idle`)` (see
|
|
1648
|
+
* `node_modules/@github/copilot-sdk/dist/session.js:156-164`), and
|
|
1649
|
+
* the SDK docs explicitly note the timeout "does not abort in-flight
|
|
1650
|
+
* agent work". The caller is therefore responsible for setting the
|
|
1651
|
+
* timed-out state and (for paths where in-flight work should be
|
|
1652
|
+
* stopped) calling `session.abort()`.
|
|
1653
|
+
*
|
|
1654
|
+
* Keep the regex anchored and narrow so unrelated errors that happen
|
|
1655
|
+
* to mention "Timeout" are NOT mis-classified. The shape is a literal
|
|
1656
|
+
* template-string concatenation in the 1.0.0-beta line; a minor
|
|
1657
|
+
* version bump that changes the wording will safely fall through to
|
|
1658
|
+
* the generic prompt-error path.
|
|
1659
|
+
*/
|
|
1660
|
+
function isSdkSendAndWaitTimeoutError(error) {
|
|
1661
|
+
if (error === null || typeof error !== "object") return false;
|
|
1662
|
+
const message = error.message;
|
|
1663
|
+
if (typeof message !== "string") return false;
|
|
1664
|
+
return /^Timeout after \d+ms waiting for session\.idle$/.test(message);
|
|
1665
|
+
}
|
|
1666
|
+
//#endregion
|
|
1667
|
+
export { runCopilotAttempt };
|