@borgee/agents-host 0.2.56 → 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/README.md +11 -10
- package/dist/agents-host.d.ts +4 -1
- package/dist/agents-host.js +229 -20
- 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/compatibility-gates.js +2 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +21 -0
- package/dist/context/injection.d.ts +20 -2
- package/dist/context/injection.js +146 -103
- package/dist/context/prompt.js +11 -7
- package/dist/context/resolved-workspace.d.ts +1 -0
- package/dist/context/resolved-workspace.js +50 -8
- package/dist/context/turn-preparation.js +8 -0
- package/dist/gateway/localhost-gateway.js +2 -5
- package/dist/local-config.js +11 -1
- package/dist/managed-daemon.js +18 -13
- package/dist/plugin-sdk.js +0 -17
- package/dist/plugin-sdk.js.map +2 -2
- package/dist/policy/copilot-permission.d.ts +1 -0
- package/dist/policy/copilot-permission.js +18 -0
- package/dist/policy/gateway-authorization.js +2 -2
- 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 +2 -1
- package/dist/providers/claude/cli-client.js +53 -3
- 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 +3 -0
- package/dist/providers/codex/cli-client.js +50 -22
- 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 +2 -1
- package/dist/providers/copilot/cli-client.js +30 -3
- package/dist/providers/create-provider.d.ts +1 -1
- package/dist/providers/create-provider.js +8 -0
- package/dist/providers/provider-adapter.d.ts +7 -0
- package/dist/providers/provider-adapter.js +8 -0
- package/dist/types.d.ts +16 -3
- package/package.json +2 -2
- package/skills/borgee-agent/SKILL.md +4 -4
- package/skills/borgee-agent/references/errors.md +2 -2
- package/skills/borgee-agent/references/task-properties.md +4 -3
|
@@ -10,6 +10,7 @@ export interface CopilotPermissionPolicyContext {
|
|
|
10
10
|
auditSink?: AuthorizationAuditSinkLike;
|
|
11
11
|
agentId?: string;
|
|
12
12
|
channelId?: string;
|
|
13
|
+
toolPermissionMode?: 'default' | 'deny-all';
|
|
13
14
|
}
|
|
14
15
|
export declare function resolveCopilotPermissionResponse(context: CopilotPermissionPolicyContext): RequestPermissionResponse;
|
|
15
16
|
export declare function createLegacyMissingAllowOptionError(): Error;
|
|
@@ -84,6 +84,24 @@ function appendAuditRecord(context, effectiveDecision, reason, policyDecision) {
|
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
86
|
export function resolveCopilotPermissionResponse(context) {
|
|
87
|
+
if (context.toolPermissionMode === 'deny-all') {
|
|
88
|
+
const rejectOption = resolveRejectOption(context.params.options);
|
|
89
|
+
const deniedDecision = rejectOption
|
|
90
|
+
? {
|
|
91
|
+
outcome: 'selected',
|
|
92
|
+
optionId: rejectOption.optionId,
|
|
93
|
+
optionKind: rejectOption.kind,
|
|
94
|
+
reason: 'discussion-only-task-thread',
|
|
95
|
+
toolKind: normalizeToolKind(context.params.toolCall.kind),
|
|
96
|
+
}
|
|
97
|
+
: {
|
|
98
|
+
outcome: 'cancelled',
|
|
99
|
+
reason: 'discussion-only-task-thread',
|
|
100
|
+
toolKind: normalizeToolKind(context.params.toolCall.kind),
|
|
101
|
+
};
|
|
102
|
+
appendAuditRecord(context, deniedDecision, deniedDecision.reason, deniedDecision);
|
|
103
|
+
return decisionToResponse(deniedDecision);
|
|
104
|
+
}
|
|
87
105
|
if (!context.gateEnabled) {
|
|
88
106
|
const legacyOption = legacySelectPermissionOption(context.params.options);
|
|
89
107
|
return {
|
|
@@ -33,7 +33,7 @@ function decodePathSegment(segment) {
|
|
|
33
33
|
export function matchChannelRoute(pathname, allowCollaborationRoutes = false) {
|
|
34
34
|
const resources = allowCollaborationRoutes
|
|
35
35
|
? '(bootstrap|me|history|draft|users|messages|tasks|current-task)'
|
|
36
|
-
: '(bootstrap|me|history|tasks|current-task)';
|
|
36
|
+
: '(bootstrap|me|history|users|tasks|current-task)';
|
|
37
37
|
const match = new RegExp(`^/v1/channels/([^/]+)/${resources}$`).exec(pathname);
|
|
38
38
|
if (!match) {
|
|
39
39
|
return null;
|
|
@@ -112,7 +112,7 @@ function allowedMethodsForRoute(route, collaborationRoutesEnabled) {
|
|
|
112
112
|
export function evaluateGatewayAuthorization(input) {
|
|
113
113
|
const url = new URL(input.request.url ?? '/', input.baseUrl);
|
|
114
114
|
const hiddenCollaborationRoute = !input.allowCollaborationRoutes
|
|
115
|
-
&& ['draft', '
|
|
115
|
+
&& ['draft', 'messages'].includes(matchChannelRoute(url.pathname, true)?.resource ?? '');
|
|
116
116
|
if (hasVisibleHeader(input.request.headers.origin)) {
|
|
117
117
|
return {
|
|
118
118
|
reason: 'browser-origin-not-allowed',
|
|
@@ -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;
|
|
@@ -93,7 +94,7 @@ export declare class ClaudeCliClient {
|
|
|
93
94
|
private invalidateSession;
|
|
94
95
|
private closeSession;
|
|
95
96
|
private rejectQueuedTurnsAfterSessionTaint;
|
|
96
|
-
private
|
|
97
|
+
private findChannelStateBySessionId;
|
|
97
98
|
private currentSessionStoreAgentId;
|
|
98
99
|
private ensureSessionStoreLoaded;
|
|
99
100
|
private readPersistedSessionId;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Readable, Writable } from 'node:stream';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
3
4
|
import { createRequire } from 'node:module';
|
|
4
5
|
import { dirname, resolve } from 'node:path';
|
|
5
6
|
import spawn from 'cross-spawn';
|
|
@@ -8,9 +9,11 @@ import { assertClaudeCommandCompatibility, DEFAULT_CLAUDE_COMMAND, isLegacyClaud
|
|
|
8
9
|
import { PROTOCOL_VERSION, client, methods, ndJsonStream, } from '@agentclientprotocol/sdk';
|
|
9
10
|
import { HostLogger, summarizeChildStderr, summarizeError } from '../../debug.js';
|
|
10
11
|
import { AcpProgressCollector } from '../acp-progress-collector.js';
|
|
12
|
+
import { ProviderTurnCancelledError } from '../provider-adapter.js';
|
|
11
13
|
import { readClaudeActivityMetadata } from './activity-metadata.js';
|
|
12
14
|
import { resolveCopilotPermissionResponse } from '../../policy/copilot-permission.js';
|
|
13
15
|
import { buildClaudeSessionPromptAppend } from '../../context/prompt.js';
|
|
16
|
+
import { isDiscussionOnlyResolvedWorkspace } from '../../context/resolved-workspace.js';
|
|
14
17
|
import { IDLE_BACKEND_SHUTDOWN_DISABLED_MS, IdleBackendShutdownScheduler, } from '../idle-backend-shutdown.js';
|
|
15
18
|
const SESSION_TAINTED_ERRORS = new WeakSet();
|
|
16
19
|
const IDLE_BACKEND_STOPPED_MESSAGE = 'Claude ACP backend stopped after idle timeout';
|
|
@@ -30,6 +33,7 @@ const DEFAULT_SESSION_CAPABILITIES = {
|
|
|
30
33
|
};
|
|
31
34
|
const CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD_ENV = 'CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD';
|
|
32
35
|
const require = createRequire(import.meta.url);
|
|
36
|
+
const CLAUDE_CANCELLED_REPLY_TEXT = 'Info: Operation cancelled by user';
|
|
33
37
|
const DEFAULT_RUNTIME = {
|
|
34
38
|
spawn,
|
|
35
39
|
client,
|
|
@@ -386,6 +390,22 @@ function hashSessionAppend(systemPromptAppend) {
|
|
|
386
390
|
}
|
|
387
391
|
return createHash('sha256').update(systemPromptAppend).digest('hex');
|
|
388
392
|
}
|
|
393
|
+
function resolveSkillManualContentHash(promptContext) {
|
|
394
|
+
const skillMarkdownPath = promptContext?.skillRuntime?.skillMarkdownPath;
|
|
395
|
+
if (!skillMarkdownPath) {
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
return createHash('sha256')
|
|
400
|
+
.update(skillMarkdownPath)
|
|
401
|
+
.update('\0')
|
|
402
|
+
.update(readFileSync(skillMarkdownPath))
|
|
403
|
+
.digest('hex');
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return `unreadable:${skillMarkdownPath}`;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
389
409
|
function resolveSessionVisibilityKey(promptContext, systemPromptAppend, promptStrategy) {
|
|
390
410
|
const resolvedPromptStrategy = resolveProjectionStrategy(promptStrategy);
|
|
391
411
|
const explicitPromptStrategy = resolvedPromptStrategy === DEFAULT_PROJECTION_STRATEGY && promptStrategy == null
|
|
@@ -395,9 +415,11 @@ function resolveSessionVisibilityKey(promptContext, systemPromptAppend, promptSt
|
|
|
395
415
|
: resolvedPromptStrategy;
|
|
396
416
|
const additionalDirectories = resolveSessionAdditionalDirectories(promptContext);
|
|
397
417
|
const sessionAppendHash = hashSessionAppend(systemPromptAppend);
|
|
418
|
+
const skillManualContentHash = resolveSkillManualContentHash(promptContext);
|
|
398
419
|
const projectedBriefHash = promptContext?.claudeProjectedBriefHash;
|
|
399
420
|
if (!additionalDirectories
|
|
400
421
|
&& !sessionAppendHash
|
|
422
|
+
&& !skillManualContentHash
|
|
401
423
|
&& !projectedBriefHash
|
|
402
424
|
&& !promptContext?.gatewayCredentialPath
|
|
403
425
|
&& !explicitPromptStrategy) {
|
|
@@ -407,12 +429,14 @@ function resolveSessionVisibilityKey(promptContext, systemPromptAppend, promptSt
|
|
|
407
429
|
return JSON.stringify({
|
|
408
430
|
...(additionalDirectories ? { additionalDirectories } : {}),
|
|
409
431
|
sessionAppendHash,
|
|
432
|
+
...(skillManualContentHash ? { skillManualContentHash } : {}),
|
|
410
433
|
...(projectedBriefHash ? { projectedBriefHash } : {}),
|
|
411
434
|
...(explicitPromptStrategy ? { promptStrategy: explicitPromptStrategy } : {}),
|
|
412
435
|
});
|
|
413
436
|
}
|
|
414
437
|
return JSON.stringify({
|
|
415
438
|
...(additionalDirectories ? { additionalDirectories } : {}),
|
|
439
|
+
...(skillManualContentHash ? { skillManualContentHash } : {}),
|
|
416
440
|
...(promptContext?.gatewayCredentialPath
|
|
417
441
|
? { gatewayCredentialPath: promptContext.gatewayCredentialPath }
|
|
418
442
|
: {}),
|
|
@@ -473,6 +497,22 @@ export class ClaudeCliClient {
|
|
|
473
497
|
sessionIdForChannel(channelId) {
|
|
474
498
|
return this.channels.get(channelId)?.session?.sessionId;
|
|
475
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
|
+
}
|
|
476
516
|
persistedSessions = new Map();
|
|
477
517
|
closingSessions = new WeakSet();
|
|
478
518
|
pendingSessionStarts = new Set();
|
|
@@ -721,6 +761,9 @@ export class ClaudeCliClient {
|
|
|
721
761
|
state.activeTurn = turn;
|
|
722
762
|
try {
|
|
723
763
|
state.cwd = this.resolveSessionCwd(turn.preparedTurn.promptContext);
|
|
764
|
+
state.toolPermissionMode = isDiscussionOnlyResolvedWorkspace(turn.preparedTurn.promptContext)
|
|
765
|
+
? 'deny-all'
|
|
766
|
+
: 'default';
|
|
724
767
|
const sessionPromptAppend = resolveTurnSessionPromptAppend(turn.preparedTurn);
|
|
725
768
|
state.visibilityKey = resolveSessionVisibilityKey(turn.preparedTurn.promptContext, sessionPromptAppend, turn.preparedTurn.projectionStrategy);
|
|
726
769
|
await this.ensureStarted();
|
|
@@ -950,6 +993,11 @@ export class ClaudeCliClient {
|
|
|
950
993
|
throw markSessionTainted(error);
|
|
951
994
|
}
|
|
952
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
|
+
}
|
|
953
1001
|
if (response.stopReason !== 'end_turn' && !hasVisibleText(output)) {
|
|
954
1002
|
throw new Error(`Claude ACP turn stopped with stopReason "${response.stopReason}"`);
|
|
955
1003
|
}
|
|
@@ -999,13 +1047,15 @@ export class ClaudeCliClient {
|
|
|
999
1047
|
this.shutdownPromise = this.shutdownBackend(error);
|
|
1000
1048
|
}
|
|
1001
1049
|
handlePermissionRequest(params) {
|
|
1050
|
+
const state = this.findChannelStateBySessionId(params.sessionId);
|
|
1002
1051
|
return resolveCopilotPermissionResponse({
|
|
1003
1052
|
gateEnabled: true,
|
|
1004
1053
|
policyMode: 'enforce',
|
|
1005
1054
|
params,
|
|
1006
1055
|
logger: this.logger,
|
|
1007
1056
|
agentId: this.resolveSessionStoreAgentId()?.trim() || undefined,
|
|
1008
|
-
channelId:
|
|
1057
|
+
channelId: state?.channelId,
|
|
1058
|
+
toolPermissionMode: state?.toolPermissionMode ?? 'default',
|
|
1009
1059
|
});
|
|
1010
1060
|
}
|
|
1011
1061
|
invalidateSession(channelId, state, session) {
|
|
@@ -1057,10 +1107,10 @@ export class ClaudeCliClient {
|
|
|
1057
1107
|
}
|
|
1058
1108
|
state.queue.length = 0;
|
|
1059
1109
|
}
|
|
1060
|
-
|
|
1110
|
+
findChannelStateBySessionId(sessionId) {
|
|
1061
1111
|
for (const state of this.channels.values()) {
|
|
1062
1112
|
if (state.session?.sessionId === sessionId) {
|
|
1063
|
-
return state
|
|
1113
|
+
return state;
|
|
1064
1114
|
}
|
|
1065
1115
|
}
|
|
1066
1116
|
return undefined;
|
|
@@ -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;
|
|
@@ -108,6 +109,8 @@ export declare class CodexCliClient {
|
|
|
108
109
|
private failAll;
|
|
109
110
|
private closeSession;
|
|
110
111
|
private clearIdleTimer;
|
|
112
|
+
private handlePermissionRequest;
|
|
113
|
+
private findChannelStateBySessionId;
|
|
111
114
|
private reconcileIdleChannelState;
|
|
112
115
|
private ensureSessionStoreLoaded;
|
|
113
116
|
private readPersistedSessionId;
|
|
@@ -6,8 +6,11 @@ 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';
|
|
12
|
+
import { isDiscussionOnlyResolvedWorkspace } from '../../context/resolved-workspace.js';
|
|
13
|
+
import { resolveCopilotPermissionResponse } from '../../policy/copilot-permission.js';
|
|
11
14
|
import { IDLE_BACKEND_SHUTDOWN_DISABLED_MS, IdleBackendShutdownScheduler, } from '../idle-backend-shutdown.js';
|
|
12
15
|
const SESSION_TAINTED_ERRORS = new WeakSet();
|
|
13
16
|
const DEFAULT_IDLE_SESSION_TTL_MS = 2 * 24 * 60 * 60 * 1000;
|
|
@@ -32,6 +35,7 @@ const CODEX_CONTEXT_ROOT_DIRNAME = 'codex-context';
|
|
|
32
35
|
const PROJECTION_UNAVAILABLE_PLACEHOLDER = '[codex-projection-unavailable]';
|
|
33
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';
|
|
34
37
|
const require = createRequire(import.meta.url);
|
|
38
|
+
const CODEX_CANCELLED_REPLY_TEXT = 'Info: Operation cancelled by user';
|
|
35
39
|
const DEFAULT_RUNTIME = {
|
|
36
40
|
spawn,
|
|
37
41
|
client,
|
|
@@ -193,26 +197,6 @@ function markSessionTainted(error) {
|
|
|
193
197
|
function isSessionTainted(error) {
|
|
194
198
|
return error instanceof Error && SESSION_TAINTED_ERRORS.has(error);
|
|
195
199
|
}
|
|
196
|
-
function createPermissionResponse(params) {
|
|
197
|
-
const options = Array.isArray(params.options) ? params.options : [];
|
|
198
|
-
const preferredKinds = ['allow_once', 'allow_always', 'reject_once', 'reject_always'];
|
|
199
|
-
for (const kind of preferredKinds) {
|
|
200
|
-
const match = options.find((option) => option.kind === kind);
|
|
201
|
-
if (match) {
|
|
202
|
-
return {
|
|
203
|
-
outcome: {
|
|
204
|
-
outcome: 'selected',
|
|
205
|
-
optionId: match.optionId,
|
|
206
|
-
},
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return {
|
|
211
|
-
outcome: {
|
|
212
|
-
outcome: 'cancelled',
|
|
213
|
-
},
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
200
|
function resolveBundledCodexAdapterPath() {
|
|
217
201
|
return require.resolve('@agentclientprotocol/codex-acp');
|
|
218
202
|
}
|
|
@@ -248,6 +232,23 @@ export class CodexCliClient {
|
|
|
248
232
|
sessionIdForChannel(channelId) {
|
|
249
233
|
return this.channels.get(channelId)?.session?.sessionId;
|
|
250
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
|
+
}
|
|
251
252
|
persistedSessions = new Map();
|
|
252
253
|
closingSessions = new WeakSet();
|
|
253
254
|
pendingSessionStarts = new Set();
|
|
@@ -423,7 +424,7 @@ export class CodexCliClient {
|
|
|
423
424
|
const stream = this.runtime.ndJsonStream(output, input);
|
|
424
425
|
const app = this.runtime
|
|
425
426
|
.client({ name: 'borgee-agents-host' })
|
|
426
|
-
.onRequest(this.runtime.methods.client.session.requestPermission, ({ params }) => (
|
|
427
|
+
.onRequest(this.runtime.methods.client.session.requestPermission, ({ params }) => (this.handlePermissionRequest(params)));
|
|
427
428
|
const connection = app.connect(stream);
|
|
428
429
|
this.connection = connection;
|
|
429
430
|
void connection.closed.then(() => {
|
|
@@ -487,6 +488,9 @@ export class CodexCliClient {
|
|
|
487
488
|
state.activeTurn = turn;
|
|
488
489
|
try {
|
|
489
490
|
state.cwd = await this.resolveSessionCwd(turn.preparedTurn.promptContext);
|
|
491
|
+
state.toolPermissionMode = isDiscussionOnlyResolvedWorkspace(turn.preparedTurn.promptContext)
|
|
492
|
+
? 'deny-all'
|
|
493
|
+
: 'default';
|
|
490
494
|
const projectedPromptContext = await this.refreshProjectedPromptContextBestEffort(turn.channelId, turn.preparedTurn.promptContext);
|
|
491
495
|
state.additionalDirectories = this.resolveSessionAdditionalDirectories(projectedPromptContext);
|
|
492
496
|
state.visibilityKey = this.resolveSessionVisibilityKey(projectedPromptContext);
|
|
@@ -846,10 +850,15 @@ export class CodexCliClient {
|
|
|
846
850
|
catch (error) {
|
|
847
851
|
throw markSessionTainted(error);
|
|
848
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
|
+
}
|
|
849
859
|
if (response.stopReason !== 'end_turn') {
|
|
850
860
|
throw new Error(`Codex ACP turn stopped with stopReason "${response.stopReason}"`);
|
|
851
861
|
}
|
|
852
|
-
const output = collector.getFinalText();
|
|
853
862
|
if (!output) {
|
|
854
863
|
throw new Error('Codex ACP returned empty output');
|
|
855
864
|
}
|
|
@@ -953,6 +962,25 @@ export class CodexCliClient {
|
|
|
953
962
|
clearTimeout(state.idleTimer);
|
|
954
963
|
state.idleTimer = undefined;
|
|
955
964
|
}
|
|
965
|
+
handlePermissionRequest(params) {
|
|
966
|
+
const state = this.findChannelStateBySessionId(params.sessionId);
|
|
967
|
+
return resolveCopilotPermissionResponse({
|
|
968
|
+
gateEnabled: true,
|
|
969
|
+
policyMode: 'enforce',
|
|
970
|
+
params,
|
|
971
|
+
logger: this.logger,
|
|
972
|
+
channelId: state?.channelId,
|
|
973
|
+
toolPermissionMode: state?.toolPermissionMode ?? 'default',
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
findChannelStateBySessionId(sessionId) {
|
|
977
|
+
for (const state of this.channels.values()) {
|
|
978
|
+
if (state.session?.sessionId === sessionId) {
|
|
979
|
+
return state;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return undefined;
|
|
983
|
+
}
|
|
956
984
|
reconcileIdleChannelState(channelId, state) {
|
|
957
985
|
if (this.channels.get(channelId) !== state) {
|
|
958
986
|
return;
|
|
@@ -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;
|
|
@@ -103,7 +104,7 @@ export declare class CopilotCliClient {
|
|
|
103
104
|
private closeSession;
|
|
104
105
|
private rejectQueuedTurnsAfterSessionTaint;
|
|
105
106
|
private clearIdleTimer;
|
|
106
|
-
private
|
|
107
|
+
private findChannelStateBySessionId;
|
|
107
108
|
private reconcileIdleChannelState;
|
|
108
109
|
private ensureSessionStoreLoaded;
|
|
109
110
|
private readPersistedSessionId;
|
|
@@ -3,7 +3,9 @@ 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';
|
|
8
|
+
import { isDiscussionOnlyResolvedWorkspace } from '../../context/resolved-workspace.js';
|
|
7
9
|
import { IDLE_BACKEND_SHUTDOWN_DISABLED_MS, IdleBackendShutdownScheduler, } from '../idle-backend-shutdown.js';
|
|
8
10
|
const SESSION_TAINTED_ERRORS = new WeakSet();
|
|
9
11
|
const DEFAULT_IDLE_SESSION_TTL_MS = 2 * 24 * 60 * 60 * 1000;
|
|
@@ -169,6 +171,23 @@ export class CopilotCliClient {
|
|
|
169
171
|
sessionIdForChannel(channelId) {
|
|
170
172
|
return this.channels.get(channelId)?.session?.sessionId;
|
|
171
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
|
+
}
|
|
172
191
|
persistedSessions = new Map();
|
|
173
192
|
closingSessions = new WeakSet();
|
|
174
193
|
pendingSessionStarts = new Set();
|
|
@@ -407,6 +426,9 @@ export class CopilotCliClient {
|
|
|
407
426
|
state.activeTurn = turn;
|
|
408
427
|
try {
|
|
409
428
|
state.cwd = await this.resolveSessionCwd(turn.preparedTurn.promptContext);
|
|
429
|
+
state.toolPermissionMode = isDiscussionOnlyResolvedWorkspace(turn.preparedTurn.promptContext)
|
|
430
|
+
? 'deny-all'
|
|
431
|
+
: 'default';
|
|
410
432
|
await this.ensureStarted();
|
|
411
433
|
await this.recycleSessionIfCwdChanged(channelId, state);
|
|
412
434
|
const session = await this.getOrCreateSession(channelId, state);
|
|
@@ -647,6 +669,9 @@ export class CopilotCliClient {
|
|
|
647
669
|
catch (error) {
|
|
648
670
|
throw markSessionTainted(error);
|
|
649
671
|
}
|
|
672
|
+
if (response.stopReason === 'cancelled') {
|
|
673
|
+
throw new ProviderTurnCancelledError('Copilot ACP turn cancelled');
|
|
674
|
+
}
|
|
650
675
|
if (response.stopReason !== 'end_turn') {
|
|
651
676
|
throw new Error(`Copilot ACP turn stopped with stopReason "${response.stopReason}"`);
|
|
652
677
|
}
|
|
@@ -666,6 +691,7 @@ export class CopilotCliClient {
|
|
|
666
691
|
return Promise.race([promise, this.fatalPromise]);
|
|
667
692
|
}
|
|
668
693
|
handlePermissionRequest(params) {
|
|
694
|
+
const state = this.findChannelStateBySessionId(params.sessionId);
|
|
669
695
|
return resolveCopilotPermissionResponse({
|
|
670
696
|
gateEnabled: this.permissionPolicy.gateEnabled ?? false,
|
|
671
697
|
policyMode: this.permissionPolicy.policyMode ?? 'audit-only',
|
|
@@ -673,7 +699,8 @@ export class CopilotCliClient {
|
|
|
673
699
|
logger: this.logger,
|
|
674
700
|
auditSink: this.permissionPolicy.auditSink,
|
|
675
701
|
agentId: this.resolveSessionStoreAgentId()?.trim() || undefined,
|
|
676
|
-
channelId:
|
|
702
|
+
channelId: state?.channelId,
|
|
703
|
+
toolPermissionMode: state?.toolPermissionMode ?? 'default',
|
|
677
704
|
});
|
|
678
705
|
}
|
|
679
706
|
failAll(error) {
|
|
@@ -761,10 +788,10 @@ export class CopilotCliClient {
|
|
|
761
788
|
clearTimeout(state.idleTimer);
|
|
762
789
|
state.idleTimer = undefined;
|
|
763
790
|
}
|
|
764
|
-
|
|
791
|
+
findChannelStateBySessionId(sessionId) {
|
|
765
792
|
for (const state of this.channels.values()) {
|
|
766
793
|
if (state.session?.sessionId === sessionId) {
|
|
767
|
-
return state
|
|
794
|
+
return state;
|
|
768
795
|
}
|
|
769
796
|
}
|
|
770
797
|
return undefined;
|
|
@@ -8,7 +8,7 @@ interface CreateProviderOptions {
|
|
|
8
8
|
compatibilityGates?: ReadonlySet<string>;
|
|
9
9
|
localhostGateway?: LocalhostGatewayContextPublisher;
|
|
10
10
|
authorizationAuditSink?: AuthorizationAuditSinkLike;
|
|
11
|
-
controlPlane?: Pick<ChatControlPlane, 'getTask' | 'listChannels' | 'listTasks'>;
|
|
11
|
+
controlPlane?: Pick<ChatControlPlane, 'getTask' | 'listChannels' | 'listTasks' | 'listUsers'>;
|
|
12
12
|
}
|
|
13
13
|
export declare function createProvider(config: ProviderRuntimeConfig, debugLogger?: DebugLogger, options?: CreateProviderOptions): ProviderAdapter;
|
|
14
14
|
export {};
|
|
@@ -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
|
}
|
|
@@ -135,6 +141,8 @@ export function createProvider(config, debugLogger, options = {}) {
|
|
|
135
141
|
? options.localhostGateway
|
|
136
142
|
: undefined,
|
|
137
143
|
taskReader: options.controlPlane,
|
|
144
|
+
allowCrossAgentIndependentWorkspaceHandoff: config.allowCrossAgentIndependentWorkspaceHandoff,
|
|
145
|
+
resolveStableAgentId: config.resolveStableAgentId,
|
|
138
146
|
})
|
|
139
147
|
: undefined;
|
|
140
148
|
const resolveInternalProjection = () => resolveInternalProjectionStrategy();
|
|
@@ -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
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ export interface ProviderCommandConfig {
|
|
|
10
10
|
copilotSessionTtlMinutes: number;
|
|
11
11
|
/** Minutes a provider's shared ACP adapter process may stay idle before it is shut down; `0` keeps it resident. */
|
|
12
12
|
providerIdleShutdownMinutes: number;
|
|
13
|
+
/**
|
|
14
|
+
* Whether this agents-host may let a hosted agent enter an independent
|
|
15
|
+
* execution.local_directory workspace for a task thread created by another
|
|
16
|
+
* agent. Undefined keeps the default allow behavior.
|
|
17
|
+
*/
|
|
18
|
+
allowCrossAgentIndependentWorkspaceHandoff?: boolean;
|
|
13
19
|
}
|
|
14
20
|
export interface ProviderRuntimeConfig extends ProviderCommandConfig {
|
|
15
21
|
provider: ProviderKind;
|
|
@@ -286,6 +292,8 @@ export interface ProviderInput {
|
|
|
286
292
|
taskThreadCollaborationContract?: TaskThreadCollaborationContract;
|
|
287
293
|
collaborationCapabilities?: CollaborationCapabilityDeclaration;
|
|
288
294
|
missedCollaborationDiagnostic?: MissedCollaborationDiagnostic;
|
|
295
|
+
taskAssignmentContextOverride?: TaskAssignmentThreadContext;
|
|
296
|
+
taskAssignmentContextOverridePersistence?: 'persist' | 'ephemeral';
|
|
289
297
|
}
|
|
290
298
|
export interface ProviderProtocolContext {
|
|
291
299
|
anchorMessageId: string;
|
|
@@ -334,12 +342,17 @@ export interface ChannelResolvedWorkspaceContext {
|
|
|
334
342
|
owningChannelId: string;
|
|
335
343
|
rootPath: string;
|
|
336
344
|
}
|
|
337
|
-
export interface
|
|
338
|
-
authority: 'task';
|
|
345
|
+
export interface TaskThreadScratchWorkspaceContext {
|
|
346
|
+
authority: 'task-thread-scratch';
|
|
347
|
+
rootPath: string;
|
|
348
|
+
reason?: 'missing-or-invalid-execution-target' | 'cross-agent-independent-workspace-disabled';
|
|
349
|
+
}
|
|
350
|
+
export interface TaskExecutionTargetResolvedWorkspaceContext {
|
|
351
|
+
authority: 'task-execution-target';
|
|
339
352
|
taskId: string;
|
|
340
353
|
rootPath: string;
|
|
341
354
|
}
|
|
342
|
-
export type ResolvedWorkspaceContext = ChannelResolvedWorkspaceContext |
|
|
355
|
+
export type ResolvedWorkspaceContext = ChannelResolvedWorkspaceContext | TaskThreadScratchWorkspaceContext | TaskExecutionTargetResolvedWorkspaceContext;
|
|
343
356
|
export interface RuntimeSurface {
|
|
344
357
|
schemaVersion: 1;
|
|
345
358
|
turnMode: ProviderCollaborationTurnMode;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@borgee/agents-host",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.65",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"tsx": "^4.20.5",
|
|
36
36
|
"typescript": "^5.9.3",
|
|
37
37
|
"vitest": "^4.1.5",
|
|
38
|
-
"@borgee/plugin-sdk": "0.
|
|
38
|
+
"@borgee/plugin-sdk": "0.12.0"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"predev": "pnpm --filter @borgee/plugin-sdk build",
|