@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
package/README.md
CHANGED
|
@@ -479,7 +479,7 @@ successfully applied snapshot running.
|
|
|
479
479
|
Each Borgee channel is mapped 1:1 to a provider-native session while it stays
|
|
480
480
|
active:
|
|
481
481
|
|
|
482
|
-
- **Claude**: one persistent `@agentclientprotocol/claude-agent-acp` subprocess is shared by a single `AgentsHost`, with one ACP session per Borgee channel. Same-channel turns are serialized; different channels keep isolated ACP sessions. The real ACP session cwd now comes from the resolved channel workspace `~/.borgee/channels/<encoded-channel-id>/workspace`. Task threads
|
|
482
|
+
- **Claude**: one persistent `@agentclientprotocol/claude-agent-acp` subprocess is shared by a single `AgentsHost`, with one ACP session per Borgee channel. Same-channel turns are serialized; different channels keep isolated ACP sessions. The real ACP session cwd now comes from the resolved channel workspace `~/.borgee/channels/<encoded-channel-id>/workspace`. Task threads only switch execution onto an explicit existing `execution.local_directory`; otherwise they stay discussion-only on `~/.borgee/channels/<encoded-thread-id>/scratch/workspace`, and ACP permission requests are denied so filesystem and shell tools stay unavailable. The host persists the channel→ACP-session map in its state root, restores same-host continuity with ACP `session/resume` when available, retries stale restores with a fresh session immediately, and injects the per-channel local context paths through Claude ACP `_meta.systemPrompt.append`.
|
|
483
483
|
- **Codex**: one persistent `@agentclientprotocol/codex-acp` subprocess is
|
|
484
484
|
shared by a single `AgentsHost`, with one ACP session per Borgee channel. When
|
|
485
485
|
the current turn has a materialized channel context payload, Codex refreshes a
|
|
@@ -487,12 +487,11 @@ active:
|
|
|
487
487
|
per-channel projection directory and exposes that directory to Codex through
|
|
488
488
|
ACP `additionalDirectories`. The real ACP session cwd now comes from the
|
|
489
489
|
resolved channel workspace `~/.borgee/channels/<encoded-channel-id>/workspace`.
|
|
490
|
-
Task threads
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
repository is already checked out there. The host persists the
|
|
490
|
+
Task threads only switch execution onto an explicit existing
|
|
491
|
+
`execution.local_directory`; otherwise they stay discussion-only on
|
|
492
|
+
`~/.borgee/channels/<encoded-thread-id>/scratch/workspace`, and ACP
|
|
493
|
+
permission requests are denied so filesystem and shell tools stay
|
|
494
|
+
unavailable. The host persists the
|
|
496
495
|
channel→ACP-session map in its state root and attempts ACP `session/resume`
|
|
497
496
|
first when the adapter advertises it, otherwise `session/load`.
|
|
498
497
|
- **Copilot**: one persistent `copilot --acp` subprocess is shared by a single
|
|
@@ -500,9 +499,11 @@ active:
|
|
|
500
499
|
serialized; different channels keep isolated ACP sessions. The real ACP
|
|
501
500
|
session cwd now comes from the resolved channel workspace
|
|
502
501
|
`~/.borgee/channels/<encoded-channel-id>/workspace` by default. Task threads
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
502
|
+
only switch execution onto an explicit existing `execution.local_directory`;
|
|
503
|
+
otherwise they stay discussion-only on
|
|
504
|
+
`~/.borgee/channels/<encoded-thread-id>/scratch/workspace`, and ACP
|
|
505
|
+
permission requests are denied so filesystem and shell tools stay
|
|
506
|
+
unavailable. The
|
|
506
507
|
host persists the channel→ACP-session map in its state root and restores
|
|
507
508
|
same-host continuity with ACP `session/resume` when available, otherwise
|
|
508
509
|
`session/load` plus local replay cleanup on runtimes that advertise only
|
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
|
|
@@ -125,6 +125,7 @@ export declare class AgentsHost {
|
|
|
125
125
|
private revalidateAttentionSnapshotForTurn;
|
|
126
126
|
private getAttentionSnapshotForTurn;
|
|
127
127
|
private isHumanWakeFiltered;
|
|
128
|
+
private isOrdinaryAgentWakeFiltered;
|
|
128
129
|
private applyAttentionUpdate;
|
|
129
130
|
private commitAttentionSnapshot;
|
|
130
131
|
private resolveTaskThreadCollaborationContractForTurn;
|
|
@@ -159,6 +160,8 @@ export declare class AgentsHost {
|
|
|
159
160
|
private readCollaborationDraft;
|
|
160
161
|
private supersedeActiveTurn;
|
|
161
162
|
private invalidateChannelTurn;
|
|
163
|
+
private stopTurn;
|
|
164
|
+
private responseStateForExecuted;
|
|
162
165
|
private rememberRecentTurn;
|
|
163
166
|
private resolveAuthorizedCollaborationTurn;
|
|
164
167
|
private updateAwaitingUserState;
|
package/dist/agents-host.js
CHANGED
|
@@ -9,13 +9,14 @@ import { buildNormalizedAttentionSnapshot, parsePersistedAttentionSnapshot, } fr
|
|
|
9
9
|
import { mergeCompactionSnapshot, parsePersistedCompactionSnapshot, } from './context/compaction.js';
|
|
10
10
|
import { projectTaskThreadCollaborationContract } from './context/task-thread-collaboration.js';
|
|
11
11
|
import { createLocalhostGatewayController, LOCALHOST_GATEWAY_COLLABORATION_TARGET_COOLDOWN_MS, } from './gateway/localhost-gateway.js';
|
|
12
|
-
import { extractTaskIdFromTaskAssignmentContent,
|
|
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
|
-
import { waitForTaskForThread } from './task-thread-resolution.js';
|
|
18
18
|
import { buildHostedIncomingContentText, buildHostedTurnContentParts, extractHostedMessageAttachments, normalizeHostedMessageAttachment, } from './hosted-turn-content.js';
|
|
19
|
+
import { waitForTaskForThread } from './task-thread-resolution.js';
|
|
19
20
|
const PRIVATE_STATE_ROOT_MODE = 0o700;
|
|
20
21
|
const PROTOCOL_STATE_DIRNAME = 'protocol-state';
|
|
21
22
|
const PROTOCOL_PROVIDER_TURN_TIMEOUT_MS = 120_000;
|
|
@@ -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);
|
|
@@ -136,6 +140,7 @@ class DefaultHostRuntime {
|
|
|
136
140
|
copilotArgs: config.copilotArgs,
|
|
137
141
|
copilotSessionTtlMinutes: config.copilotSessionTtlMinutes,
|
|
138
142
|
providerIdleShutdownMinutes: config.providerIdleShutdownMinutes,
|
|
143
|
+
allowCrossAgentIndependentWorkspaceHandoff: config.allowCrossAgentIndependentWorkspaceHandoff,
|
|
139
144
|
}, deps.logger, {
|
|
140
145
|
compatibilityGates,
|
|
141
146
|
authorizationAuditSink: this.authorizationAuditSink,
|
|
@@ -144,13 +149,15 @@ class DefaultHostRuntime {
|
|
|
144
149
|
});
|
|
145
150
|
this.ensureStateRoot = deps.ensureStateRoot ?? ensurePrivateStateRoot;
|
|
146
151
|
}
|
|
147
|
-
async start(onMessage) {
|
|
152
|
+
async start(onMessage, onStopTurn) {
|
|
148
153
|
await this.ensureStateRoot(this.config.stateRootDir);
|
|
149
154
|
await this.gateway.start();
|
|
150
155
|
try {
|
|
156
|
+
this.controlPlane.onStopTurn?.(onStopTurn);
|
|
151
157
|
await this.controlPlane.connect(onMessage);
|
|
152
158
|
}
|
|
153
159
|
catch (error) {
|
|
160
|
+
this.controlPlane.onStopTurn?.(undefined);
|
|
154
161
|
await this.gateway.stop().catch(() => { });
|
|
155
162
|
throw error;
|
|
156
163
|
}
|
|
@@ -158,6 +165,7 @@ class DefaultHostRuntime {
|
|
|
158
165
|
async stop() {
|
|
159
166
|
let thrown;
|
|
160
167
|
try {
|
|
168
|
+
this.controlPlane.onStopTurn?.(undefined);
|
|
161
169
|
await this.controlPlane.close();
|
|
162
170
|
}
|
|
163
171
|
catch (error) {
|
|
@@ -365,7 +373,7 @@ export class AgentsHost {
|
|
|
365
373
|
return;
|
|
366
374
|
}
|
|
367
375
|
this.dispatchMessage(message);
|
|
368
|
-
});
|
|
376
|
+
}, async (channelId) => this.stopTurn(channelId));
|
|
369
377
|
const agentId = await this.ensureSelfAgentId();
|
|
370
378
|
bufferingMessages = false;
|
|
371
379
|
while (bufferedMessages.length > 0) {
|
|
@@ -461,6 +469,9 @@ export class AgentsHost {
|
|
|
461
469
|
const authorKind = await this.classifyAuthor(normalized.authorId, false);
|
|
462
470
|
await this.ensureAttentionSnapshotLoaded(msg.channel_id, channelState);
|
|
463
471
|
attentionSnapshot = this.getAttentionSnapshotForTurn(msg.channel_id, taskThreadContext);
|
|
472
|
+
if (this.isOrdinaryAgentWakeFiltered(msg, authorKind)) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
464
475
|
if (this.isHumanWakeFiltered(msg, authorKind, attentionSnapshot)) {
|
|
465
476
|
return;
|
|
466
477
|
}
|
|
@@ -553,6 +564,9 @@ export class AgentsHost {
|
|
|
553
564
|
}
|
|
554
565
|
}
|
|
555
566
|
}
|
|
567
|
+
if (this.isOrdinaryAgentWakeFiltered(msg, authorKind)) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
556
570
|
if (this.isHumanWakeFiltered(msg, authorKind, attentionSnapshot)) {
|
|
557
571
|
return;
|
|
558
572
|
}
|
|
@@ -668,7 +682,7 @@ export class AgentsHost {
|
|
|
668
682
|
return null;
|
|
669
683
|
}
|
|
670
684
|
this.recordCollaborationOutcome(msg.channel_id, {
|
|
671
|
-
responseState: executed
|
|
685
|
+
responseState: this.responseStateForExecuted(executed),
|
|
672
686
|
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
673
687
|
});
|
|
674
688
|
this.updateMissedCollaborationDiagnostic(msg.channel_id, priorMissedCollaborationDiagnostic);
|
|
@@ -1103,7 +1117,7 @@ export class AgentsHost {
|
|
|
1103
1117
|
channelState.blocked = undefined;
|
|
1104
1118
|
this.awaitingUserByChannel.delete(channelId);
|
|
1105
1119
|
this.recordCollaborationOutcome(channelId, {
|
|
1106
|
-
responseState: executed
|
|
1120
|
+
responseState: this.responseStateForExecuted(executed),
|
|
1107
1121
|
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
1108
1122
|
...(message.wakeBlockedState ? { wakeState: 'resumed-human' } : {}),
|
|
1109
1123
|
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
@@ -1167,7 +1181,7 @@ export class AgentsHost {
|
|
|
1167
1181
|
const visibleMessageId = deliveredMessageId ?? protocolState.lastProtocolMessageId;
|
|
1168
1182
|
if (executed.reply?.controlMalformed) {
|
|
1169
1183
|
this.recordCollaborationOutcome(channelId, {
|
|
1170
|
-
responseState: executed
|
|
1184
|
+
responseState: this.responseStateForExecuted(executed),
|
|
1171
1185
|
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
1172
1186
|
...(message.wakeBlockedState ? { wakeState: 'resumed-human' } : {}),
|
|
1173
1187
|
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
@@ -1177,7 +1191,7 @@ export class AgentsHost {
|
|
|
1177
1191
|
}
|
|
1178
1192
|
if (!executed.reply?.control) {
|
|
1179
1193
|
this.recordCollaborationOutcome(channelId, {
|
|
1180
|
-
responseState: executed
|
|
1194
|
+
responseState: this.responseStateForExecuted(executed),
|
|
1181
1195
|
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
1182
1196
|
...(message.wakeBlockedState ? { wakeState: 'resumed-human' } : {}),
|
|
1183
1197
|
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
@@ -1211,7 +1225,7 @@ export class AgentsHost {
|
|
|
1211
1225
|
}
|
|
1212
1226
|
if (executed.reply.control.kind === 'start-protocol') {
|
|
1213
1227
|
this.recordCollaborationOutcome(channelId, {
|
|
1214
|
-
responseState: executed
|
|
1228
|
+
responseState: this.responseStateForExecuted(executed),
|
|
1215
1229
|
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
1216
1230
|
...(message.wakeBlockedState ? { wakeState: 'resumed-human' } : {}),
|
|
1217
1231
|
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
@@ -1222,7 +1236,7 @@ export class AgentsHost {
|
|
|
1222
1236
|
}
|
|
1223
1237
|
if (executed.reply.control.kind === 'attention-only') {
|
|
1224
1238
|
this.recordCollaborationOutcome(channelId, {
|
|
1225
|
-
responseState: executed
|
|
1239
|
+
responseState: this.responseStateForExecuted(executed),
|
|
1226
1240
|
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
1227
1241
|
...(message.wakeBlockedState ? { wakeState: 'resumed-human' } : {}),
|
|
1228
1242
|
turnExecutionId: activeTurn.turnExecutionId ?? undefined,
|
|
@@ -1824,19 +1838,74 @@ export class AgentsHost {
|
|
|
1824
1838
|
if (!this.contextInjectionEnabled) {
|
|
1825
1839
|
return { state: 'inactive' };
|
|
1826
1840
|
}
|
|
1841
|
+
const recoverFromControlPlane = async (preferredTaskId) => {
|
|
1842
|
+
try {
|
|
1843
|
+
if (preferredTaskId) {
|
|
1844
|
+
try {
|
|
1845
|
+
const preferredTask = await this.borgee.getTask({ taskId: preferredTaskId });
|
|
1846
|
+
if (preferredTask.threadId === channelId) {
|
|
1847
|
+
return preferredTask.id.trim().length > 0
|
|
1848
|
+
? { state: 'recovered', currentTaskId: preferredTask.id }
|
|
1849
|
+
: { state: 'recovered' };
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
catch {
|
|
1853
|
+
// Fall through to the visibility-based scan.
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
const channels = await this.borgee.listChannels();
|
|
1857
|
+
const currentChannel = channels.find((channel) => channel.id === channelId);
|
|
1858
|
+
if (currentChannel && currentChannel.kind !== 'thread') {
|
|
1859
|
+
return { state: 'inactive' };
|
|
1860
|
+
}
|
|
1861
|
+
let sawTaskListSuccess = false;
|
|
1862
|
+
let sawTaskListFailure = false;
|
|
1863
|
+
for (const channel of channels) {
|
|
1864
|
+
let tasks;
|
|
1865
|
+
try {
|
|
1866
|
+
tasks = await this.borgee.listTasks({ channelId: channel.id });
|
|
1867
|
+
sawTaskListSuccess = true;
|
|
1868
|
+
}
|
|
1869
|
+
catch {
|
|
1870
|
+
sawTaskListFailure = true;
|
|
1871
|
+
continue;
|
|
1872
|
+
}
|
|
1873
|
+
for (const task of tasks) {
|
|
1874
|
+
if (task.threadId === channelId) {
|
|
1875
|
+
return task.id.trim().length > 0 ? { state: 'recovered', currentTaskId: task.id } : { state: 'recovered' };
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
return sawTaskListSuccess && !sawTaskListFailure ? { state: 'inactive' } : { state: 'unavailable' };
|
|
1880
|
+
}
|
|
1881
|
+
catch {
|
|
1882
|
+
return { state: 'unavailable' };
|
|
1883
|
+
}
|
|
1884
|
+
};
|
|
1827
1885
|
try {
|
|
1828
|
-
const content = await readFile(
|
|
1886
|
+
const content = await readFile(resolveTaskAssignmentStatePath(this.config.stateRootDir, channelId), 'utf8');
|
|
1829
1887
|
const payload = JSON.parse(content);
|
|
1830
1888
|
if (payload.taskAssignmentContext?.active !== true) {
|
|
1831
|
-
return
|
|
1889
|
+
return await recoverFromControlPlane();
|
|
1832
1890
|
}
|
|
1833
1891
|
const currentTaskId = typeof payload.taskAssignmentContext.currentTaskId === 'string'
|
|
1834
1892
|
? payload.taskAssignmentContext.currentTaskId.trim()
|
|
1835
1893
|
: '';
|
|
1836
|
-
|
|
1894
|
+
if (currentTaskId) {
|
|
1895
|
+
try {
|
|
1896
|
+
const task = await this.borgee.getTask({ taskId: currentTaskId });
|
|
1897
|
+
if (task.threadId === channelId) {
|
|
1898
|
+
return { state: 'active', currentTaskId: task.id };
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
catch {
|
|
1902
|
+
// Fall through to the recovery scan below.
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
return await recoverFromControlPlane(currentTaskId || undefined);
|
|
1837
1906
|
}
|
|
1838
1907
|
catch {
|
|
1839
|
-
return
|
|
1908
|
+
return await recoverFromControlPlane();
|
|
1840
1909
|
}
|
|
1841
1910
|
}
|
|
1842
1911
|
async ensureAttentionSnapshotLoaded(channelId, channelState) {
|
|
@@ -1936,11 +2005,28 @@ export class AgentsHost {
|
|
|
1936
2005
|
if (!ORDINARY_HUMAN_EVENT_KINDS.has(eventKind)) {
|
|
1937
2006
|
return false;
|
|
1938
2007
|
}
|
|
1939
|
-
if (
|
|
2008
|
+
if (msg.channel_type === 'dm'
|
|
2009
|
+
&& attentionSnapshot?.wakeMode !== 'mute-non-mention') {
|
|
1940
2010
|
return false;
|
|
1941
2011
|
}
|
|
1942
|
-
|
|
1943
|
-
|
|
2012
|
+
if (attentionSnapshot?.wakeMode === 'mute-non-mention') {
|
|
2013
|
+
this.noteWakeSuppressedAttentionPolicy(msg, authorKind);
|
|
2014
|
+
return true;
|
|
2015
|
+
}
|
|
2016
|
+
return false;
|
|
2017
|
+
}
|
|
2018
|
+
isOrdinaryAgentWakeFiltered(msg, authorKind) {
|
|
2019
|
+
if (authorKind !== 'agent') {
|
|
2020
|
+
return false;
|
|
2021
|
+
}
|
|
2022
|
+
if (msg.message_type === 'task_assignment') {
|
|
2023
|
+
return false;
|
|
2024
|
+
}
|
|
2025
|
+
if (msg.type?.trim() === 'mention') {
|
|
2026
|
+
return false;
|
|
2027
|
+
}
|
|
2028
|
+
const eventKind = msg.type?.trim() ?? '';
|
|
2029
|
+
return ORDINARY_HUMAN_EVENT_KINDS.has(eventKind);
|
|
1944
2030
|
}
|
|
1945
2031
|
applyAttentionUpdate(channelId, baseSnapshot, attentionUpdate, taskThreadContext, options) {
|
|
1946
2032
|
if (!this.attentionFollowSemanticsEnabled) {
|
|
@@ -2109,9 +2195,7 @@ export class AgentsHost {
|
|
|
2109
2195
|
this.recordCollaborationOutcome(msg.channel_id, {
|
|
2110
2196
|
responseState: executed.completed && executed.reply?.awaitingUser
|
|
2111
2197
|
? 'blocked'
|
|
2112
|
-
: executed
|
|
2113
|
-
? 'responded'
|
|
2114
|
-
: 'failed',
|
|
2198
|
+
: this.responseStateForExecuted(executed),
|
|
2115
2199
|
deliveryState: executed.deliveryState ?? 'not-delivered',
|
|
2116
2200
|
...(executed.completed && executed.reply?.awaitingUser
|
|
2117
2201
|
? {
|
|
@@ -2202,6 +2286,21 @@ export class AgentsHost {
|
|
|
2202
2286
|
...(taskThreadCollaborationContract ? { taskThreadCollaborationContract } : {}),
|
|
2203
2287
|
...(collaborationCapabilities ? { collaborationCapabilities } : {}),
|
|
2204
2288
|
...(missedCollaborationDiagnostic ? { missedCollaborationDiagnostic } : {}),
|
|
2289
|
+
...((taskThreadContext.state === 'active'
|
|
2290
|
+
|| taskThreadContext.state === 'recovered'
|
|
2291
|
+
|| taskThreadContext.state === 'unavailable')
|
|
2292
|
+
? {
|
|
2293
|
+
taskAssignmentContextOverride: {
|
|
2294
|
+
active: true,
|
|
2295
|
+
...(taskThreadContext.state === 'active' && taskThreadContext.currentTaskId
|
|
2296
|
+
? { currentTaskId: taskThreadContext.currentTaskId }
|
|
2297
|
+
: {}),
|
|
2298
|
+
},
|
|
2299
|
+
...(taskThreadContext.state === 'active'
|
|
2300
|
+
? { taskAssignmentContextOverridePersistence: 'persist' }
|
|
2301
|
+
: { taskAssignmentContextOverridePersistence: 'ephemeral' }),
|
|
2302
|
+
}
|
|
2303
|
+
: {}),
|
|
2205
2304
|
}, observeProviderProgress
|
|
2206
2305
|
? {
|
|
2207
2306
|
onProgress: (update) => {
|
|
@@ -2350,6 +2449,65 @@ export class AgentsHost {
|
|
|
2350
2449
|
turnExecutionId: activeTurn.turnExecutionId,
|
|
2351
2450
|
});
|
|
2352
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
|
+
}
|
|
2353
2511
|
this.logger.debugError('failed to generate or send reply', {
|
|
2354
2512
|
...turnContext,
|
|
2355
2513
|
error,
|
|
@@ -2561,6 +2719,29 @@ export class AgentsHost {
|
|
|
2561
2719
|
activeTurn.turnExecutionId = null;
|
|
2562
2720
|
}
|
|
2563
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
|
+
}
|
|
2564
2745
|
rememberRecentTurn(channelState, activeTurn) {
|
|
2565
2746
|
if (!activeTurn.turnExecutionId) {
|
|
2566
2747
|
channelState.recentTurn = undefined;
|
|
@@ -2759,6 +2940,34 @@ export class AgentsHost {
|
|
|
2759
2940
|
task = null;
|
|
2760
2941
|
}
|
|
2761
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
|
+
}
|
|
2762
2971
|
if (task.status === 'open') {
|
|
2763
2972
|
try {
|
|
2764
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) {
|
|
@@ -24,8 +24,10 @@ export const INTERNAL_CLAUDE_PROMPT_STRATEGY_ENV = 'AGENTS_HOST_INTERNAL_CLAUDE_
|
|
|
24
24
|
export const MANAGED_RUNTIME_SETTINGS_SCHEMA_VERSION = 1;
|
|
25
25
|
const VALID_PROVIDER_IMPLEMENTATION_ENTRIES = '"claude:v1", "claude:v2", "copilot:v1", or "copilot:v2"';
|
|
26
26
|
export const DEFAULT_INTERNAL_COMPATIBILITY_GATES = Object.freeze([
|
|
27
|
+
ATTENTION_FOLLOW_SEMANTICS_COMPATIBILITY_GATE,
|
|
27
28
|
COLLABORATION_CAPABILITIES_DIAGNOSTICS_COMPATIBILITY_GATE,
|
|
28
29
|
COLLABORATION_OUTCOME_MODEL_COMPATIBILITY_GATE,
|
|
30
|
+
COLLABORATION_SKILL_FIRST_COMPATIBILITY_GATE,
|
|
29
31
|
CONNECTIONS_STATE_LAYER_COMPATIBILITY_GATE,
|
|
30
32
|
CONTEXT_INJECTION_COMPATIBILITY_GATE,
|
|
31
33
|
CODEX_PROVIDER_COMPATIBILITY_GATE,
|
package/dist/config.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export declare const MAX_TIMER_DELAY_MINUTES: number;
|
|
|
3
3
|
export declare const DEFAULT_COPILOT_SESSION_TTL_MINUTES: number;
|
|
4
4
|
export declare const PROVIDER_IDLE_SHUTDOWN_DISABLED_MINUTES = 0;
|
|
5
5
|
export declare const DEFAULT_PROVIDER_IDLE_SHUTDOWN_MINUTES = 10;
|
|
6
|
+
export declare const DEFAULT_ALLOW_CROSS_AGENT_INDEPENDENT_WORKSPACE_HANDOFF = true;
|
|
6
7
|
export declare const DEFAULT_CLAUDE_COMMAND = "claude-agent-acp";
|
|
7
8
|
export declare const LEGACY_CLAUDE_COMMAND = "claude";
|
|
8
9
|
export declare const LEGACY_CLAUDE_ONE_SHOT_ARGS: readonly ["--print", "--permission-mode", "bypassPermissions"];
|
|
@@ -13,6 +14,7 @@ export declare function parseArgs(value: string): string[];
|
|
|
13
14
|
export declare function optionalStringArray(value: unknown, fieldName: string, sourceLabel: string): string[] | undefined;
|
|
14
15
|
export declare function parseCopilotSessionTtlMinutesValue(value: unknown, sourceLabel: string): number;
|
|
15
16
|
export declare function parseProviderIdleShutdownMinutesValue(value: unknown, sourceLabel: string): number;
|
|
17
|
+
export declare function parseBooleanValue(value: unknown, envName: string, sourceLabel: string): boolean;
|
|
16
18
|
export declare function resolveProvider(rawValue: string, sourceLabel: string): ProviderKind;
|
|
17
19
|
export declare function assertProviderCompatibility(provider: ProviderKind, sourceLabel: string, env?: NodeJS.ProcessEnv): void;
|
|
18
20
|
export declare function isLegacyClaudeCompatibilityAlias(command: string, args: string[]): boolean;
|
package/dist/config.js
CHANGED
|
@@ -6,6 +6,7 @@ export const MAX_TIMER_DELAY_MINUTES = MAX_TIMER_DELAY_MS / 60_000;
|
|
|
6
6
|
export const DEFAULT_COPILOT_SESSION_TTL_MINUTES = 2 * 24 * 60;
|
|
7
7
|
export const PROVIDER_IDLE_SHUTDOWN_DISABLED_MINUTES = 0;
|
|
8
8
|
export const DEFAULT_PROVIDER_IDLE_SHUTDOWN_MINUTES = 10;
|
|
9
|
+
export const DEFAULT_ALLOW_CROSS_AGENT_INDEPENDENT_WORKSPACE_HANDOFF = true;
|
|
9
10
|
export const DEFAULT_CLAUDE_COMMAND = 'claude-agent-acp';
|
|
10
11
|
export const LEGACY_CLAUDE_COMMAND = 'claude';
|
|
11
12
|
export const LEGACY_CLAUDE_ONE_SHOT_ARGS = ['--print', '--permission-mode', 'bypassPermissions'];
|
|
@@ -18,6 +19,7 @@ export const DEFAULT_PROVIDER_COMMAND_CONFIG = {
|
|
|
18
19
|
copilotArgs: ['-s', '--no-color', '--allow-all-tools', '--output-format', 'text'],
|
|
19
20
|
copilotSessionTtlMinutes: DEFAULT_COPILOT_SESSION_TTL_MINUTES,
|
|
20
21
|
providerIdleShutdownMinutes: DEFAULT_PROVIDER_IDLE_SHUTDOWN_MINUTES,
|
|
22
|
+
allowCrossAgentIndependentWorkspaceHandoff: DEFAULT_ALLOW_CROSS_AGENT_INDEPENDENT_WORKSPACE_HANDOFF,
|
|
21
23
|
};
|
|
22
24
|
function requireEnv(name, env) {
|
|
23
25
|
return requireNonEmptyString(env[name], `Missing required environment variable: ${name}`);
|
|
@@ -75,6 +77,19 @@ export function parseProviderIdleShutdownMinutesValue(value, sourceLabel) {
|
|
|
75
77
|
}
|
|
76
78
|
return parsed;
|
|
77
79
|
}
|
|
80
|
+
export function parseBooleanValue(value, envName, sourceLabel) {
|
|
81
|
+
if (typeof value === 'boolean') {
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
const normalized = String(value).trim().toLowerCase();
|
|
85
|
+
if (normalized === 'true') {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
if (normalized === 'false') {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
throw new Error(`Invalid ${envName} in ${sourceLabel}: expected true or false`);
|
|
92
|
+
}
|
|
78
93
|
export function resolveProvider(rawValue, sourceLabel) {
|
|
79
94
|
const raw = rawValue.trim().toLowerCase();
|
|
80
95
|
if (raw === 'claude' || raw === 'codex' || raw === 'copilot') {
|
|
@@ -134,6 +149,8 @@ export function resolveProviderCommandConfig(overrides = {}) {
|
|
|
134
149
|
copilotSessionTtlMinutes: overrides.copilotSessionTtlMinutes ?? DEFAULT_PROVIDER_COMMAND_CONFIG.copilotSessionTtlMinutes,
|
|
135
150
|
providerIdleShutdownMinutes: overrides.providerIdleShutdownMinutes
|
|
136
151
|
?? DEFAULT_PROVIDER_COMMAND_CONFIG.providerIdleShutdownMinutes,
|
|
152
|
+
allowCrossAgentIndependentWorkspaceHandoff: overrides.allowCrossAgentIndependentWorkspaceHandoff
|
|
153
|
+
?? DEFAULT_PROVIDER_COMMAND_CONFIG.allowCrossAgentIndependentWorkspaceHandoff,
|
|
137
154
|
};
|
|
138
155
|
}
|
|
139
156
|
export function loadConfigFromEnv(env = process.env) {
|
|
@@ -153,6 +170,10 @@ export function loadConfigFromEnv(env = process.env) {
|
|
|
153
170
|
providerIdleShutdownMinutes: env.PROVIDER_IDLE_SHUTDOWN_MINUTES && env.PROVIDER_IDLE_SHUTDOWN_MINUTES.trim().length > 0
|
|
154
171
|
? parseProviderIdleShutdownMinutesValue(env.PROVIDER_IDLE_SHUTDOWN_MINUTES, 'environment variable PROVIDER_IDLE_SHUTDOWN_MINUTES')
|
|
155
172
|
: DEFAULT_PROVIDER_COMMAND_CONFIG.providerIdleShutdownMinutes,
|
|
173
|
+
allowCrossAgentIndependentWorkspaceHandoff: env.ALLOW_CROSS_AGENT_INDEPENDENT_WORKSPACE_HANDOFF
|
|
174
|
+
&& env.ALLOW_CROSS_AGENT_INDEPENDENT_WORKSPACE_HANDOFF.trim().length > 0
|
|
175
|
+
? parseBooleanValue(env.ALLOW_CROSS_AGENT_INDEPENDENT_WORKSPACE_HANDOFF, 'ALLOW_CROSS_AGENT_INDEPENDENT_WORKSPACE_HANDOFF', 'environment variable ALLOW_CROSS_AGENT_INDEPENDENT_WORKSPACE_HANDOFF')
|
|
176
|
+
: DEFAULT_PROVIDER_COMMAND_CONFIG.allowCrossAgentIndependentWorkspaceHandoff,
|
|
156
177
|
});
|
|
157
178
|
assertProviderCommandCompatibility(provider, providerConfig, 'environment variables');
|
|
158
179
|
return {
|