@dsh-enhanced/assistant-delivery 0.1.23 → 0.1.30
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 +81 -10
- package/lib/acceptance.d.ts +46 -0
- package/lib/acceptance.d.ts.map +1 -0
- package/lib/acceptance.js +2 -0
- package/lib/acceptance.js.map +1 -0
- package/lib/agent-runtime.d.ts +33 -3
- package/lib/agent-runtime.d.ts.map +1 -1
- package/lib/agent-runtime.js +835 -62
- package/lib/agent-runtime.js.map +1 -1
- package/lib/coordinator.d.ts +4 -1
- package/lib/coordinator.d.ts.map +1 -1
- package/lib/coordinator.js +9 -0
- package/lib/coordinator.js.map +1 -1
- package/lib/feedback-command.d.ts +21 -1
- package/lib/feedback-command.d.ts.map +1 -1
- package/lib/feedback-command.js +18 -0
- package/lib/feedback-command.js.map +1 -1
- package/lib/goal-wake-types.d.ts +93 -0
- package/lib/goal-wake-types.d.ts.map +1 -0
- package/lib/goal-wake-types.js +70 -0
- package/lib/goal-wake-types.js.map +1 -0
- package/lib/inbound-images.d.ts.map +1 -1
- package/lib/inbound-images.js +11 -9
- package/lib/inbound-images.js.map +1 -1
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/native-web-owner.d.ts +78 -0
- package/lib/native-web-owner.d.ts.map +1 -0
- package/lib/native-web-owner.js +291 -0
- package/lib/native-web-owner.js.map +1 -0
- package/lib/operator-snapshot.d.ts +80 -0
- package/lib/operator-snapshot.d.ts.map +1 -0
- package/lib/operator-snapshot.js +935 -0
- package/lib/operator-snapshot.js.map +1 -0
- package/lib/operator.d.ts +2 -0
- package/lib/operator.d.ts.map +1 -1
- package/lib/operator.js +10 -0
- package/lib/operator.js.map +1 -1
- package/lib/service.d.ts +63 -13
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +917 -349
- package/lib/service.js.map +1 -1
- package/lib/session-lease-runtime.d.ts +69 -0
- package/lib/session-lease-runtime.d.ts.map +1 -0
- package/lib/session-lease-runtime.js +395 -0
- package/lib/session-lease-runtime.js.map +1 -0
- package/lib/session-lease-types.d.ts +27 -0
- package/lib/session-lease-types.d.ts.map +1 -0
- package/lib/session-lease-types.js +2 -0
- package/lib/session-lease-types.js.map +1 -0
- package/lib/sqlite.d.ts +1 -1
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +201 -3
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +133 -4
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +771 -43
- package/lib/store.js.map +1 -1
- package/lib/types.d.ts +9 -2
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +61 -44
package/lib/agent-runtime.js
CHANGED
|
@@ -2,16 +2,19 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { mkdir, stat } from 'node:fs/promises';
|
|
3
3
|
import { installModelSelection, } from '@deepseek-ai/dsh-agent';
|
|
4
4
|
import { contentHasImage, createUserMessage, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
|
|
5
|
-
import {
|
|
5
|
+
import { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
|
|
6
6
|
import { isAppendSurfaceEvent, SessionId } from '@deepseek-ai/dsh-session';
|
|
7
|
-
import {
|
|
7
|
+
import { setApprovalPolicy, } from '@deepseek-ai/dsh-user-approval';
|
|
8
8
|
import { approvalReviewerOf, setApprovalReviewer, waitForApprovalReviewerSessionEventReady, } from '@dsh-enhanced/assistant-policy';
|
|
9
9
|
import { resolveLlmRouteCapability } from '@dsh-enhanced/llm-route-capabilities';
|
|
10
10
|
import { externalPrincipalId } from './canonical.js';
|
|
11
11
|
import { feedbackSignalInput, feedbackUsage, parseFeedbackCommand, } from './feedback-command.js';
|
|
12
|
+
import { objectiveStateMessage } from './feedback-command.js';
|
|
12
13
|
import { learningCommandUsage, parseLearningCommand, serializeLearningExport, } from './learning-command.js';
|
|
13
14
|
import { isExactDeliveryCommand, learningDispatchRecoveryCode, parseDeliveryCommand, permissionDispatchRecoveryCode, } from './session-commands.js';
|
|
14
15
|
import { DeliveryStoreError } from './store.js';
|
|
16
|
+
import { SessionLeaseUnavailable } from './session-lease-runtime.js';
|
|
17
|
+
import { isDeliveryGoalWakeDeadline } from './goal-wake-types.js';
|
|
15
18
|
import { parseWorkflowCommand, workflowCommandUsage, } from './workflow-command.js';
|
|
16
19
|
const SAFE_NATIVE_COMMANDS = new Set(['compact']);
|
|
17
20
|
class UserSessionCancellation extends Error {
|
|
@@ -477,16 +480,17 @@ async function appendApprovalReviewer(session, reviewer) {
|
|
|
477
480
|
throw new ApprovalReviewerReaderUnavailableError(error);
|
|
478
481
|
}
|
|
479
482
|
}
|
|
480
|
-
function currentPermissionLevel(service, targets,
|
|
481
|
-
const
|
|
483
|
+
function currentPermissionLevel(service, targets, session) {
|
|
484
|
+
const events = session.snapshotEvents();
|
|
485
|
+
const current = service.current(session);
|
|
482
486
|
if (current === targets.full) {
|
|
483
|
-
return
|
|
484
|
-
&&
|
|
487
|
+
return explicitSandboxMode(events) === 'danger-full-access'
|
|
488
|
+
&& explicitApprovalPolicy(events) === 'never'
|
|
485
489
|
&& approvalReviewerOf(events) === 'none'
|
|
486
490
|
? 'full'
|
|
487
491
|
: 'custom';
|
|
488
492
|
}
|
|
489
|
-
if (
|
|
493
|
+
if (explicitSandboxMode(events) !== 'workspace-write' || explicitApprovalPolicy(events) !== 'ask')
|
|
490
494
|
return 'custom';
|
|
491
495
|
const reviewer = approvalReviewerOf(events);
|
|
492
496
|
if (targets.ask === targets.auto && current === targets.ask)
|
|
@@ -507,7 +511,7 @@ function canonicalPresetReviewer(name) {
|
|
|
507
511
|
return undefined;
|
|
508
512
|
}
|
|
509
513
|
async function ensurePermissionReviewer(session, preset, reviewer) {
|
|
510
|
-
if (approvalReviewerOf(session.
|
|
514
|
+
if (approvalReviewerOf(session.snapshotEvents()) === reviewer)
|
|
511
515
|
return;
|
|
512
516
|
if (canonicalPresetReviewer(preset) === reviewer) {
|
|
513
517
|
// PermissionPresetService deliberately makes a net-zero selection a no-op.
|
|
@@ -519,17 +523,17 @@ async function ensurePermissionReviewer(session, preset, reviewer) {
|
|
|
519
523
|
await appendApprovalReviewer(session, reviewer);
|
|
520
524
|
}
|
|
521
525
|
function ensureSandboxMode(session, mode) {
|
|
522
|
-
if (explicitSandboxMode(session.
|
|
526
|
+
if (explicitSandboxMode(session.snapshotEvents()) === mode)
|
|
523
527
|
return;
|
|
524
528
|
setSandboxMode(session, mode);
|
|
525
|
-
if (explicitSandboxMode(session.
|
|
529
|
+
if (explicitSandboxMode(session.snapshotEvents()) !== mode)
|
|
526
530
|
session.append('sandbox/mode', { mode });
|
|
527
531
|
}
|
|
528
532
|
function ensureApprovalPolicy(session, policy) {
|
|
529
|
-
if (explicitApprovalPolicy(session.
|
|
533
|
+
if (explicitApprovalPolicy(session.snapshotEvents()) === policy)
|
|
530
534
|
return;
|
|
531
535
|
setApprovalPolicy(session, policy);
|
|
532
|
-
if (explicitApprovalPolicy(session.
|
|
536
|
+
if (explicitApprovalPolicy(session.snapshotEvents()) !== policy)
|
|
533
537
|
session.append('approval/policy', { policy });
|
|
534
538
|
}
|
|
535
539
|
function permissionLevelLabel(level) {
|
|
@@ -846,7 +850,7 @@ function assertAdoptableUnstartedSession(session, workspace, presetId) {
|
|
|
846
850
|
if (session.header.cwd !== workspace || session.header.agentPreset !== presetId) {
|
|
847
851
|
throw new DurableAgentIdentityError('assistant-delivery: durable orphan session identity does not match the requested generation');
|
|
848
852
|
}
|
|
849
|
-
if (session.
|
|
853
|
+
if (session.snapshotEvents().some(event => event.type === 'turn/start' || event.type === 'user/message')
|
|
850
854
|
|| session.deriveMessages().length > 0) {
|
|
851
855
|
throw new DurableAgentIdentityError('assistant-delivery: refusing to adopt a deterministic session that already started a user turn');
|
|
852
856
|
}
|
|
@@ -1018,16 +1022,27 @@ function finalAssistant(events, from, expectedTurn) {
|
|
|
1018
1022
|
...(failureCode === undefined ? {} : { failureCode }),
|
|
1019
1023
|
};
|
|
1020
1024
|
}
|
|
1025
|
+
function isScheduledGoalRuntimeContext(source) {
|
|
1026
|
+
return (source.kind === 'plugin' && source.plugin === '@deepseek-ai/dsh-system-prompt' && source.form === 'snapshot')
|
|
1027
|
+
|| (source.kind === 'skill-catalog' && source.form === 'catalog');
|
|
1028
|
+
}
|
|
1021
1029
|
export class DshDeliveryRuntime {
|
|
1022
1030
|
ctx;
|
|
1023
1031
|
policy;
|
|
1024
1032
|
options;
|
|
1025
1033
|
dispatchControl = 'explicit';
|
|
1026
1034
|
activeSessionControls = new Map();
|
|
1035
|
+
sessionLeases;
|
|
1027
1036
|
constructor(ctx, policy, options) {
|
|
1028
1037
|
this.ctx = ctx;
|
|
1029
1038
|
this.policy = policy;
|
|
1030
1039
|
this.options = options;
|
|
1040
|
+
this.sessionLeases = options.sessionLeases;
|
|
1041
|
+
}
|
|
1042
|
+
async resumeAgent(options) {
|
|
1043
|
+
const id = String(options.resumeSessionId);
|
|
1044
|
+
this.sessionLeases.assert(id);
|
|
1045
|
+
return this.sessionLeases.resume(this.ctx, options);
|
|
1031
1046
|
}
|
|
1032
1047
|
async cancelActive(binding, command) {
|
|
1033
1048
|
const sessionId = binding.sessionId;
|
|
@@ -1045,8 +1060,9 @@ export class DshDeliveryRuntime {
|
|
|
1045
1060
|
if (!control.controller.signal.aborted) {
|
|
1046
1061
|
control.controller.abort(new UserSessionCancellation(control.command));
|
|
1047
1062
|
}
|
|
1063
|
+
this.sessionLeases.cancel(sessionId, new UserSessionCancellation(control.command));
|
|
1048
1064
|
}
|
|
1049
|
-
//
|
|
1065
|
+
// `whenIdle()` follows every later wakeup. Waiting on it here lets an
|
|
1050
1066
|
// unrelated followup keep `/stop` or `/new` alive forever. Clear pending
|
|
1051
1067
|
// input and wait only for Delivery's exact reply-scheduling boundary.
|
|
1052
1068
|
if (activeAgent)
|
|
@@ -1085,27 +1101,377 @@ export class DshDeliveryRuntime {
|
|
|
1085
1101
|
if (this.activeSessionControls.get(sessionId) === control)
|
|
1086
1102
|
this.activeSessionControls.delete(sessionId);
|
|
1087
1103
|
}
|
|
1088
|
-
|
|
1104
|
+
/**
|
|
1105
|
+
* Keep one Delivery-owned handle alive just long enough for the host's native
|
|
1106
|
+
* goal driver to retire the exact armed goal it observed. Delivery never
|
|
1107
|
+
* queues a round itself: the driver remains the only continuation producer.
|
|
1108
|
+
*/
|
|
1109
|
+
waitForNativeGoalContinuation(binding, envelope, agent, signal) {
|
|
1110
|
+
if (this.options.goalContinuationTimeoutMs === 0)
|
|
1111
|
+
return undefined;
|
|
1112
|
+
const goals = this.ctx.get('goals');
|
|
1113
|
+
const agents = this.ctx.get('agents');
|
|
1114
|
+
if (goals === undefined || agents?.get(agent.id) !== agent)
|
|
1115
|
+
return undefined;
|
|
1116
|
+
let initial;
|
|
1117
|
+
try {
|
|
1118
|
+
initial = goals.get(agent);
|
|
1119
|
+
}
|
|
1120
|
+
catch {
|
|
1121
|
+
return undefined;
|
|
1122
|
+
}
|
|
1123
|
+
// Install this watcher before the foreground followup. A goal can be
|
|
1124
|
+
// resumed or created by that turn, and the native round driver observes
|
|
1125
|
+
// the same idle boundary immediately afterwards.
|
|
1126
|
+
if (initial?.phase !== 'active' || initial.activation !== 'armed')
|
|
1127
|
+
initial = undefined;
|
|
1128
|
+
let goalId = initial === undefined ? undefined : String(initial.id);
|
|
1129
|
+
const session = agent.session;
|
|
1130
|
+
let settled = false;
|
|
1131
|
+
let settledQuiescent = false;
|
|
1132
|
+
// `goal/changed` is synchronous. In particular, disarming our own goal can
|
|
1133
|
+
// emit it before cancelUnknown() has reached finish(false), so remember that
|
|
1134
|
+
// this is a forced, unknown outcome before mutating the native goal.
|
|
1135
|
+
let cancelling = false;
|
|
1136
|
+
let activeGoalSteps = 0;
|
|
1137
|
+
let settleRequested = false;
|
|
1138
|
+
let resolve;
|
|
1139
|
+
const completion = new Promise(done => { resolve = done; });
|
|
1140
|
+
let timeout;
|
|
1141
|
+
let deadlineAt;
|
|
1142
|
+
let removeGoalChanged;
|
|
1143
|
+
let removeStatus;
|
|
1144
|
+
let removeAbort;
|
|
1145
|
+
let removePreStep;
|
|
1146
|
+
let removeRequest;
|
|
1147
|
+
let removePreExecute;
|
|
1148
|
+
let removeToolGuard;
|
|
1149
|
+
const releaseFences = () => {
|
|
1150
|
+
removeGoalChanged?.();
|
|
1151
|
+
removeStatus?.();
|
|
1152
|
+
removeAbort?.();
|
|
1153
|
+
removePreStep?.();
|
|
1154
|
+
removeRequest?.();
|
|
1155
|
+
removePreExecute?.();
|
|
1156
|
+
removeToolGuard?.();
|
|
1157
|
+
removeGoalChanged = undefined;
|
|
1158
|
+
removeStatus = undefined;
|
|
1159
|
+
removeAbort = undefined;
|
|
1160
|
+
removePreStep = undefined;
|
|
1161
|
+
removeRequest = undefined;
|
|
1162
|
+
removePreExecute = undefined;
|
|
1163
|
+
removeToolGuard = undefined;
|
|
1164
|
+
};
|
|
1165
|
+
const finish = (quiescent) => {
|
|
1166
|
+
if (settled)
|
|
1167
|
+
return;
|
|
1168
|
+
settled = true;
|
|
1169
|
+
settledQuiescent = quiescent;
|
|
1170
|
+
resolve(quiescent);
|
|
1171
|
+
};
|
|
1172
|
+
const isAuthorized = () => {
|
|
1173
|
+
try {
|
|
1174
|
+
return this.options.isInboundAuthorized(binding, envelope);
|
|
1175
|
+
}
|
|
1176
|
+
catch {
|
|
1177
|
+
return false;
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
const pinArmedGoal = () => {
|
|
1181
|
+
if (goalId !== undefined || settleRequested)
|
|
1182
|
+
return goalId !== undefined;
|
|
1183
|
+
try {
|
|
1184
|
+
const current = goals.get(agent);
|
|
1185
|
+
if (current?.phase !== 'active' || current.activation !== 'armed')
|
|
1186
|
+
return false;
|
|
1187
|
+
initial = current;
|
|
1188
|
+
goalId = String(current.id);
|
|
1189
|
+
return true;
|
|
1190
|
+
}
|
|
1191
|
+
catch {
|
|
1192
|
+
return false;
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
const disposition = () => {
|
|
1196
|
+
// A replacement is deliberately not adopted. It needs its own explicit
|
|
1197
|
+
// foreground owner/acceptance boundary.
|
|
1198
|
+
if (goalId === undefined || initial === undefined)
|
|
1199
|
+
return 'still-running';
|
|
1200
|
+
if (agents?.get(agent.id) !== agent || agent.session !== session)
|
|
1201
|
+
return 'unavailable';
|
|
1202
|
+
try {
|
|
1203
|
+
const current = goals.get(agent);
|
|
1204
|
+
if (current === undefined)
|
|
1205
|
+
return 'settled';
|
|
1206
|
+
if (String(current.id) !== goalId)
|
|
1207
|
+
return 'replacement';
|
|
1208
|
+
return current.phase !== 'active' || current.activation !== 'armed'
|
|
1209
|
+
? 'settled'
|
|
1210
|
+
: 'still-running';
|
|
1211
|
+
}
|
|
1212
|
+
catch {
|
|
1213
|
+
return 'unavailable';
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
const disarmAndCancel = (reason) => {
|
|
1217
|
+
try {
|
|
1218
|
+
const current = goals.get(agent);
|
|
1219
|
+
if (goalId !== undefined && current !== undefined && String(current.id) === goalId
|
|
1220
|
+
&& current.phase === 'active' && current.activation === 'armed') {
|
|
1221
|
+
goals.disarm(agent);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
catch { }
|
|
1225
|
+
try {
|
|
1226
|
+
agent.cancel({ kind: 'hook', reason });
|
|
1227
|
+
}
|
|
1228
|
+
catch { }
|
|
1229
|
+
};
|
|
1230
|
+
const finishNaturally = () => {
|
|
1231
|
+
// Goal state is only an observation. It cannot turn a cancelled or
|
|
1232
|
+
// revoked foreground authority into a successful, quiescent outcome.
|
|
1233
|
+
if (signal.aborted) {
|
|
1234
|
+
cancelUnknown('assistant-delivery-goal-continuation-cancelled');
|
|
1235
|
+
}
|
|
1236
|
+
else if (!isAuthorized()) {
|
|
1237
|
+
cancelUnknown('assistant-delivery-goal-continuation-authorization-revoked');
|
|
1238
|
+
}
|
|
1239
|
+
else {
|
|
1240
|
+
finish(true);
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
const maybeFinish = () => {
|
|
1244
|
+
if (settled || cancelling || activeGoalSteps !== 0 || goalId === undefined)
|
|
1245
|
+
return;
|
|
1246
|
+
// A terminal goal record does not prove that its final model/tool turn
|
|
1247
|
+
// has retired. `whenIdle()` is deliberately avoided here because it
|
|
1248
|
+
// can follow a later wakeup forever; the current Agent state and inbox
|
|
1249
|
+
// are the bounded observation needed before declaring quiescence.
|
|
1250
|
+
if (agent.status !== 'idle' || agent.inbox.hasPending)
|
|
1251
|
+
return;
|
|
1252
|
+
const current = disposition();
|
|
1253
|
+
if (current === 'settled')
|
|
1254
|
+
finishNaturally();
|
|
1255
|
+
// Missing identity/readback and replacement are intentionally unknown:
|
|
1256
|
+
// neither proves that the exact pinned goal drained.
|
|
1257
|
+
else if (current !== 'still-running')
|
|
1258
|
+
finish(false);
|
|
1259
|
+
};
|
|
1260
|
+
const cancelUnknown = (reason) => {
|
|
1261
|
+
if (cancelling)
|
|
1262
|
+
return;
|
|
1263
|
+
cancelling = true;
|
|
1264
|
+
disarmAndCancel(reason);
|
|
1265
|
+
// A hard timeout or /stop must settle this foreground operation even if
|
|
1266
|
+
// a third-party tool ignores cancellation. Keep the fences until the
|
|
1267
|
+
// Delivery-owned handle is disposed, rather than waiting for idle here.
|
|
1268
|
+
if (!settled)
|
|
1269
|
+
finish(false);
|
|
1270
|
+
};
|
|
1271
|
+
const guardContinuationAuthorization = () => {
|
|
1272
|
+
if (cancelling)
|
|
1273
|
+
return false;
|
|
1274
|
+
if (isAuthorized())
|
|
1275
|
+
return true;
|
|
1276
|
+
cancelUnknown('assistant-delivery-goal-continuation-authorization-revoked');
|
|
1277
|
+
return false;
|
|
1278
|
+
};
|
|
1279
|
+
const teardownTimeoutMs = () => deadlineAt === undefined
|
|
1280
|
+
? undefined
|
|
1281
|
+
: Math.max(0, deadlineAt - Date.now());
|
|
1282
|
+
const isQuiescent = () => {
|
|
1283
|
+
if (!settledQuiescent || cancelling)
|
|
1284
|
+
return false;
|
|
1285
|
+
// The goal can reach a terminal native state before the Agent scope has
|
|
1286
|
+
// finished disposing. Authority is still live at that point.
|
|
1287
|
+
if (signal.aborted || !isAuthorized()) {
|
|
1288
|
+
cancelUnknown(signal.aborted
|
|
1289
|
+
? 'assistant-delivery-goal-continuation-cancelled'
|
|
1290
|
+
: 'assistant-delivery-goal-continuation-authorization-revoked');
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
return true;
|
|
1294
|
+
};
|
|
1295
|
+
removeGoalChanged = this.ctx.on('goal/changed', ({ agent: changed }) => {
|
|
1296
|
+
if (changed !== agent)
|
|
1297
|
+
return;
|
|
1298
|
+
pinArmedGoal();
|
|
1299
|
+
maybeFinish();
|
|
1300
|
+
});
|
|
1301
|
+
removeStatus = this.ctx.on('agent/status', ({ agent: changed, status }) => {
|
|
1302
|
+
// The round driver can wake after an idle status, so idle alone is not a
|
|
1303
|
+
// completion signal. It is only useful after the goal itself disarmed.
|
|
1304
|
+
if (changed === agent && status === 'idle')
|
|
1305
|
+
maybeFinish();
|
|
1306
|
+
});
|
|
1307
|
+
const aborted = () => cancelUnknown('assistant-delivery-goal-continuation-cancelled');
|
|
1308
|
+
signal.addEventListener('abort', aborted, { once: true });
|
|
1309
|
+
removeAbort = () => signal.removeEventListener('abort', aborted);
|
|
1310
|
+
removePreStep = agent.ctx.on('agent/pre-step', async (_input, next) => {
|
|
1311
|
+
// The native loop supplies only the inbox claim for a later model step;
|
|
1312
|
+
// after a tool result it need not repeat the original `goal` source.
|
|
1313
|
+
// Once this watcher fixed a GoalId, every later model step on the owned
|
|
1314
|
+
// Agent is therefore fenced until its handle tears down.
|
|
1315
|
+
if (goalId === undefined)
|
|
1316
|
+
return await next();
|
|
1317
|
+
if (!guardContinuationAuthorization())
|
|
1318
|
+
return { kind: 'reject' };
|
|
1319
|
+
activeGoalSteps += 1;
|
|
1320
|
+
try {
|
|
1321
|
+
const result = await next();
|
|
1322
|
+
// A pre-step waterfall's `next()` can finish before the next model
|
|
1323
|
+
// boundary. Recheck immediately after it so a revocation in that
|
|
1324
|
+
// window still cancels the owned Agent before another request/tool.
|
|
1325
|
+
if (!guardContinuationAuthorization())
|
|
1326
|
+
return { kind: 'reject' };
|
|
1327
|
+
return result;
|
|
1328
|
+
}
|
|
1329
|
+
finally {
|
|
1330
|
+
activeGoalSteps -= 1;
|
|
1331
|
+
// Do not wait for `whenIdle()`: an uncooperative tool can keep it
|
|
1332
|
+
// pending forever. The timeout/cancellation path already settled
|
|
1333
|
+
// non-quiescent, and the fences remain until handle teardown.
|
|
1334
|
+
maybeFinish();
|
|
1335
|
+
}
|
|
1336
|
+
}, { prepend: true });
|
|
1337
|
+
removeRequest = agent.ctx.on('agent/request', async (_payload, next) => {
|
|
1338
|
+
if (goalId === undefined)
|
|
1339
|
+
return await next();
|
|
1340
|
+
if (!guardContinuationAuthorization()) {
|
|
1341
|
+
throw new Error('assistant-delivery: goal continuation authorization revoked before model request');
|
|
1342
|
+
}
|
|
1343
|
+
const request = await next();
|
|
1344
|
+
if (!guardContinuationAuthorization()) {
|
|
1345
|
+
throw new Error('assistant-delivery: goal continuation authorization revoked while preparing model request');
|
|
1346
|
+
}
|
|
1347
|
+
return request;
|
|
1348
|
+
}, { prepend: true });
|
|
1349
|
+
removePreExecute = agent.ctx.on('tools/pre-execute', async (_execution, next) => {
|
|
1350
|
+
if (guardContinuationAuthorization())
|
|
1351
|
+
return await next();
|
|
1352
|
+
return { kind: 'deny', reason: 'assistant-delivery: goal continuation authorization revoked' };
|
|
1353
|
+
}, { prepend: true });
|
|
1354
|
+
removeToolGuard = agent.ctx.tools.guard(() => guardContinuationAuthorization()
|
|
1355
|
+
? undefined
|
|
1356
|
+
: 'assistant-delivery: goal continuation authorization revoked');
|
|
1357
|
+
const beginSettling = () => {
|
|
1358
|
+
if (!settleRequested) {
|
|
1359
|
+
settleRequested = true;
|
|
1360
|
+
// A goal may create and complete during the foreground pre-step,
|
|
1361
|
+
// before Delivery reaches `settle()`. That still leaves its Agent
|
|
1362
|
+
// scope to tear down, so it needs the same bounded deadline.
|
|
1363
|
+
deadlineAt = Date.now() + this.options.goalContinuationTimeoutMs;
|
|
1364
|
+
}
|
|
1365
|
+
if (settled)
|
|
1366
|
+
return completion;
|
|
1367
|
+
if (goalId === undefined) {
|
|
1368
|
+
finishNaturally();
|
|
1369
|
+
return completion;
|
|
1370
|
+
}
|
|
1371
|
+
timeout = setTimeout(() => cancelUnknown('assistant-delivery-goal-continuation-timeout'), this.options.goalContinuationTimeoutMs);
|
|
1372
|
+
timeout.unref?.();
|
|
1373
|
+
// A driver may have already claimed its first round before the original
|
|
1374
|
+
// Delivery turn reached its reply boundary. All step and tool guards
|
|
1375
|
+
// were installed above, before that round could begin.
|
|
1376
|
+
if (signal.aborted)
|
|
1377
|
+
cancelUnknown('assistant-delivery-goal-continuation-cancelled');
|
|
1378
|
+
else if (!isAuthorized())
|
|
1379
|
+
cancelUnknown('assistant-delivery-goal-continuation-authorization-revoked');
|
|
1380
|
+
else
|
|
1381
|
+
maybeFinish();
|
|
1382
|
+
return completion;
|
|
1383
|
+
};
|
|
1384
|
+
if (signal.aborted)
|
|
1385
|
+
aborted();
|
|
1386
|
+
return {
|
|
1387
|
+
settle: beginSettling,
|
|
1388
|
+
teardownTimeoutMs,
|
|
1389
|
+
isQuiescent,
|
|
1390
|
+
dispose: () => {
|
|
1391
|
+
if (timeout !== undefined)
|
|
1392
|
+
clearTimeout(timeout);
|
|
1393
|
+
if (!settled)
|
|
1394
|
+
cancelUnknown('assistant-delivery-goal-continuation-teardown');
|
|
1395
|
+
releaseFences();
|
|
1396
|
+
},
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
async disposeAfterReplyBoundary(sessionId, handle, afterDisposed, abandonDisposal = false, timeoutMs) {
|
|
1089
1400
|
const control = this.activeSessionControls.get(sessionId);
|
|
1090
1401
|
control?.resolveReplySafe();
|
|
1091
|
-
if (handle === undefined)
|
|
1092
|
-
|
|
1402
|
+
if (handle === undefined) {
|
|
1403
|
+
afterDisposed?.();
|
|
1404
|
+
return true;
|
|
1405
|
+
}
|
|
1093
1406
|
const disposal = Promise.resolve().then(() => handle.dispose());
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1407
|
+
let clearDeadline;
|
|
1408
|
+
const deadline = timeoutMs === undefined
|
|
1409
|
+
? undefined
|
|
1410
|
+
: timeoutMs <= 0
|
|
1411
|
+
? Promise.resolve('timed-out')
|
|
1412
|
+
: new Promise(resolve => {
|
|
1413
|
+
const timeout = setTimeout(() => resolve('timed-out'), timeoutMs);
|
|
1414
|
+
timeout.unref?.();
|
|
1415
|
+
clearDeadline = () => clearTimeout(timeout);
|
|
1416
|
+
});
|
|
1417
|
+
let removeAbort;
|
|
1418
|
+
const aborted = control === undefined
|
|
1419
|
+
? undefined
|
|
1420
|
+
: new Promise(resolve => {
|
|
1421
|
+
const abort = () => resolve('aborted');
|
|
1422
|
+
control.controller.signal.addEventListener('abort', abort, { once: true });
|
|
1423
|
+
removeAbort = () => control.controller.signal.removeEventListener('abort', abort);
|
|
1424
|
+
if (control.controller.signal.aborted)
|
|
1425
|
+
abort();
|
|
1426
|
+
});
|
|
1427
|
+
let released = false;
|
|
1428
|
+
const releaseAfterDisposal = () => {
|
|
1429
|
+
if (released)
|
|
1430
|
+
return;
|
|
1431
|
+
released = true;
|
|
1432
|
+
try {
|
|
1433
|
+
afterDisposed?.();
|
|
1434
|
+
}
|
|
1435
|
+
catch (error) {
|
|
1436
|
+
this.ctx.logger.warn(`assistant-delivery: continuation fence release failed: ${String(error)}`);
|
|
1437
|
+
}
|
|
1438
|
+
};
|
|
1439
|
+
let disposition;
|
|
1440
|
+
try {
|
|
1441
|
+
disposition = control === undefined
|
|
1442
|
+
? (abandonDisposal ? 'abandoned' : await Promise.race([
|
|
1443
|
+
disposal.then(() => 'disposed', () => 'rejected'),
|
|
1444
|
+
...(deadline === undefined ? [] : [deadline]),
|
|
1445
|
+
]))
|
|
1446
|
+
: await Promise.race([
|
|
1447
|
+
disposal.then(() => 'disposed', () => 'rejected'),
|
|
1448
|
+
control.cancelRequested.then(() => 'cancelled'),
|
|
1449
|
+
...(aborted === undefined ? [] : [aborted]),
|
|
1450
|
+
...(deadline === undefined ? [] : [deadline]),
|
|
1451
|
+
...(abandonDisposal ? [Promise.resolve('abandoned')] : []),
|
|
1452
|
+
]);
|
|
1453
|
+
}
|
|
1454
|
+
finally {
|
|
1455
|
+
removeAbort?.();
|
|
1456
|
+
clearDeadline?.();
|
|
1457
|
+
}
|
|
1458
|
+
if (disposition === 'cancelled' || disposition === 'abandoned'
|
|
1459
|
+
|| disposition === 'aborted' || disposition === 'timed-out') {
|
|
1101
1460
|
// The Delivery turn is already cancelled and cannot enqueue another
|
|
1102
1461
|
// reply. Teardown remains owned, but a third-party disposer must not
|
|
1103
1462
|
// hold the conversation transition or its fresh generation hostage.
|
|
1104
|
-
void disposal.
|
|
1105
|
-
this.ctx.logger.warn(`assistant-delivery:
|
|
1463
|
+
void disposal.then(() => releaseAfterDisposal(), error => {
|
|
1464
|
+
this.ctx.logger.warn(`assistant-delivery: detached session disposer failed for ${sessionFingerprint(sessionId)}: ${String(error)}`);
|
|
1465
|
+
releaseAfterDisposal();
|
|
1106
1466
|
});
|
|
1107
|
-
return;
|
|
1467
|
+
return false;
|
|
1108
1468
|
}
|
|
1469
|
+
releaseAfterDisposal();
|
|
1470
|
+
if (disposition === 'rejected') {
|
|
1471
|
+
this.ctx.logger.warn(`assistant-delivery: session disposer failed for ${sessionFingerprint(sessionId)}`);
|
|
1472
|
+
return false;
|
|
1473
|
+
}
|
|
1474
|
+
return true;
|
|
1109
1475
|
}
|
|
1110
1476
|
/** Wait for only the turn that claims Delivery's exact message identity. */
|
|
1111
1477
|
followupTurn(agent, message, onClaimed, onSettled) {
|
|
@@ -1209,7 +1575,7 @@ export class DshDeliveryRuntime {
|
|
|
1209
1575
|
}
|
|
1210
1576
|
}
|
|
1211
1577
|
/**
|
|
1212
|
-
* Queue a synthetic completion turn only after the previous exact turn retired.
|
|
1578
|
+
* Queue a synthetic completion turn only after the previous exact turn retired. The runtime may still
|
|
1213
1579
|
* serialize scoped tool schemas, but every call is denied before approval/execution while this
|
|
1214
1580
|
* turn owns the Agent, so recovering prose cannot repeat an external side effect.
|
|
1215
1581
|
*/
|
|
@@ -1264,7 +1630,7 @@ export class DshDeliveryRuntime {
|
|
|
1264
1630
|
if (agent.status !== 'idle' || agent.inbox.hasPending) {
|
|
1265
1631
|
throw new AutomaticCompletionUnavailableError('assistant-delivery: automatic completion lost the idle scheduling boundary');
|
|
1266
1632
|
}
|
|
1267
|
-
const latestBoundary = agent.session.
|
|
1633
|
+
const latestBoundary = agent.session.snapshotEvents().findLast(event => event.type === 'turn/start' || event.type === 'turn/end');
|
|
1268
1634
|
if (latestBoundary?.type !== 'turn/end' || latestBoundary.data.turn !== expectedPreviousTurn) {
|
|
1269
1635
|
throw new AutomaticCompletionUnavailableError('assistant-delivery: another turn crossed the automatic completion boundary');
|
|
1270
1636
|
}
|
|
@@ -1292,15 +1658,15 @@ export class DshDeliveryRuntime {
|
|
|
1292
1658
|
releaseToolBarriers();
|
|
1293
1659
|
}
|
|
1294
1660
|
}
|
|
1295
|
-
async setupAgent(agentCtx, workspace, presetId, principal, selected, agentPresets) {
|
|
1661
|
+
async setupAgent(agentCtx, workspace, presetId, principal, selected, agentPresets, initiator = 'external') {
|
|
1296
1662
|
const agent = agentCtx.agent;
|
|
1297
1663
|
if (agent === undefined)
|
|
1298
1664
|
throw new Error('assistant-delivery: unpublished Agent identity is missing');
|
|
1299
1665
|
if (agent.session.header.cwd !== workspace || agent.session.header.agentPreset !== presetId) {
|
|
1300
1666
|
throw new DurableAgentIdentityError('assistant-delivery: durable Agent identity does not match its conversation binding');
|
|
1301
1667
|
}
|
|
1302
|
-
const unbind = this.policy.bindInitiator(agent,
|
|
1303
|
-
agentCtx.effect(() => unbind,
|
|
1668
|
+
const unbind = this.policy.bindInitiator(agent, initiator, principal);
|
|
1669
|
+
agentCtx.effect(() => unbind, `assistant-delivery.${initiator}-initiator`);
|
|
1304
1670
|
installModelSelection(agentCtx, { current: selected, assembled: undefined });
|
|
1305
1671
|
await agentPresets?.mount(agentCtx, presetId);
|
|
1306
1672
|
}
|
|
@@ -1343,7 +1709,9 @@ export class DshDeliveryRuntime {
|
|
|
1343
1709
|
}
|
|
1344
1710
|
}
|
|
1345
1711
|
async reconcileNativeFullPermissionReviewer(session) {
|
|
1712
|
+
this.sessionLeases.assert(String(session.id));
|
|
1346
1713
|
const result = await this.policy.reconcileNativeFullReviewer(session);
|
|
1714
|
+
this.sessionLeases.assert(String(session.id));
|
|
1347
1715
|
if (result === 'unavailable') {
|
|
1348
1716
|
throw new ApprovalReviewerReaderUnavailableError(new Error('assistant-policy: native full reviewer reconciliation is unavailable'));
|
|
1349
1717
|
}
|
|
@@ -1482,7 +1850,7 @@ export class DshDeliveryRuntime {
|
|
|
1482
1850
|
signal.throwIfAborted();
|
|
1483
1851
|
await requireWorkspace(binding.workspace);
|
|
1484
1852
|
signal.throwIfAborted();
|
|
1485
|
-
handle = await
|
|
1853
|
+
handle = await this.resumeAgent({
|
|
1486
1854
|
resumeSessionId: SessionId(binding.sessionId),
|
|
1487
1855
|
signal,
|
|
1488
1856
|
agentOptions: {
|
|
@@ -1532,7 +1900,7 @@ export class DshDeliveryRuntime {
|
|
|
1532
1900
|
const flushed = await sessions.flush(session);
|
|
1533
1901
|
return reviewerReady
|
|
1534
1902
|
&& flushed
|
|
1535
|
-
&& currentPermissionLevel(permissionPresets, targets, session
|
|
1903
|
+
&& currentPermissionLevel(permissionPresets, targets, session) === 'ask';
|
|
1536
1904
|
}
|
|
1537
1905
|
catch {
|
|
1538
1906
|
return false;
|
|
@@ -1596,8 +1964,8 @@ export class DshDeliveryRuntime {
|
|
|
1596
1964
|
if (action.kind === 'invalid' || action.kind === 'warn-full') {
|
|
1597
1965
|
return retryPermissionRecovery(recovery ?? 'failure-notice');
|
|
1598
1966
|
}
|
|
1599
|
-
const currentStateHash = permissionStateHash(session.
|
|
1600
|
-
const beforeLevel = currentPermissionLevel(permissionPresets, targets, session
|
|
1967
|
+
const currentStateHash = permissionStateHash(session.snapshotEvents(), targets.tableFingerprint);
|
|
1968
|
+
const beforeLevel = currentPermissionLevel(permissionPresets, targets, session);
|
|
1601
1969
|
const recoveredCommittedTarget = recovery === 'commit'
|
|
1602
1970
|
&& action.kind === 'switch'
|
|
1603
1971
|
&& beforeLevel === action.level;
|
|
@@ -1641,7 +2009,7 @@ export class DshDeliveryRuntime {
|
|
|
1641
2009
|
return reply({ text: '权限卡片已过期或状态已变化;当前权限未改变。请重新发送 /permissions。', format: 'plain' });
|
|
1642
2010
|
}
|
|
1643
2011
|
if (action.kind === 'show') {
|
|
1644
|
-
const level = currentPermissionLevel(permissionPresets, targets, session
|
|
2012
|
+
const level = currentPermissionLevel(permissionPresets, targets, session);
|
|
1645
2013
|
const emergencyStop = this.policy.getEmergencyStop();
|
|
1646
2014
|
if (emergencyStop.enabled)
|
|
1647
2015
|
return authorizationFailure('revoked');
|
|
@@ -1653,7 +2021,7 @@ export class DshDeliveryRuntime {
|
|
|
1653
2021
|
issuedAt,
|
|
1654
2022
|
expiresAt: issuedAt + this.options.permissionPickerTtlMs,
|
|
1655
2023
|
current: level,
|
|
1656
|
-
expectedStateHash: permissionStateHash(session.
|
|
2024
|
+
expectedStateHash: permissionStateHash(session.snapshotEvents(), targets.tableFingerprint),
|
|
1657
2025
|
emergencyStopVersion: emergencyStop.version,
|
|
1658
2026
|
bindingVersion: binding.version,
|
|
1659
2027
|
sessionId: binding.sessionId,
|
|
@@ -1727,7 +2095,7 @@ export class DshDeliveryRuntime {
|
|
|
1727
2095
|
signal.throwIfAborted();
|
|
1728
2096
|
return retryPermissionRecovery('failure-notice');
|
|
1729
2097
|
}
|
|
1730
|
-
const afterLevel = currentPermissionLevel(permissionPresets, targets, session
|
|
2098
|
+
const afterLevel = currentPermissionLevel(permissionPresets, targets, session);
|
|
1731
2099
|
if (afterLevel !== action.level) {
|
|
1732
2100
|
const safeAskPersisted = await compensateToAsk();
|
|
1733
2101
|
signal.throwIfAborted();
|
|
@@ -1812,9 +2180,14 @@ export class DshDeliveryRuntime {
|
|
|
1812
2180
|
const setup = async (agentCtx) => {
|
|
1813
2181
|
await this.setupAgent(agentCtx, workspace, presetId, externalPrincipalId(input.envelope.principal), selected, agentPresets);
|
|
1814
2182
|
};
|
|
2183
|
+
const lease = this.sessionLeases.open({ kind: 'construction', sessionId: String(id),
|
|
2184
|
+
conversation: input.envelope.conversation, principal: input.envelope.principal,
|
|
2185
|
+
workspace, agentPreset: presetId, generation: input.generation,
|
|
2186
|
+
...(input.previous === undefined ? {} : { previous: input.previous }) }, input.signal);
|
|
1815
2187
|
let handle;
|
|
1816
2188
|
let adopted = false;
|
|
1817
2189
|
try {
|
|
2190
|
+
lease.dispatch();
|
|
1818
2191
|
const agentOptions = {
|
|
1819
2192
|
provider: selected.provider,
|
|
1820
2193
|
model: selected.model,
|
|
@@ -1823,22 +2196,22 @@ export class DshDeliveryRuntime {
|
|
|
1823
2196
|
const persistence = this.ctx.get('sessionPersistence');
|
|
1824
2197
|
const persisted = persistence === undefined
|
|
1825
2198
|
? false
|
|
1826
|
-
: (await persistence.list(
|
|
2199
|
+
: (await persistence.list(lease.signal)).some(header => String(header.id) === String(id));
|
|
1827
2200
|
if (persisted) {
|
|
1828
|
-
handle = await
|
|
2201
|
+
handle = await this.resumeAgent({
|
|
1829
2202
|
resumeSessionId: id,
|
|
1830
2203
|
agentOptions,
|
|
1831
|
-
signal:
|
|
2204
|
+
signal: lease.signal,
|
|
1832
2205
|
setup,
|
|
1833
2206
|
});
|
|
1834
2207
|
adopted = true;
|
|
1835
2208
|
}
|
|
1836
2209
|
if (handle === undefined) {
|
|
1837
|
-
handle = await
|
|
2210
|
+
handle = await this.sessionLeases.create(this.ctx, {
|
|
1838
2211
|
sessionId: id,
|
|
1839
2212
|
meta: { cwd: workspace, agentPreset: presetId },
|
|
1840
2213
|
agentOptions,
|
|
1841
|
-
signal:
|
|
2214
|
+
signal: lease.signal,
|
|
1842
2215
|
setup,
|
|
1843
2216
|
});
|
|
1844
2217
|
}
|
|
@@ -1853,8 +2226,340 @@ export class DshDeliveryRuntime {
|
|
|
1853
2226
|
policyRef: input.previous?.policyRef ?? this.options.policyRef };
|
|
1854
2227
|
}
|
|
1855
2228
|
finally {
|
|
1856
|
-
|
|
2229
|
+
try {
|
|
2230
|
+
if (handle !== undefined)
|
|
2231
|
+
await handle.dispose();
|
|
2232
|
+
}
|
|
2233
|
+
finally {
|
|
2234
|
+
lease.close();
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
/**
|
|
2239
|
+
* Resume one previously owner-scheduled native Goal. This is deliberately
|
|
2240
|
+
* not an inbound-message path: it creates no delivery source and never
|
|
2241
|
+
* calls followup(). The native round driver remains the only producer of
|
|
2242
|
+
* `source.kind === 'goal'` work.
|
|
2243
|
+
*/
|
|
2244
|
+
async resumeScheduledGoal(binding, input) {
|
|
2245
|
+
const denied = () => ({ outcome: 'denied', dispatched: false, quiescent: false });
|
|
2246
|
+
const now = Date.now();
|
|
2247
|
+
if (!this.validGoalWakeInput(binding, input, now))
|
|
2248
|
+
return denied();
|
|
2249
|
+
const duration = input.deadlineAt - now;
|
|
2250
|
+
const deadline = new AbortController();
|
|
2251
|
+
const expire = setTimeout(() => deadline.abort(new Error('assistant-delivery: scheduled goal wake expired')), duration);
|
|
2252
|
+
expire.unref?.();
|
|
2253
|
+
const abort = () => deadline.abort(input.signal.reason);
|
|
2254
|
+
input.signal.addEventListener('abort', abort, { once: true });
|
|
2255
|
+
if (input.signal.aborted)
|
|
2256
|
+
abort();
|
|
2257
|
+
const signal = deadline.signal;
|
|
2258
|
+
let lease;
|
|
2259
|
+
let control;
|
|
2260
|
+
let handle;
|
|
2261
|
+
let releaseFences;
|
|
2262
|
+
let dispatched = false;
|
|
2263
|
+
let terminal = false;
|
|
2264
|
+
let disposed = false;
|
|
2265
|
+
let goalOutput;
|
|
2266
|
+
try {
|
|
2267
|
+
signal.throwIfAborted();
|
|
2268
|
+
const persisted = this.options.getModelSelection(binding.conversation);
|
|
2269
|
+
const requested = toModelRoute(persisted ?? { provider: this.options.provider, model: this.options.model });
|
|
2270
|
+
const resolved = await this.resolveExecutionRoute(binding.conversation, persisted, requested, signal);
|
|
2271
|
+
if ('retry' in resolved) {
|
|
2272
|
+
return denied();
|
|
2273
|
+
}
|
|
2274
|
+
const selected = agentSelection(resolved.route);
|
|
2275
|
+
const presets = this.options.getAgentPresets();
|
|
2276
|
+
const presetId = presets === undefined ? binding.agentPreset : (await presets.resolve(binding.agentPreset)).id;
|
|
2277
|
+
signal.throwIfAborted();
|
|
2278
|
+
await requireWorkspace(binding.workspace);
|
|
2279
|
+
signal.throwIfAborted();
|
|
2280
|
+
try {
|
|
2281
|
+
lease = this.sessionLeases.open({ kind: 'bound', binding }, signal);
|
|
2282
|
+
}
|
|
2283
|
+
catch (error) {
|
|
2284
|
+
if (error instanceof SessionLeaseUnavailable) {
|
|
2285
|
+
return { outcome: error.kind, dispatched: false, quiescent: false };
|
|
2286
|
+
}
|
|
2287
|
+
throw error;
|
|
2288
|
+
}
|
|
2289
|
+
control = this.beginSessionControl(binding.sessionId, lease.signal);
|
|
2290
|
+
lease.dispatch();
|
|
2291
|
+
handle = await this.resumeAgent({
|
|
2292
|
+
resumeSessionId: SessionId(binding.sessionId),
|
|
2293
|
+
signal: control.controller.signal,
|
|
2294
|
+
agentOptions: { provider: selected.provider, model: selected.model, maxTokens: this.options.maxOutputTokens },
|
|
2295
|
+
setup: async (agentCtx) => {
|
|
2296
|
+
// Restored inbox entries may wake immediately on Session publication.
|
|
2297
|
+
// Install the background-only gate before that publication boundary.
|
|
2298
|
+
if (agentCtx.agent === undefined)
|
|
2299
|
+
throw new Error('assistant-delivery: wake Agent missing');
|
|
2300
|
+
releaseFences = this.installScheduledGoalFences(agentCtx.agent, input);
|
|
2301
|
+
await this.setupAgent(agentCtx, binding.workspace, presetId, input.attestation.principalId, selected, presets, 'background');
|
|
2302
|
+
const llm = this.ctx.get('llm');
|
|
2303
|
+
if (llm === undefined)
|
|
2304
|
+
throw new Error('assistant-delivery: llm service is required');
|
|
2305
|
+
requireAdapterToolCallProtocol(llm, selected.provider, selected.model, presetId, agentCtx.tools.schemas(agentCtx.agent).length);
|
|
2306
|
+
},
|
|
2307
|
+
});
|
|
2308
|
+
const agent = handle.agent;
|
|
2309
|
+
this.assertScheduledGoal(input, agent, 'before-resume');
|
|
2310
|
+
await this.reconcileNativeFullPermissionReviewer(agent.session);
|
|
2311
|
+
this.assertScheduledGoal(input, agent, 'before-resume');
|
|
2312
|
+
const goals = this.ctx.get('goals');
|
|
2313
|
+
const current = goals?.get(agent);
|
|
2314
|
+
if (goals === undefined || current === undefined || String(current.id) !== input.native.goalId
|
|
2315
|
+
|| current.revision !== input.native.revision || current.phase !== 'paused'
|
|
2316
|
+
|| current.activation !== 'disarmed' || current.roundsStarted >= current.maxGoalRounds) {
|
|
2317
|
+
return denied();
|
|
2318
|
+
}
|
|
2319
|
+
if (agent.status !== 'idle' || agent.inbox.hasPending) {
|
|
2320
|
+
return denied();
|
|
2321
|
+
}
|
|
2322
|
+
this.assertScheduledGoal(input, agent, 'before-resume');
|
|
2323
|
+
input.beforeResume(agent);
|
|
2324
|
+
// `beforeResume` is the durable goal-wake CAS. Once it returned, native
|
|
2325
|
+
// resume might have committed before throwing, so this invocation is no
|
|
2326
|
+
// longer retry-safe regardless of the result of `goals.resume()`.
|
|
2327
|
+
dispatched = true;
|
|
2328
|
+
this.assertScheduledGoal(input, agent, 'before-resume');
|
|
2329
|
+
const beforeSequence = agent.session.snapshotEvents().at(-1)?.seq ?? 0;
|
|
2330
|
+
goals.resume(agent, { id: current.id, revision: current.revision });
|
|
2331
|
+
terminal = await this.waitForScheduledGoalTerminal(agent, input, control.controller.signal);
|
|
2332
|
+
if (terminal && input.includeOutput === true) {
|
|
2333
|
+
this.assertScheduledGoal(input, agent, 'terminal');
|
|
2334
|
+
const events = agent.session.snapshotEvents().filter(event => event.seq > beforeSequence);
|
|
2335
|
+
const message = events.findLast(event => event.type === 'user/message'
|
|
2336
|
+
&& event.data.source.kind === 'goal' && String(event.data.source.goalId) === input.native.goalId
|
|
2337
|
+
&& event.data.source.revision === input.native.revision + 1);
|
|
2338
|
+
const start = message === undefined ? undefined : events.findLast(event => event.type === 'turn/start' && event.seq < message.seq);
|
|
2339
|
+
const turn = start?.type === 'turn/start' ? start.data.turn : undefined;
|
|
2340
|
+
const end = events.find(event => event.type === 'turn/end' && event.data.turn === turn);
|
|
2341
|
+
const inputs = start === undefined || end === undefined ? [] : events.filter(event => event.seq > start.seq && event.seq < end.seq
|
|
2342
|
+
&& event.type === 'user/message' && !isScheduledGoalRuntimeContext(event.data.source));
|
|
2343
|
+
const output = finalAssistant(events, 0, inputs.length === 1 && inputs[0]?.seq === message?.seq ? turn : undefined);
|
|
2344
|
+
if (output.completed && !output.truncated && !output.stopped && !output.hasUnpairedToolCall
|
|
2345
|
+
&& output.failureCode === undefined && output.text.trim() !== ''
|
|
2346
|
+
&& Buffer.byteLength(output.text, 'utf8') <= this.options.maxTextBytes)
|
|
2347
|
+
goalOutput = output.text;
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
catch (error) {
|
|
2351
|
+
if (!(error instanceof SessionLeaseUnavailable) && !signal.aborted) {
|
|
2352
|
+
this.ctx.logger.warn(`assistant-delivery: scheduled goal wake failed: ${String(error)}`);
|
|
2353
|
+
}
|
|
2354
|
+
terminal = false;
|
|
2355
|
+
try {
|
|
2356
|
+
const agent = handle?.agent;
|
|
2357
|
+
const goals = this.ctx.get('goals');
|
|
2358
|
+
if (agent !== undefined) {
|
|
2359
|
+
try {
|
|
2360
|
+
goals?.disarm(agent);
|
|
2361
|
+
}
|
|
2362
|
+
catch { }
|
|
2363
|
+
agent.cancel({ kind: 'hook', reason: 'assistant-delivery-scheduled-goal-wake-cancelled' });
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
catch { }
|
|
1857
2367
|
}
|
|
2368
|
+
finally {
|
|
2369
|
+
input.signal.removeEventListener('abort', abort);
|
|
2370
|
+
clearTimeout(expire);
|
|
2371
|
+
try {
|
|
2372
|
+
const remaining = Math.max(0, input.deadlineAt - Date.now());
|
|
2373
|
+
disposed = await this.disposeAfterReplyBoundary(binding.sessionId, handle, releaseFences, !terminal, remaining);
|
|
2374
|
+
}
|
|
2375
|
+
finally {
|
|
2376
|
+
if (handle === undefined)
|
|
2377
|
+
releaseFences?.();
|
|
2378
|
+
if (control !== undefined)
|
|
2379
|
+
this.endSessionControl(binding.sessionId, control);
|
|
2380
|
+
lease?.close();
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
const quiescent = terminal && disposed && !signal.aborted;
|
|
2384
|
+
return { outcome: quiescent ? 'succeeded' : 'unknown', dispatched, quiescent,
|
|
2385
|
+
...(quiescent && goalOutput !== undefined ? { output: goalOutput } : {}) };
|
|
2386
|
+
}
|
|
2387
|
+
validGoalWakeInput(binding, input, now) {
|
|
2388
|
+
if (input === null || typeof input !== 'object' || input.signal === undefined
|
|
2389
|
+
|| typeof input.signal.aborted !== 'boolean' || typeof input.signal.addEventListener !== 'function'
|
|
2390
|
+
|| typeof input.signal.removeEventListener !== 'function' || input.signal.aborted
|
|
2391
|
+
|| !isDeliveryGoalWakeDeadline(input.deadlineAt, now) || typeof input.assertCurrent !== 'function'
|
|
2392
|
+
|| typeof input.beforeResume !== 'function' || typeof input.settle !== 'function'
|
|
2393
|
+
|| typeof input.resolveOutcomeFeedbackTarget !== 'function'
|
|
2394
|
+
|| (input.includeOutput !== undefined && typeof input.includeOutput !== 'boolean') || typeof input.native?.goalId !== 'string'
|
|
2395
|
+
|| input.native.goalId.length === 0 || input.native.goalId.length > 512
|
|
2396
|
+
|| !Number.isSafeInteger(input.native.revision) || input.native.revision < 1)
|
|
2397
|
+
return false;
|
|
2398
|
+
const attestation = input.attestation;
|
|
2399
|
+
return attestation !== null && typeof attestation === 'object'
|
|
2400
|
+
&& attestation.bindingId === binding.id && attestation.bindingVersion === binding.version
|
|
2401
|
+
&& attestation.bindingGeneration === binding.generation && attestation.sessionId === binding.sessionId
|
|
2402
|
+
&& attestation.scope?.workspace === binding.workspace && attestation.scope.preset === binding.agentPreset
|
|
2403
|
+
&& attestation.principalId === externalPrincipalId(binding.principal)
|
|
2404
|
+
&& typeof attestation.principalLineage?.principalRecordId === 'string'
|
|
2405
|
+
&& attestation.principalLineage.principalRecordId.length > 0
|
|
2406
|
+
&& Number.isSafeInteger(attestation.principalLineage.principalVersion)
|
|
2407
|
+
&& attestation.principalLineage.principalVersion >= 1;
|
|
2408
|
+
}
|
|
2409
|
+
assertScheduledGoal(input, agent, phase) {
|
|
2410
|
+
this.sessionLeases.assert(String(agent.session.id));
|
|
2411
|
+
input.signal.throwIfAborted();
|
|
2412
|
+
if (Date.now() >= input.deadlineAt)
|
|
2413
|
+
throw new Error('assistant-delivery: scheduled goal wake expired');
|
|
2414
|
+
input.assertCurrent(agent, phase);
|
|
2415
|
+
}
|
|
2416
|
+
installScheduledGoalFences(agent, input) {
|
|
2417
|
+
const goalTurn = () => {
|
|
2418
|
+
const events = agent.session.snapshotEvents();
|
|
2419
|
+
const start = events.findLast(event => event.type === 'turn/start');
|
|
2420
|
+
// DSH persists runtime context snapshots as user/message too; they are
|
|
2421
|
+
// context for this turn, never a second input or a grant of authority.
|
|
2422
|
+
const messages = start === undefined ? [] : events.filter(event => event.seq > start.seq && event.type === 'user/message'
|
|
2423
|
+
&& !isScheduledGoalRuntimeContext(event.data.source));
|
|
2424
|
+
if (start === undefined || events.some(event => event.seq > start.seq && event.type === 'turn/end')
|
|
2425
|
+
|| messages.length !== 1 || messages[0]?.type !== 'user/message')
|
|
2426
|
+
throw new Error('assistant-delivery: exact native goal turn required');
|
|
2427
|
+
const source = messages[0].data.source;
|
|
2428
|
+
const native = this.ctx.get('goals')?.get(agent);
|
|
2429
|
+
if (source.kind !== 'goal' || String(source.goalId) !== input.native.goalId
|
|
2430
|
+
|| source.revision !== input.native.revision + 1 || source.round < 1
|
|
2431
|
+
|| native === undefined || native.phase !== 'active' || String(native.id) !== input.native.goalId
|
|
2432
|
+
|| native.revision !== source.revision || native.roundsStarted !== source.round)
|
|
2433
|
+
throw new Error('assistant-delivery: foreign wake input denied');
|
|
2434
|
+
};
|
|
2435
|
+
const reject = () => {
|
|
2436
|
+
try {
|
|
2437
|
+
agent.cancel({ kind: 'hook', reason: 'assistant-delivery-scheduled-goal-wake-revoked' });
|
|
2438
|
+
}
|
|
2439
|
+
catch { }
|
|
2440
|
+
throw new Error('assistant-delivery: scheduled goal wake authorization changed');
|
|
2441
|
+
};
|
|
2442
|
+
const request = agent.ctx.on('agent/request', async (_payload, next) => {
|
|
2443
|
+
try {
|
|
2444
|
+
goalTurn();
|
|
2445
|
+
this.assertScheduledGoal(input, agent, 'running');
|
|
2446
|
+
}
|
|
2447
|
+
catch {
|
|
2448
|
+
return reject();
|
|
2449
|
+
}
|
|
2450
|
+
const result = await next();
|
|
2451
|
+
try {
|
|
2452
|
+
goalTurn();
|
|
2453
|
+
this.assertScheduledGoal(input, agent, 'running');
|
|
2454
|
+
}
|
|
2455
|
+
catch {
|
|
2456
|
+
return reject();
|
|
2457
|
+
}
|
|
2458
|
+
return result;
|
|
2459
|
+
}, { prepend: true });
|
|
2460
|
+
const preExecute = agent.ctx.on('tools/pre-execute', async (_payload, next) => {
|
|
2461
|
+
try {
|
|
2462
|
+
goalTurn();
|
|
2463
|
+
this.assertScheduledGoal(input, agent, 'running');
|
|
2464
|
+
}
|
|
2465
|
+
catch {
|
|
2466
|
+
return { kind: 'deny', reason: 'assistant-delivery: scheduled goal wake authorization changed' };
|
|
2467
|
+
}
|
|
2468
|
+
const result = await next();
|
|
2469
|
+
try {
|
|
2470
|
+
goalTurn();
|
|
2471
|
+
this.assertScheduledGoal(input, agent, 'running');
|
|
2472
|
+
}
|
|
2473
|
+
catch {
|
|
2474
|
+
return { kind: 'deny', reason: 'assistant-delivery: scheduled goal wake authorization changed' };
|
|
2475
|
+
}
|
|
2476
|
+
return result;
|
|
2477
|
+
}, { prepend: true });
|
|
2478
|
+
const guard = agent.ctx.tools.guard(() => {
|
|
2479
|
+
try {
|
|
2480
|
+
goalTurn();
|
|
2481
|
+
this.assertScheduledGoal(input, agent, 'running');
|
|
2482
|
+
return undefined;
|
|
2483
|
+
}
|
|
2484
|
+
catch {
|
|
2485
|
+
return 'assistant-delivery: scheduled goal wake authorization changed';
|
|
2486
|
+
}
|
|
2487
|
+
});
|
|
2488
|
+
return () => { request(); preExecute(); guard(); };
|
|
2489
|
+
}
|
|
2490
|
+
waitForScheduledGoalTerminal(agent, input, signal) {
|
|
2491
|
+
return new Promise(resolve => {
|
|
2492
|
+
let settled = false;
|
|
2493
|
+
let verifying = false;
|
|
2494
|
+
const finish = (value) => {
|
|
2495
|
+
if (settled)
|
|
2496
|
+
return;
|
|
2497
|
+
settled = true;
|
|
2498
|
+
removeGoal();
|
|
2499
|
+
removeStatus();
|
|
2500
|
+
signal.removeEventListener('abort', aborted);
|
|
2501
|
+
resolve(value);
|
|
2502
|
+
};
|
|
2503
|
+
const check = () => {
|
|
2504
|
+
if (settled || verifying)
|
|
2505
|
+
return;
|
|
2506
|
+
try {
|
|
2507
|
+
this.sessionLeases.assert(String(agent.session.id));
|
|
2508
|
+
input.signal.throwIfAborted();
|
|
2509
|
+
if (Date.now() >= input.deadlineAt)
|
|
2510
|
+
throw new Error('assistant-delivery: scheduled goal wake expired');
|
|
2511
|
+
const goals = this.ctx.get('goals');
|
|
2512
|
+
const current = goals?.get(agent);
|
|
2513
|
+
if (current === undefined || String(current.id) !== input.native.goalId)
|
|
2514
|
+
return finish(false);
|
|
2515
|
+
if (current.phase === 'active') {
|
|
2516
|
+
input.assertCurrent(agent, 'running');
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
if (agent.status !== 'idle' || agent.inbox.hasPending)
|
|
2520
|
+
return;
|
|
2521
|
+
input.assertCurrent(agent, 'terminal');
|
|
2522
|
+
// Native idle/round-limit can precede the asynchronous step and
|
|
2523
|
+
// whole-goal verifier. Keep the Agent and its lease alive until that
|
|
2524
|
+
// exact terminal round settles; cancellation still ends this wait.
|
|
2525
|
+
verifying = true;
|
|
2526
|
+
void Promise.resolve().then(() => input.settle(agent, signal)).then(() => {
|
|
2527
|
+
if (settled)
|
|
2528
|
+
return;
|
|
2529
|
+
signal.throwIfAborted();
|
|
2530
|
+
this.assertScheduledGoal(input, agent, 'terminal');
|
|
2531
|
+
if (agent.status !== 'idle' || agent.inbox.hasPending)
|
|
2532
|
+
throw new Error('assistant-delivery: goal changed during terminal settlement');
|
|
2533
|
+
finish(true);
|
|
2534
|
+
}).catch(() => {
|
|
2535
|
+
if (settled)
|
|
2536
|
+
return;
|
|
2537
|
+
try {
|
|
2538
|
+
agent.cancel({ kind: 'hook', reason: 'assistant-delivery-scheduled-goal-settlement-failed' });
|
|
2539
|
+
}
|
|
2540
|
+
catch { }
|
|
2541
|
+
finish(false);
|
|
2542
|
+
});
|
|
2543
|
+
}
|
|
2544
|
+
catch {
|
|
2545
|
+
try {
|
|
2546
|
+
agent.cancel({ kind: 'hook', reason: 'assistant-delivery-scheduled-goal-wake-revoked' });
|
|
2547
|
+
}
|
|
2548
|
+
catch { }
|
|
2549
|
+
finish(false);
|
|
2550
|
+
}
|
|
2551
|
+
};
|
|
2552
|
+
const removeGoal = this.ctx.on('goal/changed', ({ agent: changed }) => { if (changed === agent)
|
|
2553
|
+
check(); });
|
|
2554
|
+
const removeStatus = this.ctx.on('agent/status', ({ agent: changed }) => { if (changed === agent)
|
|
2555
|
+
check(); });
|
|
2556
|
+
const aborted = () => finish(false);
|
|
2557
|
+
signal.addEventListener('abort', aborted, { once: true });
|
|
2558
|
+
if (signal.aborted)
|
|
2559
|
+
aborted();
|
|
2560
|
+
else
|
|
2561
|
+
check();
|
|
2562
|
+
});
|
|
1858
2563
|
}
|
|
1859
2564
|
async prepare(binding, envelope, signal, context) {
|
|
1860
2565
|
const imageCount = (envelope.attachments ?? [])
|
|
@@ -1954,9 +2659,13 @@ export class DshDeliveryRuntime {
|
|
|
1954
2659
|
signal.throwIfAborted();
|
|
1955
2660
|
markDispatching();
|
|
1956
2661
|
signal.throwIfAborted();
|
|
1957
|
-
if (parsed.kind === 'objective') {
|
|
1958
|
-
const objective = await this.options.dispatchObjectiveFeedback(binding, envelope, parsed.objectiveStatus);
|
|
2662
|
+
if (parsed.kind === 'objective' || parsed.kind === 'objective-revision' || parsed.kind === 'objective-status') {
|
|
2663
|
+
const objective = await this.options.dispatchObjectiveFeedback(binding, envelope, parsed.kind === 'objective' ? parsed.objectiveStatus : parsed);
|
|
1959
2664
|
signal.throwIfAborted();
|
|
2665
|
+
if (typeof objective === 'object') {
|
|
2666
|
+
this.options.replyCommand(binding, envelope.eventId, { text: (parsed.kind === 'objective-status' ? '' : `已把该次任务结果记录为 ${objective.objectiveStatus};同一次任务只计一票。\n`) + objectiveStateMessage(objective), format: 'plain' });
|
|
2667
|
+
return { outcome: 'processed' };
|
|
2668
|
+
}
|
|
1960
2669
|
if (objective === 'unknown') {
|
|
1961
2670
|
return {
|
|
1962
2671
|
outcome: 'not-processed',
|
|
@@ -1980,13 +2689,13 @@ export class DshDeliveryRuntime {
|
|
|
1980
2689
|
}
|
|
1981
2690
|
if (objective === 'conflict') {
|
|
1982
2691
|
this.options.replyCommand(binding, envelope.eventId, {
|
|
1983
|
-
text: '
|
|
2692
|
+
text: '该次任务已经记录了不同的任务结果或反馈版本已变化,本次未覆盖。请回复原任务结果发送 /feedback status,使用当前版本更正或撤回。',
|
|
1984
2693
|
format: 'plain',
|
|
1985
2694
|
});
|
|
1986
2695
|
return { outcome: 'processed' };
|
|
1987
2696
|
}
|
|
1988
2697
|
this.options.replyCommand(binding, envelope.eventId, {
|
|
1989
|
-
text: `已把该次任务结果记录为 ${parsed.objectiveStatus}
|
|
2698
|
+
text: parsed.kind === 'objective-status' ? '尚未记录任务反馈,请回复原任务结果发送 /feedback achieved、partial 或 not-achieved。' : `已把该次任务结果记录为 ${parsed.objectiveStatus};同一次任务只计一票。回复原结果发送 /feedback status 可更正或撤回。`,
|
|
1990
2699
|
format: 'plain',
|
|
1991
2700
|
});
|
|
1992
2701
|
return { outcome: 'processed' };
|
|
@@ -2271,7 +2980,7 @@ export class DshDeliveryRuntime {
|
|
|
2271
2980
|
? binding.agentPreset
|
|
2272
2981
|
: (await agentPresets.resolve(binding.agentPreset)).id;
|
|
2273
2982
|
await requireWorkspace(binding.workspace);
|
|
2274
|
-
handle = await
|
|
2983
|
+
handle = await this.resumeAgent({
|
|
2275
2984
|
resumeSessionId: SessionId(binding.sessionId),
|
|
2276
2985
|
signal,
|
|
2277
2986
|
agentOptions: {
|
|
@@ -2299,7 +3008,7 @@ export class DshDeliveryRuntime {
|
|
|
2299
3008
|
}
|
|
2300
3009
|
if (command.name === 'status' || command.name === 'session') {
|
|
2301
3010
|
const messages = agent.session.deriveMessages().length;
|
|
2302
|
-
const turns = agent.session.
|
|
3011
|
+
const turns = agent.session.snapshotEvents().filter(event => event.type === 'turn/end').length;
|
|
2303
3012
|
return this.replySessionCommand(binding, envelope, {
|
|
2304
3013
|
text: [
|
|
2305
3014
|
'当前会话',
|
|
@@ -2433,12 +3142,41 @@ export class DshDeliveryRuntime {
|
|
|
2433
3142
|
format: 'plain',
|
|
2434
3143
|
}, outerSignal, markDispatching);
|
|
2435
3144
|
}
|
|
2436
|
-
|
|
3145
|
+
let lease;
|
|
3146
|
+
try {
|
|
3147
|
+
lease = this.sessionLeases.open({ kind: 'bound', binding }, outerSignal);
|
|
3148
|
+
}
|
|
3149
|
+
catch (error) {
|
|
3150
|
+
if (error instanceof SessionLeaseUnavailable) {
|
|
3151
|
+
if (error.kind === 'unknown' && !outerSignal.aborted) {
|
|
3152
|
+
try {
|
|
3153
|
+
if (this.options.isInboundAuthorized(binding, envelope))
|
|
3154
|
+
this.options.replyCommand(binding, envelope.eventId, {
|
|
3155
|
+
text: '上一次执行是否已经停止还无法确认,因此暂时无法继续这个会话。请先核对外部操作;需要独立开始新任务可发送 /new,旧会话会保留。',
|
|
3156
|
+
format: 'plain',
|
|
3157
|
+
});
|
|
3158
|
+
}
|
|
3159
|
+
catch { /* Keep the exact unknown execution diagnostic even if its notice is unavailable. */ }
|
|
3160
|
+
}
|
|
3161
|
+
return {
|
|
3162
|
+
outcome: 'not-processed', failureCode: `session-lease-${error.kind}`,
|
|
3163
|
+
retryable: error.kind === 'busy', retryAfterMs: this.sessionLeases.leaseMs,
|
|
3164
|
+
};
|
|
3165
|
+
}
|
|
3166
|
+
throw error;
|
|
3167
|
+
}
|
|
3168
|
+
let control;
|
|
2437
3169
|
try {
|
|
2438
|
-
|
|
3170
|
+
control = this.beginSessionControl(binding.sessionId, lease.signal);
|
|
3171
|
+
lease.dispatch();
|
|
3172
|
+
return await this.processControlled(binding, envelope, control.controller.signal, sessionCommand, prepared, markDispatching === undefined ? undefined : () => {
|
|
3173
|
+
lease.assert();
|
|
3174
|
+
markDispatching();
|
|
3175
|
+
lease.assert();
|
|
3176
|
+
});
|
|
2439
3177
|
}
|
|
2440
3178
|
catch (error) {
|
|
2441
|
-
if (control
|
|
3179
|
+
if (control?.command !== undefined && causedByUserCancellation(error)) {
|
|
2442
3180
|
return permissionCommand(sessionCommand) === undefined
|
|
2443
3181
|
? { outcome: 'processed' }
|
|
2444
3182
|
: {
|
|
@@ -2450,7 +3188,9 @@ export class DshDeliveryRuntime {
|
|
|
2450
3188
|
throw error;
|
|
2451
3189
|
}
|
|
2452
3190
|
finally {
|
|
2453
|
-
|
|
3191
|
+
if (control !== undefined)
|
|
3192
|
+
this.endSessionControl(binding.sessionId, control);
|
|
3193
|
+
lease.close();
|
|
2454
3194
|
}
|
|
2455
3195
|
}
|
|
2456
3196
|
async processControlled(binding, envelope, signal, sessionCommand, prepared, markDispatching) {
|
|
@@ -2560,6 +3300,10 @@ export class DshDeliveryRuntime {
|
|
|
2560
3300
|
const selected = agentSelection(selectedRoute);
|
|
2561
3301
|
let handle;
|
|
2562
3302
|
let dispatched = false;
|
|
3303
|
+
let acceptance;
|
|
3304
|
+
let acceptanceSucceeded = false;
|
|
3305
|
+
let goalContinuation;
|
|
3306
|
+
let goalContinuationQuiescent = true;
|
|
2563
3307
|
let removeAbort;
|
|
2564
3308
|
let removeProgress;
|
|
2565
3309
|
let progressOpen = true;
|
|
@@ -2582,7 +3326,7 @@ export class DshDeliveryRuntime {
|
|
|
2582
3326
|
? binding.agentPreset
|
|
2583
3327
|
: (await agentPresets.resolve(binding.agentPreset)).id;
|
|
2584
3328
|
await requireWorkspace(binding.workspace);
|
|
2585
|
-
handle = await
|
|
3329
|
+
handle = await this.resumeAgent({ resumeSessionId: SessionId(binding.sessionId), signal,
|
|
2586
3330
|
agentOptions: { provider: selected.provider, model: selected.model,
|
|
2587
3331
|
maxTokens: this.options.maxOutputTokens },
|
|
2588
3332
|
setup: async (agentCtx) => {
|
|
@@ -2607,7 +3351,7 @@ export class DshDeliveryRuntime {
|
|
|
2607
3351
|
if (authorizationFailure !== undefined)
|
|
2608
3352
|
return authorizationFailure;
|
|
2609
3353
|
}
|
|
2610
|
-
let turnFrom = agent.session.
|
|
3354
|
+
let turnFrom = agent.session.snapshotEvents().length;
|
|
2611
3355
|
const automaticCompletionTurns = new Map();
|
|
2612
3356
|
const automaticCompletionToolViolations = new Set();
|
|
2613
3357
|
removeProgress = this.ctx.on('session/event', (session, event) => {
|
|
@@ -2649,6 +3393,11 @@ export class DshDeliveryRuntime {
|
|
|
2649
3393
|
const authorizationFailure = preDispatchAuthorizationFailure();
|
|
2650
3394
|
if (authorizationFailure !== undefined)
|
|
2651
3395
|
return authorizationFailure;
|
|
3396
|
+
// Mount the native-goal fences before the foreground message is queued:
|
|
3397
|
+
// an owner tool can create/resume a goal in that very first turn and the
|
|
3398
|
+
// host driver is otherwise free to claim its first round at idle.
|
|
3399
|
+
goalContinuation = this.waitForNativeGoalContinuation(binding, envelope, agent, signal);
|
|
3400
|
+
acceptance = this.options.prepareForegroundTaskAcceptance(binding, envelope);
|
|
2652
3401
|
markDispatching();
|
|
2653
3402
|
// Once the durable marker exists, even a synchronous followup failure is ambiguous:
|
|
2654
3403
|
// implementations may enqueue before throwing, so no retry is safe.
|
|
@@ -2663,7 +3412,7 @@ export class DshDeliveryRuntime {
|
|
|
2663
3412
|
let assembledText = '';
|
|
2664
3413
|
let bestEffortText = '';
|
|
2665
3414
|
while (true) {
|
|
2666
|
-
const output = finalAssistant(agent.session.
|
|
3415
|
+
const output = finalAssistant(agent.session.snapshotEvents(), turnFrom, deliveryTurn);
|
|
2667
3416
|
let addedText = false;
|
|
2668
3417
|
if (output.text.trim() !== '') {
|
|
2669
3418
|
if (completionKind === 'compact' || completionKind === 'regenerate') {
|
|
@@ -2714,7 +3463,7 @@ export class DshDeliveryRuntime {
|
|
|
2714
3463
|
? 'compact'
|
|
2715
3464
|
: 'continue';
|
|
2716
3465
|
automaticTurns += 1;
|
|
2717
|
-
turnFrom = agent.session.
|
|
3466
|
+
turnFrom = agent.session.snapshotEvents().length;
|
|
2718
3467
|
const completionMessage = createUserMessage({
|
|
2719
3468
|
content: [{
|
|
2720
3469
|
type: 'text',
|
|
@@ -2828,13 +3577,20 @@ export class DshDeliveryRuntime {
|
|
|
2828
3577
|
break;
|
|
2829
3578
|
}
|
|
2830
3579
|
signal.throwIfAborted();
|
|
2831
|
-
//
|
|
2832
|
-
//
|
|
3580
|
+
// The foreground reply has already crossed its durable reply path.
|
|
3581
|
+
// Retain the same Agent afterwards only for the native goal driver; its
|
|
3582
|
+
// unknown result must never turn the original reply into a claim that
|
|
3583
|
+
// the business goal was achieved.
|
|
2833
3584
|
const learning = await this.options.replyCompletedPreferenceTurn(agent, binding, envelope, { text: assembledText, format: 'markdown' });
|
|
2834
3585
|
if (learning === 'unknown') {
|
|
2835
3586
|
this.ctx.logger.warn('assistant-delivery: completed-turn preference projection is ambiguous');
|
|
2836
3587
|
}
|
|
3588
|
+
if (goalContinuation !== undefined) {
|
|
3589
|
+
goalContinuationQuiescent = await goalContinuation.settle();
|
|
3590
|
+
}
|
|
3591
|
+
signal.throwIfAborted();
|
|
2837
3592
|
publishProgress({ kind: 'completed' });
|
|
3593
|
+
acceptanceSucceeded = goalContinuationQuiescent;
|
|
2838
3594
|
return { outcome: 'processed' };
|
|
2839
3595
|
}
|
|
2840
3596
|
catch (error) {
|
|
@@ -2878,7 +3634,24 @@ export class DshDeliveryRuntime {
|
|
|
2878
3634
|
removeAbort?.();
|
|
2879
3635
|
removeProgress?.();
|
|
2880
3636
|
progressOpen = false;
|
|
2881
|
-
|
|
3637
|
+
let disposed = false;
|
|
3638
|
+
try {
|
|
3639
|
+
disposed = await this.disposeAfterReplyBoundary(binding.sessionId, handle, () => goalContinuation?.dispose(), !goalContinuationQuiescent, goalContinuation?.teardownTimeoutMs());
|
|
3640
|
+
}
|
|
3641
|
+
finally {
|
|
3642
|
+
acceptanceSucceeded = acceptanceSucceeded && disposed && !signal.aborted
|
|
3643
|
+
&& (goalContinuation?.isQuiescent() ?? true);
|
|
3644
|
+
if (acceptance !== undefined) {
|
|
3645
|
+
try {
|
|
3646
|
+
await this.options.completeForegroundTaskAcceptance(acceptance, {
|
|
3647
|
+
status: acceptanceSucceeded ? 'succeeded' : 'unknown', quiescent: acceptanceSucceeded,
|
|
3648
|
+
});
|
|
3649
|
+
}
|
|
3650
|
+
catch (error) {
|
|
3651
|
+
this.ctx.logger.warn(`assistant-delivery: acceptance completion failed: ${String(error)}`);
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
2882
3655
|
}
|
|
2883
3656
|
}
|
|
2884
3657
|
}
|