@borgee/agents-host 0.2.62 → 0.2.65
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/dist/agents-host.d.ts +3 -1
- package/dist/agents-host.js +126 -11
- package/dist/chat/chat-control-plane.d.ts +2 -1
- package/dist/chat/sdk-chat-control-plane.d.ts +3 -2
- package/dist/chat/sdk-chat-control-plane.js +4 -0
- package/dist/providers/claude/adapter.d.ts +1 -0
- package/dist/providers/claude/adapter.js +3 -0
- package/dist/providers/claude/cli-client.d.ts +1 -0
- package/dist/providers/claude/cli-client.js +23 -0
- package/dist/providers/codex/adapter.d.ts +1 -0
- package/dist/providers/codex/adapter.js +3 -0
- package/dist/providers/codex/cli-client.d.ts +1 -0
- package/dist/providers/codex/cli-client.js +25 -1
- package/dist/providers/copilot/adapter.d.ts +1 -0
- package/dist/providers/copilot/adapter.js +3 -0
- package/dist/providers/copilot/cli-client.d.ts +1 -0
- package/dist/providers/copilot/cli-client.js +21 -0
- package/dist/providers/create-provider.js +6 -0
- package/dist/providers/provider-adapter.d.ts +7 -0
- package/dist/providers/provider-adapter.js +8 -0
- package/package.json +1 -1
package/dist/agents-host.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ChatControlPlane } from './chat/chat-control-plane.js';
|
|
2
2
|
import { type AgentsHostRuntimeOptions } from './debug.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type ProviderAdapter } from './providers/provider-adapter.js';
|
|
4
4
|
import type { AgentsHostConfig } from './types.js';
|
|
5
5
|
/**
|
|
6
6
|
* Minimal single-agent agents host: connects one local Claude/Copilot CLI
|
|
@@ -160,6 +160,8 @@ export declare class AgentsHost {
|
|
|
160
160
|
private readCollaborationDraft;
|
|
161
161
|
private supersedeActiveTurn;
|
|
162
162
|
private invalidateChannelTurn;
|
|
163
|
+
private stopTurn;
|
|
164
|
+
private responseStateForExecuted;
|
|
163
165
|
private rememberRecentTurn;
|
|
164
166
|
private resolveAuthorizedCollaborationTurn;
|
|
165
167
|
private updateAwaitingUserState;
|
package/dist/agents-host.js
CHANGED
|
@@ -12,6 +12,7 @@ import { createLocalhostGatewayController, LOCALHOST_GATEWAY_COLLABORATION_TARGE
|
|
|
12
12
|
import { extractTaskIdFromTaskAssignmentContent, resolveTaskAssignmentStatePath, } from './context/injection.js';
|
|
13
13
|
import { AuthorizationAuditSink, } from './policy/authorization-audit.js';
|
|
14
14
|
import { createProvider } from './providers/create-provider.js';
|
|
15
|
+
import { ProviderTurnCancelledError } from './providers/provider-adapter.js';
|
|
15
16
|
import { resolveAttentionStatePath, resolveCompactionStatePath, resolveSharedProtocolKickoffDecisionPath, resolveSharedProtocolStatusPath, } from './state-paths.js';
|
|
16
17
|
import { appendVisibleMention as appendVisibleBodyMention, extractVisibleMentionIds as extractCanonicalMentionIds, } from './visible-mentions.js';
|
|
17
18
|
import { buildHostedIncomingContentText, buildHostedTurnContentParts, extractHostedMessageAttachments, normalizeHostedMessageAttachment, } from './hosted-turn-content.js';
|
|
@@ -24,6 +25,9 @@ const PROTOCOL_KICKOFF_DECISION_POLL_INTERVAL_MS = 50;
|
|
|
24
25
|
const PROTOCOL_KICKOFF_ROUND_LIMIT = 20;
|
|
25
26
|
const COLLABORATION_LATE_SEND_GRACE_MS = 5_000;
|
|
26
27
|
const ORDINARY_HUMAN_EVENT_KINDS = new Set(['', 'message']);
|
|
28
|
+
const TASK_EXECUTION_LOCAL_DIRECTORY_PROPERTY_KEY = 'execution.local_directory';
|
|
29
|
+
const TASK_EXECUTION_LOCAL_DIRECTORY_REMINDER = 'This task has no Execution Local Directory yet. Set it before asking me to work in a real local project directory.';
|
|
30
|
+
const TASK_EXECUTION_LOCAL_DIRECTORY_IN_USE_PREFIX = 'Using Execution Local Directory:';
|
|
27
31
|
async function ensurePrivateStateRoot(path) {
|
|
28
32
|
await mkdir(path, { recursive: true, mode: PRIVATE_STATE_ROOT_MODE });
|
|
29
33
|
await chmod(path, PRIVATE_STATE_ROOT_MODE);
|
|
@@ -145,13 +149,15 @@ class DefaultHostRuntime {
|
|
|
145
149
|
});
|
|
146
150
|
this.ensureStateRoot = deps.ensureStateRoot ?? ensurePrivateStateRoot;
|
|
147
151
|
}
|
|
148
|
-
async start(onMessage) {
|
|
152
|
+
async start(onMessage, onStopTurn) {
|
|
149
153
|
await this.ensureStateRoot(this.config.stateRootDir);
|
|
150
154
|
await this.gateway.start();
|
|
151
155
|
try {
|
|
156
|
+
this.controlPlane.onStopTurn?.(onStopTurn);
|
|
152
157
|
await this.controlPlane.connect(onMessage);
|
|
153
158
|
}
|
|
154
159
|
catch (error) {
|
|
160
|
+
this.controlPlane.onStopTurn?.(undefined);
|
|
155
161
|
await this.gateway.stop().catch(() => { });
|
|
156
162
|
throw error;
|
|
157
163
|
}
|
|
@@ -159,6 +165,7 @@ class DefaultHostRuntime {
|
|
|
159
165
|
async stop() {
|
|
160
166
|
let thrown;
|
|
161
167
|
try {
|
|
168
|
+
this.controlPlane.onStopTurn?.(undefined);
|
|
162
169
|
await this.controlPlane.close();
|
|
163
170
|
}
|
|
164
171
|
catch (error) {
|
|
@@ -366,7 +373,7 @@ export class AgentsHost {
|
|
|
366
373
|
return;
|
|
367
374
|
}
|
|
368
375
|
this.dispatchMessage(message);
|
|
369
|
-
});
|
|
376
|
+
}, async (channelId) => this.stopTurn(channelId));
|
|
370
377
|
const agentId = await this.ensureSelfAgentId();
|
|
371
378
|
bufferingMessages = false;
|
|
372
379
|
while (bufferedMessages.length > 0) {
|
|
@@ -675,7 +682,7 @@ export class AgentsHost {
|
|
|
675
682
|
return null;
|
|
676
683
|
}
|
|
677
684
|
this.recordCollaborationOutcome(msg.channel_id, {
|
|
678
|
-
responseState: executed
|
|
685
|
+
responseState: this.responseStateForExecuted(executed),
|
|
679
686
|
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
680
687
|
});
|
|
681
688
|
this.updateMissedCollaborationDiagnostic(msg.channel_id, priorMissedCollaborationDiagnostic);
|
|
@@ -1110,7 +1117,7 @@ export class AgentsHost {
|
|
|
1110
1117
|
channelState.blocked = undefined;
|
|
1111
1118
|
this.awaitingUserByChannel.delete(channelId);
|
|
1112
1119
|
this.recordCollaborationOutcome(channelId, {
|
|
1113
|
-
responseState: executed
|
|
1120
|
+
responseState: this.responseStateForExecuted(executed),
|
|
1114
1121
|
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
1115
1122
|
...(message.wakeBlockedState ? { wakeState: 'resumed-human' } : {}),
|
|
1116
1123
|
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
@@ -1174,7 +1181,7 @@ export class AgentsHost {
|
|
|
1174
1181
|
const visibleMessageId = deliveredMessageId ?? protocolState.lastProtocolMessageId;
|
|
1175
1182
|
if (executed.reply?.controlMalformed) {
|
|
1176
1183
|
this.recordCollaborationOutcome(channelId, {
|
|
1177
|
-
responseState: executed
|
|
1184
|
+
responseState: this.responseStateForExecuted(executed),
|
|
1178
1185
|
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
1179
1186
|
...(message.wakeBlockedState ? { wakeState: 'resumed-human' } : {}),
|
|
1180
1187
|
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
@@ -1184,7 +1191,7 @@ export class AgentsHost {
|
|
|
1184
1191
|
}
|
|
1185
1192
|
if (!executed.reply?.control) {
|
|
1186
1193
|
this.recordCollaborationOutcome(channelId, {
|
|
1187
|
-
responseState: executed
|
|
1194
|
+
responseState: this.responseStateForExecuted(executed),
|
|
1188
1195
|
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
1189
1196
|
...(message.wakeBlockedState ? { wakeState: 'resumed-human' } : {}),
|
|
1190
1197
|
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
@@ -1218,7 +1225,7 @@ export class AgentsHost {
|
|
|
1218
1225
|
}
|
|
1219
1226
|
if (executed.reply.control.kind === 'start-protocol') {
|
|
1220
1227
|
this.recordCollaborationOutcome(channelId, {
|
|
1221
|
-
responseState: executed
|
|
1228
|
+
responseState: this.responseStateForExecuted(executed),
|
|
1222
1229
|
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
1223
1230
|
...(message.wakeBlockedState ? { wakeState: 'resumed-human' } : {}),
|
|
1224
1231
|
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
@@ -1229,7 +1236,7 @@ export class AgentsHost {
|
|
|
1229
1236
|
}
|
|
1230
1237
|
if (executed.reply.control.kind === 'attention-only') {
|
|
1231
1238
|
this.recordCollaborationOutcome(channelId, {
|
|
1232
|
-
responseState: executed
|
|
1239
|
+
responseState: this.responseStateForExecuted(executed),
|
|
1233
1240
|
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
1234
1241
|
...(message.wakeBlockedState ? { wakeState: 'resumed-human' } : {}),
|
|
1235
1242
|
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
@@ -2188,9 +2195,7 @@ export class AgentsHost {
|
|
|
2188
2195
|
this.recordCollaborationOutcome(msg.channel_id, {
|
|
2189
2196
|
responseState: executed.completed && executed.reply?.awaitingUser
|
|
2190
2197
|
? 'blocked'
|
|
2191
|
-
: executed
|
|
2192
|
-
? 'responded'
|
|
2193
|
-
: 'failed',
|
|
2198
|
+
: this.responseStateForExecuted(executed),
|
|
2194
2199
|
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
2195
2200
|
...(executed.completed && executed.reply?.awaitingUser
|
|
2196
2201
|
? {
|
|
@@ -2444,6 +2449,65 @@ export class AgentsHost {
|
|
|
2444
2449
|
turnExecutionId: activeTurn.turnExecutionId,
|
|
2445
2450
|
});
|
|
2446
2451
|
}
|
|
2452
|
+
if (error instanceof ProviderTurnCancelledError) {
|
|
2453
|
+
const cancelledReplyText = error.publicReplyText?.trim();
|
|
2454
|
+
if (!silentTurn &&
|
|
2455
|
+
!deferDelivery &&
|
|
2456
|
+
typeof cancelledReplyText === 'string' &&
|
|
2457
|
+
cancelledReplyText.length > 0 &&
|
|
2458
|
+
this.started &&
|
|
2459
|
+
!this.controlPlaneClosed) {
|
|
2460
|
+
reply = { text: cancelledReplyText };
|
|
2461
|
+
publicReplyText = cancelledReplyText;
|
|
2462
|
+
visibleReplyBody = this.resolveVisibleTurnBody(activeTurn, reply, publicReplyText);
|
|
2463
|
+
try {
|
|
2464
|
+
const posted = await this.borgee.postMessage({
|
|
2465
|
+
channelId,
|
|
2466
|
+
body: visibleReplyBody,
|
|
2467
|
+
replyToId: activeTurn?.delivery?.replyToId,
|
|
2468
|
+
});
|
|
2469
|
+
this.logger.debug('posted cancelled turn feedback', {
|
|
2470
|
+
...turnContext,
|
|
2471
|
+
durationMs: Date.now() - turnStartedAt,
|
|
2472
|
+
replyLength: publicReplyText.length,
|
|
2473
|
+
delivery: 'post',
|
|
2474
|
+
});
|
|
2475
|
+
return {
|
|
2476
|
+
reply,
|
|
2477
|
+
deliveredMessageId: posted.messageId,
|
|
2478
|
+
publicReplyText,
|
|
2479
|
+
visibleReplyBody,
|
|
2480
|
+
deliveryState: 'posted',
|
|
2481
|
+
stale: activeTurn?.superseded === true,
|
|
2482
|
+
completed: false,
|
|
2483
|
+
cancelled: true,
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
catch (postError) {
|
|
2487
|
+
this.logger.debugError('failed to post cancelled turn feedback', {
|
|
2488
|
+
...turnContext,
|
|
2489
|
+
error: postError,
|
|
2490
|
+
});
|
|
2491
|
+
this.logger.error('failed to post cancelled turn feedback', {
|
|
2492
|
+
...turnContext,
|
|
2493
|
+
error: summarizeError(postError),
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
this.logger.debug('cancelled turn', {
|
|
2498
|
+
...turnContext,
|
|
2499
|
+
durationMs: Date.now() - turnStartedAt,
|
|
2500
|
+
});
|
|
2501
|
+
return {
|
|
2502
|
+
reply,
|
|
2503
|
+
publicReplyText,
|
|
2504
|
+
visibleReplyBody,
|
|
2505
|
+
deliveryState: 'not-delivered',
|
|
2506
|
+
stale: activeTurn?.superseded === true,
|
|
2507
|
+
completed: false,
|
|
2508
|
+
cancelled: true,
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2447
2511
|
this.logger.debugError('failed to generate or send reply', {
|
|
2448
2512
|
...turnContext,
|
|
2449
2513
|
error,
|
|
@@ -2655,6 +2719,29 @@ export class AgentsHost {
|
|
|
2655
2719
|
activeTurn.turnExecutionId = null;
|
|
2656
2720
|
}
|
|
2657
2721
|
}
|
|
2722
|
+
async stopTurn(channelId) {
|
|
2723
|
+
const activeTurn = this.collaborationChannels.get(channelId)?.activeTurn;
|
|
2724
|
+
const aborted = await this.provider.cancelTurn?.(channelId) ?? false;
|
|
2725
|
+
if (!aborted) {
|
|
2726
|
+
return { aborted: false };
|
|
2727
|
+
}
|
|
2728
|
+
if (activeTurn) {
|
|
2729
|
+
this.supersedeActiveTurn(channelId, activeTurn);
|
|
2730
|
+
}
|
|
2731
|
+
else {
|
|
2732
|
+
this.recordCollaborationOutcome(channelId, {
|
|
2733
|
+
responseState: 'superseded',
|
|
2734
|
+
deliveryState: 'not-delivered',
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
return { aborted: true };
|
|
2738
|
+
}
|
|
2739
|
+
responseStateForExecuted(executed) {
|
|
2740
|
+
if (executed.cancelled) {
|
|
2741
|
+
return 'superseded';
|
|
2742
|
+
}
|
|
2743
|
+
return executed.completed ? 'responded' : 'failed';
|
|
2744
|
+
}
|
|
2658
2745
|
rememberRecentTurn(channelState, activeTurn) {
|
|
2659
2746
|
if (!activeTurn.turnExecutionId) {
|
|
2660
2747
|
channelState.recentTurn = undefined;
|
|
@@ -2853,6 +2940,34 @@ export class AgentsHost {
|
|
|
2853
2940
|
task = null;
|
|
2854
2941
|
}
|
|
2855
2942
|
if (task && task.assigneeId === selfAgentId) {
|
|
2943
|
+
const executionLocalDirectory = task.properties?.[TASK_EXECUTION_LOCAL_DIRECTORY_PROPERTY_KEY];
|
|
2944
|
+
if (typeof executionLocalDirectory !== 'string' || executionLocalDirectory.trim().length === 0) {
|
|
2945
|
+
try {
|
|
2946
|
+
await this.borgee.postMessage({
|
|
2947
|
+
channelId: msg.channel_id,
|
|
2948
|
+
body: TASK_EXECUTION_LOCAL_DIRECTORY_REMINDER,
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2951
|
+
catch (error) {
|
|
2952
|
+
this.logger.debugError('task local directory reminder post failed', {
|
|
2953
|
+
channelId: msg.channel_id,
|
|
2954
|
+
error,
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
return null;
|
|
2958
|
+
}
|
|
2959
|
+
try {
|
|
2960
|
+
await this.borgee.postMessage({
|
|
2961
|
+
channelId: msg.channel_id,
|
|
2962
|
+
body: `${TASK_EXECUTION_LOCAL_DIRECTORY_IN_USE_PREFIX} ${executionLocalDirectory}`,
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
catch (error) {
|
|
2966
|
+
this.logger.debugError('task local directory notice post failed', {
|
|
2967
|
+
channelId: msg.channel_id,
|
|
2968
|
+
error,
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2856
2971
|
if (task.status === 'open') {
|
|
2857
2972
|
try {
|
|
2858
2973
|
await this.borgee.updateTask({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { ReportTurnActivityInput, TurnActivityReporter } from '../plugin-sdk.js';
|
|
1
|
+
import type { ReportTurnActivityInput, StopTurnHandler, TurnActivityReporter } from '../plugin-sdk.js';
|
|
2
2
|
import type { ChannelSummary, ChannelHistoryEntry, ChannelMessageEvent, CreateTaskInput, DirectoryUser, MeResponseUser, PostMessageInput, PostedMessage, ReadChannelHistoryInput, Task, UpdateTaskInput } from '../types.js';
|
|
3
3
|
export interface ChatControlPlane {
|
|
4
|
+
onStopTurn?(handler: StopTurnHandler | undefined): void;
|
|
4
5
|
connect(onMessage: (message: ChannelMessageEvent) => void): Promise<void>;
|
|
5
6
|
close(): Promise<void>;
|
|
6
7
|
postMessage(input: PostMessageInput): Promise<PostedMessage>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type BorgeePluginClient, type BorgeePluginOptions, type InboundMessageEvent, type ReportTurnActivityInput, type TurnActivityReporter } from '../plugin-sdk.js';
|
|
1
|
+
import { type BorgeePluginClient, type BorgeePluginOptions, type InboundMessageEvent, type ReportTurnActivityInput, type StopTurnHandler, type TurnActivityReporter } from '../plugin-sdk.js';
|
|
2
2
|
import type { ChannelSummary, ChannelHistoryEntry, ChannelMessageEvent, CreateTaskInput, DirectoryUser, MeResponseUser, PostMessageInput, PostedMessage, ReadChannelHistoryInput, Task, UpdateTaskInput } from '../types.js';
|
|
3
3
|
import type { ChatControlPlane } from './chat-control-plane.js';
|
|
4
|
-
type PluginClientLike = Pick<BorgeePluginClient, 'agentId' | 'close' | 'connect' | 'createTask' | 'deleteMessage' | 'editMessage' | 'getMe' | 'getTask' | 'listChannels' | 'listTasks' | 'listUsers' | 'on' | 'readHistory' | 'reportTurnActivity' | 'sendMessage' | 'startTyping' | 'updateTask' | 'setTaskProperty' | 'deleteTaskProperty'>;
|
|
4
|
+
type PluginClientLike = Pick<BorgeePluginClient, 'agentId' | 'close' | 'connect' | 'createTask' | 'deleteMessage' | 'editMessage' | 'getMe' | 'getTask' | 'listChannels' | 'listTasks' | 'listUsers' | 'on' | 'onStopTurn' | 'readHistory' | 'reportTurnActivity' | 'sendMessage' | 'startTyping' | 'updateTask' | 'setTaskProperty' | 'deleteTaskProperty'>;
|
|
5
5
|
type PluginClientFactory = (options: BorgeePluginOptions) => PluginClientLike;
|
|
6
6
|
type SdkChatControlPlaneOptions = Pick<BorgeePluginOptions, 'pluginId'>;
|
|
7
7
|
/**
|
|
@@ -16,6 +16,7 @@ export declare class SdkChatControlPlane implements ChatControlPlane {
|
|
|
16
16
|
private connected;
|
|
17
17
|
private me;
|
|
18
18
|
constructor(baseUrl: string, apiKey: string, createClient?: PluginClientFactory, options?: SdkChatControlPlaneOptions);
|
|
19
|
+
onStopTurn(handler: StopTurnHandler | undefined): void;
|
|
19
20
|
connect(onMessage: (message: ChannelMessageEvent) => void): Promise<void>;
|
|
20
21
|
close(): Promise<void>;
|
|
21
22
|
postMessage(input: PostMessageInput): Promise<PostedMessage>;
|
|
@@ -14,6 +14,9 @@ export class SdkChatControlPlane {
|
|
|
14
14
|
constructor(baseUrl, apiKey, createClient = createBorgeePlugin, options = {}) {
|
|
15
15
|
this.client = createClient({ baseUrl, apiKey, ...options });
|
|
16
16
|
}
|
|
17
|
+
onStopTurn(handler) {
|
|
18
|
+
this.client.onStopTurn(handler);
|
|
19
|
+
}
|
|
17
20
|
async connect(onMessage) {
|
|
18
21
|
this.unsubscribe = this.client.on('message', (event) => {
|
|
19
22
|
const message = mapInboundToChannelMessage(event);
|
|
@@ -46,6 +49,7 @@ export class SdkChatControlPlane {
|
|
|
46
49
|
this.pendingMessages = [];
|
|
47
50
|
this.unsubscribe?.();
|
|
48
51
|
this.unsubscribe = null;
|
|
52
|
+
this.client.onStopTurn(undefined);
|
|
49
53
|
await this.client.close();
|
|
50
54
|
}
|
|
51
55
|
async postMessage(input) {
|
|
@@ -9,5 +9,6 @@ export declare class ClaudeProviderAdapter implements ProviderAdapter {
|
|
|
9
9
|
readonly capabilities: import("../provider-adapter.js").ProviderCapabilities;
|
|
10
10
|
constructor(cli: ClaudeCliClient, turnPreparer: ProviderTurnPreparer);
|
|
11
11
|
generateReply(input: ProviderInput, options?: ProviderGenerateOptions): Promise<ProviderReply>;
|
|
12
|
+
cancelTurn(channelId: string): Promise<boolean>;
|
|
12
13
|
dispose(): Promise<void>;
|
|
13
14
|
}
|
|
@@ -25,6 +25,9 @@ export class ClaudeProviderAdapter {
|
|
|
25
25
|
// session as it runs, so before this point there is nothing to report.
|
|
26
26
|
return { ...parseProviderReply(text), sessionId: this.cli.sessionIdForChannel(input.channelId) };
|
|
27
27
|
}
|
|
28
|
+
async cancelTurn(channelId) {
|
|
29
|
+
return this.cli.cancelTurn(channelId);
|
|
30
|
+
}
|
|
28
31
|
async dispose() {
|
|
29
32
|
await this.cli.dispose();
|
|
30
33
|
}
|
|
@@ -43,6 +43,7 @@ export declare class ClaudeCliClient {
|
|
|
43
43
|
* worked a task without the agent being asked to report its own id.
|
|
44
44
|
*/
|
|
45
45
|
sessionIdForChannel(channelId: string): string | undefined;
|
|
46
|
+
cancelTurn(channelId: string): Promise<boolean>;
|
|
46
47
|
private readonly persistedSessions;
|
|
47
48
|
private readonly closingSessions;
|
|
48
49
|
private readonly pendingSessionStarts;
|
|
@@ -9,6 +9,7 @@ import { assertClaudeCommandCompatibility, DEFAULT_CLAUDE_COMMAND, isLegacyClaud
|
|
|
9
9
|
import { PROTOCOL_VERSION, client, methods, ndJsonStream, } from '@agentclientprotocol/sdk';
|
|
10
10
|
import { HostLogger, summarizeChildStderr, summarizeError } from '../../debug.js';
|
|
11
11
|
import { AcpProgressCollector } from '../acp-progress-collector.js';
|
|
12
|
+
import { ProviderTurnCancelledError } from '../provider-adapter.js';
|
|
12
13
|
import { readClaudeActivityMetadata } from './activity-metadata.js';
|
|
13
14
|
import { resolveCopilotPermissionResponse } from '../../policy/copilot-permission.js';
|
|
14
15
|
import { buildClaudeSessionPromptAppend } from '../../context/prompt.js';
|
|
@@ -32,6 +33,7 @@ const DEFAULT_SESSION_CAPABILITIES = {
|
|
|
32
33
|
};
|
|
33
34
|
const CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD_ENV = 'CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD';
|
|
34
35
|
const require = createRequire(import.meta.url);
|
|
36
|
+
const CLAUDE_CANCELLED_REPLY_TEXT = 'Info: Operation cancelled by user';
|
|
35
37
|
const DEFAULT_RUNTIME = {
|
|
36
38
|
spawn,
|
|
37
39
|
client,
|
|
@@ -495,6 +497,22 @@ export class ClaudeCliClient {
|
|
|
495
497
|
sessionIdForChannel(channelId) {
|
|
496
498
|
return this.channels.get(channelId)?.session?.sessionId;
|
|
497
499
|
}
|
|
500
|
+
async cancelTurn(channelId) {
|
|
501
|
+
const state = this.channels.get(channelId);
|
|
502
|
+
const session = state?.session;
|
|
503
|
+
if (!state?.activeTurn || !session) {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
this.idleBackendShutdown.cancel();
|
|
507
|
+
const activeSession = session;
|
|
508
|
+
if (typeof activeSession.cancel === 'function') {
|
|
509
|
+
await activeSession.cancel({ sessionId: session.sessionId });
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
const cancelMethod = this.runtime.methods.agent.session?.cancel ?? 'session/cancel';
|
|
513
|
+
await this.connection?.agent.notify(cancelMethod, { sessionId: session.sessionId });
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
498
516
|
persistedSessions = new Map();
|
|
499
517
|
closingSessions = new WeakSet();
|
|
500
518
|
pendingSessionStarts = new Set();
|
|
@@ -975,6 +993,11 @@ export class ClaudeCliClient {
|
|
|
975
993
|
throw markSessionTainted(error);
|
|
976
994
|
}
|
|
977
995
|
const output = collector.getFinalText();
|
|
996
|
+
if (response.stopReason === 'cancelled') {
|
|
997
|
+
throw new ProviderTurnCancelledError('Claude ACP turn cancelled', {
|
|
998
|
+
publicReplyText: hasVisibleText(output) ? output : CLAUDE_CANCELLED_REPLY_TEXT,
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
978
1001
|
if (response.stopReason !== 'end_turn' && !hasVisibleText(output)) {
|
|
979
1002
|
throw new Error(`Claude ACP turn stopped with stopReason "${response.stopReason}"`);
|
|
980
1003
|
}
|
|
@@ -9,5 +9,6 @@ export declare class CodexProviderAdapter implements ProviderAdapter {
|
|
|
9
9
|
readonly capabilities: import("../provider-adapter.js").ProviderCapabilities;
|
|
10
10
|
constructor(cli: CodexCliClient, turnPreparer: ProviderTurnPreparer);
|
|
11
11
|
generateReply(input: ProviderInput, options?: ProviderGenerateOptions): Promise<ProviderReply>;
|
|
12
|
+
cancelTurn(channelId: string): Promise<boolean>;
|
|
12
13
|
dispose(): Promise<void>;
|
|
13
14
|
}
|
|
@@ -25,6 +25,9 @@ export class CodexProviderAdapter {
|
|
|
25
25
|
// session as it runs, so before this point there is nothing to report.
|
|
26
26
|
return { ...parseProviderReply(text), sessionId: this.cli.sessionIdForChannel(input.channelId) };
|
|
27
27
|
}
|
|
28
|
+
async cancelTurn(channelId) {
|
|
29
|
+
return this.cli.cancelTurn(channelId);
|
|
30
|
+
}
|
|
28
31
|
async dispose() {
|
|
29
32
|
await this.cli.dispose();
|
|
30
33
|
}
|
|
@@ -49,6 +49,7 @@ export declare class CodexCliClient {
|
|
|
49
49
|
* worked a task without the agent being asked to report its own id.
|
|
50
50
|
*/
|
|
51
51
|
sessionIdForChannel(channelId: string): string | undefined;
|
|
52
|
+
cancelTurn(channelId: string): Promise<boolean>;
|
|
52
53
|
private readonly persistedSessions;
|
|
53
54
|
private readonly closingSessions;
|
|
54
55
|
private readonly pendingSessionStarts;
|
|
@@ -6,6 +6,7 @@ import spawn from 'cross-spawn';
|
|
|
6
6
|
import { PROTOCOL_VERSION, client, methods, ndJsonStream, } from '@agentclientprotocol/sdk';
|
|
7
7
|
import { HostLogger, summarizeChildStderr, summarizeError } from '../../debug.js';
|
|
8
8
|
import { AcpProgressCollector } from '../acp-progress-collector.js';
|
|
9
|
+
import { ProviderTurnCancelledError } from '../provider-adapter.js';
|
|
9
10
|
import { assertCodexProjectDocumentSize, buildCodexProjectDocument } from './project-doc.js';
|
|
10
11
|
import { isGatewayCredentialSidecarBasename } from '../../context/injection.js';
|
|
11
12
|
import { isDiscussionOnlyResolvedWorkspace } from '../../context/resolved-workspace.js';
|
|
@@ -34,6 +35,7 @@ const CODEX_CONTEXT_ROOT_DIRNAME = 'codex-context';
|
|
|
34
35
|
const PROJECTION_UNAVAILABLE_PLACEHOLDER = '[codex-projection-unavailable]';
|
|
35
36
|
const QUEUED_TURN_DROPPED_MESSAGE = 'Codex ACP session was reset after a failed turn; queued turns were dropped instead of replaying them on a fresh session';
|
|
36
37
|
const require = createRequire(import.meta.url);
|
|
38
|
+
const CODEX_CANCELLED_REPLY_TEXT = 'Info: Operation cancelled by user';
|
|
37
39
|
const DEFAULT_RUNTIME = {
|
|
38
40
|
spawn,
|
|
39
41
|
client,
|
|
@@ -230,6 +232,23 @@ export class CodexCliClient {
|
|
|
230
232
|
sessionIdForChannel(channelId) {
|
|
231
233
|
return this.channels.get(channelId)?.session?.sessionId;
|
|
232
234
|
}
|
|
235
|
+
async cancelTurn(channelId) {
|
|
236
|
+
const state = this.channels.get(channelId);
|
|
237
|
+
const session = state?.session;
|
|
238
|
+
if (!state?.activeTurn || !session) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
this.clearIdleTimer(state);
|
|
242
|
+
this.idleBackendShutdown.cancel();
|
|
243
|
+
const activeSession = session;
|
|
244
|
+
if (typeof activeSession.cancel === 'function') {
|
|
245
|
+
await activeSession.cancel({ sessionId: session.sessionId });
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
const cancelMethod = this.runtime.methods.agent.session?.cancel ?? 'session/cancel';
|
|
249
|
+
await this.connection?.agent.notify(cancelMethod, { sessionId: session.sessionId });
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
233
252
|
persistedSessions = new Map();
|
|
234
253
|
closingSessions = new WeakSet();
|
|
235
254
|
pendingSessionStarts = new Set();
|
|
@@ -831,10 +850,15 @@ export class CodexCliClient {
|
|
|
831
850
|
catch (error) {
|
|
832
851
|
throw markSessionTainted(error);
|
|
833
852
|
}
|
|
853
|
+
const output = collector.getFinalText();
|
|
854
|
+
if (response.stopReason === 'cancelled') {
|
|
855
|
+
throw new ProviderTurnCancelledError('Codex ACP turn cancelled', {
|
|
856
|
+
publicReplyText: output || CODEX_CANCELLED_REPLY_TEXT,
|
|
857
|
+
});
|
|
858
|
+
}
|
|
834
859
|
if (response.stopReason !== 'end_turn') {
|
|
835
860
|
throw new Error(`Codex ACP turn stopped with stopReason "${response.stopReason}"`);
|
|
836
861
|
}
|
|
837
|
-
const output = collector.getFinalText();
|
|
838
862
|
if (!output) {
|
|
839
863
|
throw new Error('Codex ACP returned empty output');
|
|
840
864
|
}
|
|
@@ -9,5 +9,6 @@ export declare class CopilotProviderAdapter implements ProviderAdapter {
|
|
|
9
9
|
readonly capabilities: import("../provider-adapter.js").ProviderCapabilities;
|
|
10
10
|
constructor(cli: CopilotCliClient, turnPreparer: ProviderTurnPreparer);
|
|
11
11
|
generateReply(input: ProviderInput, options?: ProviderGenerateOptions): Promise<ProviderReply>;
|
|
12
|
+
cancelTurn(channelId: string): Promise<boolean>;
|
|
12
13
|
dispose(): Promise<void>;
|
|
13
14
|
}
|
|
@@ -25,6 +25,9 @@ export class CopilotProviderAdapter {
|
|
|
25
25
|
// session as it runs, so before this point there is nothing to report.
|
|
26
26
|
return { ...parseProviderReply(text), sessionId: this.cli.sessionIdForChannel(input.channelId) };
|
|
27
27
|
}
|
|
28
|
+
async cancelTurn(channelId) {
|
|
29
|
+
return this.cli.cancelTurn(channelId);
|
|
30
|
+
}
|
|
28
31
|
async dispose() {
|
|
29
32
|
await this.cli.dispose();
|
|
30
33
|
}
|
|
@@ -51,6 +51,7 @@ export declare class CopilotCliClient {
|
|
|
51
51
|
* worked a task without the agent being asked to report its own id.
|
|
52
52
|
*/
|
|
53
53
|
sessionIdForChannel(channelId: string): string | undefined;
|
|
54
|
+
cancelTurn(channelId: string): Promise<boolean>;
|
|
54
55
|
private readonly persistedSessions;
|
|
55
56
|
private readonly closingSessions;
|
|
56
57
|
private readonly pendingSessionStarts;
|
|
@@ -3,6 +3,7 @@ import spawn from 'cross-spawn';
|
|
|
3
3
|
import { PROTOCOL_VERSION, client, methods, ndJsonStream, } from '@agentclientprotocol/sdk';
|
|
4
4
|
import { HostLogger, summarizeChildStderr, summarizeError } from '../../debug.js';
|
|
5
5
|
import { AcpProgressCollector } from '../acp-progress-collector.js';
|
|
6
|
+
import { ProviderTurnCancelledError } from '../provider-adapter.js';
|
|
6
7
|
import { resolveCopilotPermissionResponse } from '../../policy/copilot-permission.js';
|
|
7
8
|
import { isDiscussionOnlyResolvedWorkspace } from '../../context/resolved-workspace.js';
|
|
8
9
|
import { IDLE_BACKEND_SHUTDOWN_DISABLED_MS, IdleBackendShutdownScheduler, } from '../idle-backend-shutdown.js';
|
|
@@ -170,6 +171,23 @@ export class CopilotCliClient {
|
|
|
170
171
|
sessionIdForChannel(channelId) {
|
|
171
172
|
return this.channels.get(channelId)?.session?.sessionId;
|
|
172
173
|
}
|
|
174
|
+
async cancelTurn(channelId) {
|
|
175
|
+
const state = this.channels.get(channelId);
|
|
176
|
+
const session = state?.session;
|
|
177
|
+
if (!state?.activeTurn || !session) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
this.clearIdleTimer(state);
|
|
181
|
+
this.idleBackendShutdown.cancel();
|
|
182
|
+
const activeSession = session;
|
|
183
|
+
if (typeof activeSession.cancel === 'function') {
|
|
184
|
+
await activeSession.cancel({ sessionId: session.sessionId });
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
const cancelMethod = this.runtime.methods.agent.session?.cancel ?? 'session/cancel';
|
|
188
|
+
await this.connection?.agent.notify(cancelMethod, { sessionId: session.sessionId });
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
173
191
|
persistedSessions = new Map();
|
|
174
192
|
closingSessions = new WeakSet();
|
|
175
193
|
pendingSessionStarts = new Set();
|
|
@@ -651,6 +669,9 @@ export class CopilotCliClient {
|
|
|
651
669
|
catch (error) {
|
|
652
670
|
throw markSessionTainted(error);
|
|
653
671
|
}
|
|
672
|
+
if (response.stopReason === 'cancelled') {
|
|
673
|
+
throw new ProviderTurnCancelledError('Copilot ACP turn cancelled');
|
|
674
|
+
}
|
|
654
675
|
if (response.stopReason !== 'end_turn') {
|
|
655
676
|
throw new Error(`Copilot ACP turn stopped with stopReason "${response.stopReason}"`);
|
|
656
677
|
}
|
|
@@ -30,6 +30,9 @@ class ProviderV2CompatibilityAdapter {
|
|
|
30
30
|
turn: preparedTurn,
|
|
31
31
|
}, options);
|
|
32
32
|
}
|
|
33
|
+
async cancelTurn(channelId) {
|
|
34
|
+
return this.provider.cancelTurn?.(channelId) ?? false;
|
|
35
|
+
}
|
|
33
36
|
async dispose() {
|
|
34
37
|
await this.provider.shutdown?.();
|
|
35
38
|
}
|
|
@@ -47,6 +50,9 @@ class CliBackedProviderV2 {
|
|
|
47
50
|
// session as it runs, so before this point there is nothing to report.
|
|
48
51
|
return { ...parseProviderReply(text), sessionId: this.cli.sessionIdForChannel(input.turn.channelId) };
|
|
49
52
|
}
|
|
53
|
+
async cancelTurn(channelId) {
|
|
54
|
+
return this.cli.cancelTurn(channelId);
|
|
55
|
+
}
|
|
50
56
|
async shutdown() {
|
|
51
57
|
await this.cli.dispose();
|
|
52
58
|
}
|
|
@@ -33,8 +33,15 @@ export declare function createHostedProviderCapabilities(params: {
|
|
|
33
33
|
sharedHostFallback?: SharedHostAttachmentMetadataFallbackCapability;
|
|
34
34
|
}): ProviderCapabilities;
|
|
35
35
|
export declare const TEXT_ONLY_HOSTED_PROVIDER_CAPABILITIES: ProviderCapabilities;
|
|
36
|
+
export declare class ProviderTurnCancelledError extends Error {
|
|
37
|
+
readonly publicReplyText?: string;
|
|
38
|
+
constructor(message?: string, options?: {
|
|
39
|
+
publicReplyText?: string;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
36
42
|
export interface ProviderAdapter {
|
|
37
43
|
readonly capabilities: ProviderCapabilities;
|
|
38
44
|
generateReply(input: ProviderInput, options?: ProviderGenerateOptions): Promise<ProviderReply>;
|
|
45
|
+
cancelTurn?(channelId: string): Promise<boolean>;
|
|
39
46
|
dispose?(): Promise<void>;
|
|
40
47
|
}
|
|
@@ -42,3 +42,11 @@ export const TEXT_ONLY_HOSTED_PROVIDER_CAPABILITIES = createHostedProviderCapabi
|
|
|
42
42
|
delivery: [],
|
|
43
43
|
},
|
|
44
44
|
});
|
|
45
|
+
export class ProviderTurnCancelledError extends Error {
|
|
46
|
+
publicReplyText;
|
|
47
|
+
constructor(message = 'provider turn cancelled', options) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = 'ProviderTurnCancelledError';
|
|
50
|
+
this.publicReplyText = options?.publicReplyText;
|
|
51
|
+
}
|
|
52
|
+
}
|