Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
622
extensions/copilot/src/event-bridge.ts
Normal file
622
extensions/copilot/src/event-bridge.ts
Normal file
@@ -0,0 +1,622 @@
|
||||
// Copilot plugin module implements event bridge behavior.
|
||||
import type { MessageOptions, SessionEvent, SessionEventType } from "@github/copilot-sdk";
|
||||
import type { AgentMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import {
|
||||
buildCopilotAssistantUsage,
|
||||
normalizeCopilotUsage,
|
||||
type CopilotUsageSnapshot,
|
||||
} from "./usage-bridge.js";
|
||||
|
||||
export type AssistantMessage = Extract<AgentMessage, { role: "assistant" }>;
|
||||
|
||||
export type AssistantUsageSnapshot = CopilotUsageSnapshot;
|
||||
|
||||
export interface OnAssistantDeltaPayload {
|
||||
delta: string;
|
||||
sessionId?: string;
|
||||
text: string;
|
||||
usage?: AssistantUsageSnapshot;
|
||||
}
|
||||
|
||||
export interface SessionLike {
|
||||
abort(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
id?: string;
|
||||
off?: (eventType: string, handler: (...args: unknown[]) => void) => void;
|
||||
on: {
|
||||
<K extends SessionEventType>(
|
||||
eventType: K,
|
||||
handler: (event: Extract<SessionEvent, { type: K }>) => void,
|
||||
): (() => void) | void;
|
||||
(eventType: string, handler: (event: SessionEvent) => void): (() => void) | void;
|
||||
};
|
||||
rpc?: {
|
||||
history?: {
|
||||
cancelBackgroundCompaction?: () => Promise<unknown>;
|
||||
};
|
||||
};
|
||||
sendAndWait(options: MessageOptions, timeout?: number): Promise<SessionEvent | undefined>;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
export interface EventBridgeOptions {
|
||||
onAssistantDelta?: (payload: OnAssistantDeltaPayload) => void | Promise<void>;
|
||||
onAgentEvent?: (event: {
|
||||
stream: "item" | "plan";
|
||||
data: Record<string, unknown>;
|
||||
}) => void | Promise<void>;
|
||||
onNativeSubagentEvent?: (
|
||||
event: Extract<
|
||||
SessionEvent,
|
||||
{ type: "subagent.started" | "subagent.completed" | "subagent.failed" }
|
||||
>,
|
||||
) => void;
|
||||
onCompactionComplete?: (payload: {
|
||||
messagesRemoved?: number;
|
||||
success: boolean;
|
||||
}) => void | Promise<void>;
|
||||
onCompactionStart?: () => void | Promise<void>;
|
||||
getSdkSessionId: () => string | undefined;
|
||||
isAborted: () => boolean;
|
||||
}
|
||||
|
||||
export interface EventBridgeSnapshot {
|
||||
readonly assistantTexts: readonly string[];
|
||||
readonly completedCount: number;
|
||||
readonly lastAssistantEvent: Extract<SessionEvent, { type: "assistant.message" }> | undefined;
|
||||
readonly startedCount: number;
|
||||
readonly streamError: Error | undefined;
|
||||
readonly toolMetas: ReadonlyArray<{ meta?: string; toolName: string }>;
|
||||
readonly usage: AssistantUsageSnapshot | undefined;
|
||||
}
|
||||
|
||||
export interface BuildAssistantMessageArgs {
|
||||
modelRef: { api?: string; id: string; provider: string };
|
||||
now: () => number;
|
||||
}
|
||||
|
||||
export interface EventBridgeController {
|
||||
recordSendResult(result: SessionEvent | undefined): boolean;
|
||||
awaitCompactionChain(): Promise<void>;
|
||||
awaitCompactionCompletion(): Promise<void>;
|
||||
awaitSessionIdle(): Promise<void>;
|
||||
settleCompactionWait(): void;
|
||||
awaitDeltaChain(): Promise<void>;
|
||||
awaitAgentEventChain(): Promise<void>;
|
||||
hasObservedCompaction(): boolean;
|
||||
hasObservedSessionIdle(): boolean;
|
||||
isCompacting(): boolean;
|
||||
snapshot(): EventBridgeSnapshot;
|
||||
buildAssistantMessage(args: BuildAssistantMessageArgs): AssistantMessage | undefined;
|
||||
finalizeAssistantTexts(): string[];
|
||||
detach(): void;
|
||||
}
|
||||
|
||||
type MessageAccumulator = { messageId: string; text: string };
|
||||
type PromptErrorWithCode = Error & { code?: string; cause?: unknown };
|
||||
|
||||
export function attachEventBridge(
|
||||
session: SessionLike,
|
||||
options: EventBridgeOptions,
|
||||
): EventBridgeController {
|
||||
const messageOrder: string[] = [];
|
||||
const messagesById = new Map<string, MessageAccumulator>();
|
||||
const reasoningOrder: string[] = [];
|
||||
const reasoningById = new Map<string, string>();
|
||||
let lastAssistantEvent: Extract<SessionEvent, { type: "assistant.message" }> | undefined;
|
||||
let usage: AssistantUsageSnapshot | undefined;
|
||||
let streamError: Error | undefined;
|
||||
const toolMetas: Array<{ meta?: string; toolName: string }> = [];
|
||||
const toolNamesByCallId = new Map<string, string>();
|
||||
let startedCount = 0;
|
||||
let completedCount = 0;
|
||||
let activeCompactionCount = 0;
|
||||
let observedCompaction = false;
|
||||
let deltaQueue = Promise.resolve();
|
||||
let deltaChain = Promise.resolve();
|
||||
let agentEventChain = Promise.resolve();
|
||||
let compactionChain = Promise.resolve();
|
||||
let compactionIdle = Promise.resolve();
|
||||
let resolveCompactionIdle: (() => void) | undefined;
|
||||
let observedSessionIdle = false;
|
||||
let resolveSessionIdle: (() => void) | undefined;
|
||||
const sessionIdle = new Promise<void>((resolve) => {
|
||||
resolveSessionIdle = resolve;
|
||||
});
|
||||
let firstDeltaError: unknown;
|
||||
let detached = false;
|
||||
const unsubscribeFns: Array<() => void> = [];
|
||||
|
||||
registerListener(session, unsubscribeFns, "assistant.message_delta", (event) => {
|
||||
if (!isRootSessionEvent(event)) {
|
||||
return;
|
||||
}
|
||||
const messageId = readString(event.data.messageId) ?? "assistant-message";
|
||||
const delta = event.data.deltaContent;
|
||||
if (!delta) {
|
||||
return;
|
||||
}
|
||||
const entry = ensureMessageAccumulator(messagesById, messageOrder, messageId);
|
||||
entry.text += delta;
|
||||
const onAssistantDelta = options.onAssistantDelta;
|
||||
if (!onAssistantDelta) {
|
||||
return;
|
||||
}
|
||||
const payload: OnAssistantDeltaPayload = {
|
||||
delta,
|
||||
sessionId: options.getSdkSessionId(),
|
||||
text: entry.text,
|
||||
usage,
|
||||
};
|
||||
deltaQueue = deltaQueue
|
||||
.then(
|
||||
() => onAssistantDelta(payload),
|
||||
() => onAssistantDelta(payload),
|
||||
)
|
||||
.catch((error: unknown) => {
|
||||
firstDeltaError ??= error;
|
||||
});
|
||||
deltaChain = deltaQueue.then(() => {
|
||||
if (firstDeltaError !== undefined) {
|
||||
throw toLintErrorObject(firstDeltaError, "Non-Error thrown");
|
||||
}
|
||||
});
|
||||
void deltaChain.catch(() => undefined);
|
||||
});
|
||||
|
||||
registerListener(session, unsubscribeFns, "assistant.reasoning_delta", (event) => {
|
||||
if (!isRootSessionEvent(event)) {
|
||||
return;
|
||||
}
|
||||
const reasoningId = readString(event.data.reasoningId) ?? "assistant-reasoning";
|
||||
const delta = event.data.deltaContent;
|
||||
if (!delta) {
|
||||
return;
|
||||
}
|
||||
if (!reasoningById.has(reasoningId)) {
|
||||
reasoningById.set(reasoningId, "");
|
||||
reasoningOrder.push(reasoningId);
|
||||
}
|
||||
reasoningById.set(reasoningId, `${reasoningById.get(reasoningId) ?? ""}${delta}`);
|
||||
});
|
||||
|
||||
registerListener(session, unsubscribeFns, "assistant.message", (event) => {
|
||||
if (!isRootSessionEvent(event)) {
|
||||
return;
|
||||
}
|
||||
lastAssistantEvent = event;
|
||||
const entry = ensureMessageAccumulator(messagesById, messageOrder, event.data.messageId);
|
||||
if (typeof event.data.content === "string" && event.data.content.length >= entry.text.length) {
|
||||
entry.text = event.data.content;
|
||||
}
|
||||
});
|
||||
|
||||
registerListener(session, unsubscribeFns, "assistant.usage", (event) => {
|
||||
if (!isRootSessionEvent(event)) {
|
||||
return;
|
||||
}
|
||||
usage = normalizeCopilotUsage(event.data);
|
||||
});
|
||||
|
||||
registerListener(session, unsubscribeFns, "tool.execution_start", (event) => {
|
||||
if (isRootSessionEvent(event)) {
|
||||
startedCount += 1;
|
||||
}
|
||||
toolNamesByCallId.set(event.data.toolCallId, event.data.toolName);
|
||||
toolMetas.push({ toolName: event.data.toolName });
|
||||
});
|
||||
|
||||
registerListener(session, unsubscribeFns, "tool.execution_complete", (event) => {
|
||||
if (isRootSessionEvent(event)) {
|
||||
completedCount += 1;
|
||||
}
|
||||
const toolName = toolNamesByCallId.get(event.data.toolCallId);
|
||||
const meta = event.data.success
|
||||
? (event.data.result?.detailedContent ?? event.data.result?.content)
|
||||
: event.data.error?.message;
|
||||
if (toolName) {
|
||||
toolMetas.push({ meta, toolName });
|
||||
}
|
||||
});
|
||||
|
||||
registerListener(session, unsubscribeFns, "session.plan_changed", (event) => {
|
||||
enqueueAgentEvent({
|
||||
stream: "plan",
|
||||
data: {
|
||||
phase: "update",
|
||||
title: "Plan updated",
|
||||
source: "copilot-sdk",
|
||||
operation: event.data.operation,
|
||||
...(event.agentId ? { agentId: event.agentId } : {}),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
registerListener(session, unsubscribeFns, "exit_plan_mode.requested", (event) => {
|
||||
const steps = splitPlanText(event.data.planContent);
|
||||
enqueueAgentEvent({
|
||||
stream: "plan",
|
||||
data: {
|
||||
phase: "update",
|
||||
title: "Plan updated",
|
||||
source: "copilot-sdk",
|
||||
...(event.data.summary ? { explanation: event.data.summary } : {}),
|
||||
...(steps.length > 0 ? { steps } : {}),
|
||||
...(event.data.actions.length > 0 ? { actions: event.data.actions } : {}),
|
||||
...(event.data.requestId ? { requestId: event.data.requestId } : {}),
|
||||
...(event.data.recommendedAction
|
||||
? { recommendedAction: event.data.recommendedAction }
|
||||
: {}),
|
||||
...(event.agentId ? { agentId: event.agentId } : {}),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
registerListener(session, unsubscribeFns, "exit_plan_mode.completed", (event) => {
|
||||
enqueueAgentEvent({
|
||||
stream: "plan",
|
||||
data: {
|
||||
phase: "update",
|
||||
title: "Plan decision",
|
||||
source: "copilot-sdk",
|
||||
requestId: event.data.requestId,
|
||||
...(event.data.approved !== undefined ? { approved: event.data.approved } : {}),
|
||||
...(event.data.autoApproveEdits !== undefined
|
||||
? { autoApproveEdits: event.data.autoApproveEdits }
|
||||
: {}),
|
||||
...(event.data.feedback ? { feedback: event.data.feedback } : {}),
|
||||
...(event.data.selectedAction ? { selectedAction: event.data.selectedAction } : {}),
|
||||
...(event.agentId ? { agentId: event.agentId } : {}),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
registerListener(session, unsubscribeFns, "subagent.started", (event) => {
|
||||
forwardNativeSubagentEvent(event);
|
||||
});
|
||||
|
||||
registerListener(session, unsubscribeFns, "subagent.completed", (event) => {
|
||||
forwardNativeSubagentEvent(event);
|
||||
});
|
||||
|
||||
registerListener(session, unsubscribeFns, "subagent.failed", (event) => {
|
||||
forwardNativeSubagentEvent(event);
|
||||
});
|
||||
|
||||
registerListener(session, unsubscribeFns, "session.compaction_start", (event) => {
|
||||
if (!isRootCompactionEvent(event)) {
|
||||
return;
|
||||
}
|
||||
observedCompaction = true;
|
||||
if (activeCompactionCount === 0) {
|
||||
compactionIdle = new Promise<void>((resolve) => {
|
||||
resolveCompactionIdle = resolve;
|
||||
});
|
||||
}
|
||||
activeCompactionCount += 1;
|
||||
enqueueCompactionCallback(options.onCompactionStart);
|
||||
});
|
||||
|
||||
registerListener(session, unsubscribeFns, "session.compaction_complete", (event) => {
|
||||
if (!isRootCompactionEvent(event)) {
|
||||
return;
|
||||
}
|
||||
activeCompactionCount = Math.max(0, activeCompactionCount - 1);
|
||||
enqueueCompactionCallback(() =>
|
||||
options.onCompactionComplete?.({
|
||||
...(event.data.messagesRemoved !== undefined
|
||||
? { messagesRemoved: event.data.messagesRemoved }
|
||||
: {}),
|
||||
success: event.data.success,
|
||||
}),
|
||||
);
|
||||
if (activeCompactionCount === 0) {
|
||||
resolveCompactionIdle?.();
|
||||
resolveCompactionIdle = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
registerListener(session, unsubscribeFns, "session.idle", (event) => {
|
||||
if (!isRootCompactionEvent(event)) {
|
||||
return;
|
||||
}
|
||||
observedSessionIdle = true;
|
||||
resolveSessionIdle?.();
|
||||
resolveSessionIdle = undefined;
|
||||
});
|
||||
|
||||
registerListener(session, unsubscribeFns, "session.error", (event) => {
|
||||
if (!options.isAborted()) {
|
||||
streamError = createPromptError(
|
||||
event.data.errorCode ?? event.data.errorType,
|
||||
event.data.message,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
registerListener(session, unsubscribeFns, "abort", (event) => {
|
||||
if (!options.isAborted()) {
|
||||
streamError = createPromptError(
|
||||
"session_aborted",
|
||||
`[copilot-attempt] session aborted: ${event.data.reason}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
recordSendResult(result) {
|
||||
if (!isAssistantMessageEvent(result)) {
|
||||
return false;
|
||||
}
|
||||
lastAssistantEvent = result;
|
||||
return true;
|
||||
},
|
||||
awaitCompactionChain() {
|
||||
return compactionChain;
|
||||
},
|
||||
async awaitCompactionCompletion() {
|
||||
await awaitStableCompaction();
|
||||
},
|
||||
awaitSessionIdle() {
|
||||
return observedSessionIdle ? Promise.resolve() : sessionIdle;
|
||||
},
|
||||
settleCompactionWait() {
|
||||
activeCompactionCount = 0;
|
||||
resolveCompactionIdle?.();
|
||||
resolveCompactionIdle = undefined;
|
||||
},
|
||||
awaitDeltaChain() {
|
||||
return deltaChain;
|
||||
},
|
||||
awaitAgentEventChain() {
|
||||
return agentEventChain;
|
||||
},
|
||||
hasObservedCompaction() {
|
||||
return observedCompaction;
|
||||
},
|
||||
hasObservedSessionIdle() {
|
||||
return observedSessionIdle;
|
||||
},
|
||||
isCompacting() {
|
||||
return activeCompactionCount > 0;
|
||||
},
|
||||
snapshot() {
|
||||
return {
|
||||
assistantTexts: finalizeAssistantTexts(messageOrder, messagesById, lastAssistantEvent),
|
||||
completedCount,
|
||||
lastAssistantEvent,
|
||||
startedCount,
|
||||
streamError,
|
||||
toolMetas: toolMetas.map((toolMeta) => Object.assign({}, toolMeta)),
|
||||
usage: usage ? { ...usage } : undefined,
|
||||
};
|
||||
},
|
||||
buildAssistantMessage(args) {
|
||||
return buildAssistantMessage({
|
||||
event: lastAssistantEvent,
|
||||
modelRef: args.modelRef,
|
||||
now: args.now,
|
||||
reasoningById,
|
||||
reasoningOrder,
|
||||
usage,
|
||||
assistantTexts: finalizeAssistantTexts(messageOrder, messagesById, lastAssistantEvent),
|
||||
});
|
||||
},
|
||||
finalizeAssistantTexts() {
|
||||
return finalizeAssistantTexts(messageOrder, messagesById, lastAssistantEvent);
|
||||
},
|
||||
detach() {
|
||||
if (detached) {
|
||||
return;
|
||||
}
|
||||
detached = true;
|
||||
for (const unsubscribe of [...unsubscribeFns].toReversed()) {
|
||||
try {
|
||||
unsubscribe();
|
||||
} catch {
|
||||
// best-effort cleanup only
|
||||
}
|
||||
}
|
||||
unsubscribeFns.length = 0;
|
||||
},
|
||||
};
|
||||
|
||||
function enqueueCompactionCallback(callback: (() => void | Promise<void>) | undefined): void {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
const queued = compactionChain.then(callback, callback);
|
||||
compactionChain = queued.catch(() => undefined);
|
||||
}
|
||||
|
||||
function enqueueAgentEvent(event: {
|
||||
stream: "item" | "plan";
|
||||
data: Record<string, unknown>;
|
||||
}): void {
|
||||
const callback = options.onAgentEvent;
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
const invoke = () => callback(event);
|
||||
agentEventChain = agentEventChain.then(invoke, invoke).catch(() => undefined);
|
||||
}
|
||||
|
||||
function forwardNativeSubagentEvent(
|
||||
event: Extract<
|
||||
SessionEvent,
|
||||
{ type: "subagent.started" | "subagent.completed" | "subagent.failed" }
|
||||
>,
|
||||
): void {
|
||||
try {
|
||||
options.onNativeSubagentEvent?.(event);
|
||||
} catch {
|
||||
// Native task mirroring must not corrupt the Copilot turn.
|
||||
}
|
||||
}
|
||||
|
||||
async function awaitStableCompaction(): Promise<void> {
|
||||
const idle = activeCompactionCount > 0 ? compactionIdle : undefined;
|
||||
if (idle) {
|
||||
await idle;
|
||||
}
|
||||
const callbacks = compactionChain;
|
||||
await callbacks;
|
||||
// Compaction events can arrive while an earlier hook callback settles.
|
||||
// Recheck both queues before teardown so the root observer stays attached.
|
||||
if (activeCompactionCount > 0 || compactionChain !== callbacks) {
|
||||
await awaitStableCompaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildAssistantMessage(params: {
|
||||
assistantTexts: string[];
|
||||
event?: Extract<SessionEvent, { type: "assistant.message" }>;
|
||||
modelRef: { api?: string; id: string; provider: string };
|
||||
now: () => number;
|
||||
reasoningById: Map<string, string>;
|
||||
reasoningOrder: string[];
|
||||
usage?: AssistantUsageSnapshot;
|
||||
}): AssistantMessage | undefined {
|
||||
const event = params.event;
|
||||
const text = event
|
||||
? event.data.content || params.assistantTexts[params.assistantTexts.length - 1] || ""
|
||||
: "";
|
||||
const reasoningText =
|
||||
event?.data.reasoningText ?? joinReasoning(params.reasoningOrder, params.reasoningById);
|
||||
const toolRequests = event?.data.toolRequests ?? [];
|
||||
if (!text && !reasoningText && toolRequests.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const content: AssistantMessage["content"] = [];
|
||||
if (reasoningText) {
|
||||
content.push({ thinking: reasoningText, type: "thinking" });
|
||||
}
|
||||
if (text) {
|
||||
content.push({ text, type: "text" });
|
||||
}
|
||||
for (const request of toolRequests) {
|
||||
content.push({
|
||||
arguments: request.arguments ?? {},
|
||||
id: request.toolCallId,
|
||||
name: request.name,
|
||||
type: "toolCall",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
api: params.modelRef.api ?? "openai-responses",
|
||||
content,
|
||||
model: event?.data.model ?? params.modelRef.id,
|
||||
provider: params.modelRef.provider,
|
||||
role: "assistant",
|
||||
stopReason: toolRequests.length > 0 ? "toolUse" : "stop",
|
||||
timestamp: params.now(),
|
||||
usage: buildCopilotAssistantUsage({
|
||||
fallbackOutputTokens: event?.data.outputTokens,
|
||||
usage: params.usage,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function createPromptError(code: string, message: string, cause?: unknown): PromptErrorWithCode {
|
||||
const error = new Error(message) as PromptErrorWithCode;
|
||||
error.code = code;
|
||||
if (cause !== undefined) {
|
||||
error.cause = cause;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
function ensureMessageAccumulator(
|
||||
messagesById: Map<string, MessageAccumulator>,
|
||||
messageOrder: string[],
|
||||
messageId: string,
|
||||
): MessageAccumulator {
|
||||
let entry = messagesById.get(messageId);
|
||||
if (!entry) {
|
||||
entry = { messageId, text: "" };
|
||||
messagesById.set(messageId, entry);
|
||||
messageOrder.push(messageId);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
function finalizeAssistantTexts(
|
||||
messageOrder: string[],
|
||||
messagesById: Map<string, MessageAccumulator>,
|
||||
event?: Extract<SessionEvent, { type: "assistant.message" }>,
|
||||
): string[] {
|
||||
const texts = messageOrder
|
||||
.map((messageId) => messagesById.get(messageId)?.text ?? "")
|
||||
.filter((text) => text.length > 0);
|
||||
if (texts.length > 0) {
|
||||
return texts;
|
||||
}
|
||||
if (event?.data.content) {
|
||||
return [event.data.content];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function isAssistantMessageEvent(
|
||||
event: SessionEvent | undefined,
|
||||
): event is Extract<SessionEvent, { type: "assistant.message" }> {
|
||||
return event?.type === "assistant.message";
|
||||
}
|
||||
|
||||
function isRootSessionEvent(event: { agentId?: string }): boolean {
|
||||
return event.agentId === undefined;
|
||||
}
|
||||
|
||||
function isRootCompactionEvent(event: { agentId?: string }): boolean {
|
||||
// SDK session events include subagent compaction; only root compaction
|
||||
// affects the pooled root session's cleanup and reuse lifecycle.
|
||||
return isRootSessionEvent(event);
|
||||
}
|
||||
|
||||
function joinReasoning(order: string[], reasoningById: Map<string, string>): string {
|
||||
return order.map((reasoningId) => reasoningById.get(reasoningId) ?? "").join("");
|
||||
}
|
||||
|
||||
function splitPlanText(text: string | undefined): string[] {
|
||||
return (text ?? "")
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim().replace(/^[-*]\s+/, ""))
|
||||
.filter((line) => line.length > 0);
|
||||
}
|
||||
|
||||
function readString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.length > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function registerListener<K extends SessionEventType>(
|
||||
session: SessionLike,
|
||||
unsubscribeFns: Array<() => void>,
|
||||
eventType: K,
|
||||
handler: (event: Extract<SessionEvent, { type: K }>) => void,
|
||||
): void {
|
||||
const maybeUnsubscribe = session.on(eventType, handler);
|
||||
if (typeof maybeUnsubscribe === "function") {
|
||||
unsubscribeFns.push(maybeUnsubscribe);
|
||||
return;
|
||||
}
|
||||
unsubscribeFns.push(() => {
|
||||
session.off?.(eventType, handler as (...args: unknown[]) => void);
|
||||
});
|
||||
}
|
||||
|
||||
function toLintErrorObject(value: unknown, fallbackMessage: string): Error {
|
||||
if (value instanceof Error) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return new Error(value);
|
||||
}
|
||||
const error = new Error(fallbackMessage, { cause: value });
|
||||
if ((typeof value === "object" && value !== null) || typeof value === "function") {
|
||||
Object.assign(error, value);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
Reference in New Issue
Block a user