@dsh-enhanced/assistant-delivery 0.1.19 → 0.1.21
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 +8 -1
- package/cordis.patch.yml +1 -0
- package/lib/agent-runtime.d.ts +9 -0
- package/lib/agent-runtime.d.ts.map +1 -1
- package/lib/agent-runtime.js +437 -50
- package/lib/agent-runtime.js.map +1 -1
- package/lib/service.d.ts +1 -0
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +2 -0
- package/lib/service.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +7 -7
package/lib/agent-runtime.js
CHANGED
|
@@ -37,6 +37,15 @@ const PROGRESS_ASSIGNMENT_START = new RegExp(`(^|[^A-Za-z0-9_])(["']?)${PROGRESS
|
|
|
37
37
|
const ESCAPED_QUOTED_PROGRESS_ASSIGNMENT_START = new RegExp(`(^|[^A-Za-z0-9_])\\\\+(["'])${PROGRESS_ASSIGNMENT_KEY}\\\\+\\2(\\s*[:=]\\s*)`, 'giu');
|
|
38
38
|
class DurableAgentIdentityError extends Error {
|
|
39
39
|
}
|
|
40
|
+
class AutomaticCompletionUnavailableError extends Error {
|
|
41
|
+
}
|
|
42
|
+
class AutomaticCompletionAuthorizationError extends Error {
|
|
43
|
+
state;
|
|
44
|
+
constructor(state) {
|
|
45
|
+
super(`assistant-delivery: automatic completion authorization is ${state}`);
|
|
46
|
+
this.state = state;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
40
49
|
class ApprovalReviewerReaderUnavailableError extends Error {
|
|
41
50
|
constructor(cause) {
|
|
42
51
|
super('assistant-delivery: approval reviewer persistence reader is not proven', { cause });
|
|
@@ -856,6 +865,7 @@ function sessionFingerprint(value) {
|
|
|
856
865
|
}
|
|
857
866
|
const AGENT_OUTPUT_TRUNCATED_CODE = 'agent-output-truncated';
|
|
858
867
|
const AGENT_EMPTY_RESPONSE_CODE = 'agent-empty-response';
|
|
868
|
+
const AUTOMATIC_COMPLETION_TOOL_DENIAL = 'assistant-delivery: automatic answer completion disables tool execution';
|
|
859
869
|
function utf8Prefix(value, maxBytes) {
|
|
860
870
|
const bytes = Buffer.from(value, 'utf8');
|
|
861
871
|
if (bytes.length <= maxBytes)
|
|
@@ -870,16 +880,16 @@ function fittingNotice(candidates, maxBytes) {
|
|
|
870
880
|
}
|
|
871
881
|
function truncatedAgentReply(text, maxBytes) {
|
|
872
882
|
const notice = fittingNotice([
|
|
873
|
-
'⚠️
|
|
874
|
-
'
|
|
875
|
-
'
|
|
883
|
+
'⚠️ 本次回答未能完整生成;以下内容可能不完整。',
|
|
884
|
+
'本次回答未完整生成。',
|
|
885
|
+
'回答不完整。',
|
|
876
886
|
'retry',
|
|
877
887
|
'!',
|
|
878
888
|
], maxBytes);
|
|
879
889
|
if (text.trim() === '')
|
|
880
890
|
return fittingNotice([
|
|
881
|
-
'
|
|
882
|
-
'
|
|
891
|
+
'本次未能生成可发送的完整回答。请稍后重试或缩小问题范围。',
|
|
892
|
+
'本次未能生成完整回答,请重试。',
|
|
883
893
|
'未生成回答,请重试。',
|
|
884
894
|
'retry',
|
|
885
895
|
'!',
|
|
@@ -901,18 +911,88 @@ function emptyAgentReply(maxBytes) {
|
|
|
901
911
|
'!',
|
|
902
912
|
], maxBytes);
|
|
903
913
|
}
|
|
914
|
+
function mergeContinuationText(current, continuation) {
|
|
915
|
+
if (current === '')
|
|
916
|
+
return { text: continuation, added: continuation !== '' };
|
|
917
|
+
if (continuation === '')
|
|
918
|
+
return { text: current, added: false };
|
|
919
|
+
if (continuation.startsWith(current)) {
|
|
920
|
+
const suffix = continuation.slice(current.length);
|
|
921
|
+
const added = suffix.trim() !== '';
|
|
922
|
+
return { text: added ? continuation : current, added };
|
|
923
|
+
}
|
|
924
|
+
if (current.endsWith(continuation))
|
|
925
|
+
return { text: current, added: false };
|
|
926
|
+
const currentPoints = [...current];
|
|
927
|
+
const continuationPoints = [...continuation];
|
|
928
|
+
const maxOverlap = Math.min(currentPoints.length, continuationPoints.length, 4_096);
|
|
929
|
+
for (let length = maxOverlap; length >= 2; length -= 1) {
|
|
930
|
+
const currentOverlap = currentPoints.slice(-length);
|
|
931
|
+
if (length < 16) {
|
|
932
|
+
const hanCount = currentOverlap.filter(point => /^\p{Script=Han}$/u.test(point)).length;
|
|
933
|
+
if (hanCount < 2 || currentOverlap.some(point => !/^[\p{Script=Han}\p{P}\p{Z}]$/u.test(point)))
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
if (currentOverlap.join('') !== continuationPoints.slice(0, length).join(''))
|
|
937
|
+
continue;
|
|
938
|
+
const suffix = continuationPoints.slice(length).join('');
|
|
939
|
+
const added = suffix.trim() !== '';
|
|
940
|
+
return { text: added ? current + suffix : current, added };
|
|
941
|
+
}
|
|
942
|
+
return { text: current + continuation, added: continuation.trim() !== '' };
|
|
943
|
+
}
|
|
944
|
+
function automaticCompletionPrompt(kind, maxTextBytes, maxOutputTokens) {
|
|
945
|
+
const marker = '[DSH automatic answer completion notice]';
|
|
946
|
+
const scope = 'Work only on the current task: the most recent user request before the first marked notice '
|
|
947
|
+
+ 'in this trailing recovery sequence, plus assistant answer segments after that request. '
|
|
948
|
+
+ 'Ignore all earlier completed tasks, and treat every marked notice as an instruction rather than answer content.';
|
|
949
|
+
const common = 'Do not call tools, repeat actions, discuss this recovery instruction, or add a preamble.';
|
|
950
|
+
if (kind === 'continue') {
|
|
951
|
+
return `${marker} ${scope} The immediately preceding assistant answer segment was cut off by an output limit. `
|
|
952
|
+
+ `Continue exactly from its last character and finish the current answer. Return only the missing remainder `
|
|
953
|
+
+ `without repeating earlier text. ${common}`;
|
|
954
|
+
}
|
|
955
|
+
if (kind === 'compact') {
|
|
956
|
+
const approximateCjkCharacters = Math.max(1, Math.floor(maxTextBytes / 4));
|
|
957
|
+
const targetTokens = Math.max(1, Math.min(Math.floor(maxOutputTokens / 2), approximateCjkCharacters));
|
|
958
|
+
return `${marker} ${scope} Rewrite the current task result as one complete, self-contained, concise final answer `
|
|
959
|
+
+ `using at most ${targetTokens} tokens `
|
|
960
|
+
+ `and under ${maxTextBytes} UTF-8 bytes (about ${approximateCjkCharacters} CJK characters). `
|
|
961
|
+
+ 'Preserve the conclusion and essential facts, '
|
|
962
|
+
+ `but remove repetition and nonessential detail. ${common}`;
|
|
963
|
+
}
|
|
964
|
+
return `${marker} ${scope} The preceding attempt produced no sendable answer. Answer the current request now `
|
|
965
|
+
+ `with one complete, self-contained and concise final response. ${common}`;
|
|
966
|
+
}
|
|
904
967
|
function finalAssistant(events, from, expectedTurn) {
|
|
905
|
-
if (expectedTurn === undefined)
|
|
906
|
-
return { text: '', completed: false, truncated: false, stopped: false };
|
|
968
|
+
if (expectedTurn === undefined) {
|
|
969
|
+
return { text: '', completed: false, truncated: false, stopped: false, hasUnpairedToolCall: false, hadToolActivity: false };
|
|
970
|
+
}
|
|
907
971
|
let text = '';
|
|
908
972
|
let completed = false;
|
|
909
973
|
let truncated = false;
|
|
910
974
|
let stopped = false;
|
|
975
|
+
let hadToolActivity = false;
|
|
976
|
+
const unpairedToolCalls = new Set();
|
|
911
977
|
let failureCode;
|
|
912
978
|
for (const event of events.slice(from)) {
|
|
913
979
|
if (event.type === 'assistant/message' && event.data.turn === expectedTurn) {
|
|
914
980
|
text = event.data.message.content.filter(block => block.type === 'text')
|
|
915
981
|
.map(block => block.type === 'text' ? block.text : '').join('');
|
|
982
|
+
for (const block of event.data.message.content) {
|
|
983
|
+
if (block.type === 'tool-call') {
|
|
984
|
+
unpairedToolCalls.add(String(block.id));
|
|
985
|
+
hadToolActivity = true;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
if (event.type === 'tool/result' && event.data.turn === expectedTurn) {
|
|
990
|
+
const block = event.data.message.content.find(value => value.type === 'tool-result');
|
|
991
|
+
const callId = block?.type === 'tool-result'
|
|
992
|
+
? block.toolCallId
|
|
993
|
+
: event.data.message.source?.callId;
|
|
994
|
+
if (callId !== undefined)
|
|
995
|
+
unpairedToolCalls.delete(String(callId));
|
|
916
996
|
}
|
|
917
997
|
if (event.type === 'turn/end' && event.data.turn === expectedTurn) {
|
|
918
998
|
const reason = event.data.reason;
|
|
@@ -931,7 +1011,15 @@ function finalAssistant(events, from, expectedTurn) {
|
|
|
931
1011
|
failureCode = completed || truncated || typeof error?.code !== 'string' ? undefined : error.code;
|
|
932
1012
|
}
|
|
933
1013
|
}
|
|
934
|
-
return {
|
|
1014
|
+
return {
|
|
1015
|
+
text,
|
|
1016
|
+
completed,
|
|
1017
|
+
truncated,
|
|
1018
|
+
stopped,
|
|
1019
|
+
hasUnpairedToolCall: unpairedToolCalls.size > 0,
|
|
1020
|
+
hadToolActivity,
|
|
1021
|
+
...(failureCode === undefined ? {} : { failureCode }),
|
|
1022
|
+
};
|
|
935
1023
|
}
|
|
936
1024
|
export class DshDeliveryRuntime {
|
|
937
1025
|
ctx;
|
|
@@ -1023,7 +1111,7 @@ export class DshDeliveryRuntime {
|
|
|
1023
1111
|
}
|
|
1024
1112
|
}
|
|
1025
1113
|
/** Wait for only the turn that claims Delivery's exact message identity. */
|
|
1026
|
-
followupTurn(agent, message) {
|
|
1114
|
+
followupTurn(agent, message, onClaimed, onSettled) {
|
|
1027
1115
|
return new Promise((resolve, reject) => {
|
|
1028
1116
|
let claimedTurn;
|
|
1029
1117
|
let settled = false;
|
|
@@ -1047,15 +1135,32 @@ export class DshDeliveryRuntime {
|
|
|
1047
1135
|
reject(error);
|
|
1048
1136
|
};
|
|
1049
1137
|
const removeClaimed = this.ctx.on('agent/inbox/claimed', event => {
|
|
1050
|
-
if (event.agent === agent && event.message.id === message.id)
|
|
1138
|
+
if (event.agent === agent && event.message.id === message.id) {
|
|
1051
1139
|
claimedTurn = event.turn;
|
|
1140
|
+
onClaimed?.(event.turn);
|
|
1141
|
+
}
|
|
1052
1142
|
});
|
|
1053
1143
|
const removeDiscarded = this.ctx.on('agent/inbox/discarded', event => {
|
|
1054
|
-
if (event.agent
|
|
1055
|
-
|
|
1144
|
+
if (event.agent !== agent || event.message.id !== message.id)
|
|
1145
|
+
return;
|
|
1146
|
+
try {
|
|
1147
|
+
onSettled?.();
|
|
1148
|
+
}
|
|
1149
|
+
catch (error) {
|
|
1150
|
+
fail(error);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
settle(undefined);
|
|
1056
1154
|
});
|
|
1057
1155
|
const removeSessionEvent = this.ctx.on('session/event', (session, event) => {
|
|
1058
1156
|
if (session === agent.session && event.type === 'turn/end' && event.data.turn === claimedTurn) {
|
|
1157
|
+
try {
|
|
1158
|
+
onSettled?.();
|
|
1159
|
+
}
|
|
1160
|
+
catch (error) {
|
|
1161
|
+
fail(error);
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1059
1164
|
settle(claimedTurn);
|
|
1060
1165
|
}
|
|
1061
1166
|
});
|
|
@@ -1067,6 +1172,129 @@ export class DshDeliveryRuntime {
|
|
|
1067
1172
|
}
|
|
1068
1173
|
});
|
|
1069
1174
|
}
|
|
1175
|
+
/** Wait only for scheduling quiescence; exact turn ownership still comes from followupTurn(). */
|
|
1176
|
+
async waitForAgentIdle(agent, signal) {
|
|
1177
|
+
signal.throwIfAborted();
|
|
1178
|
+
while (true) {
|
|
1179
|
+
await new Promise((resolve, reject) => {
|
|
1180
|
+
let settled = false;
|
|
1181
|
+
const cleanup = () => signal.removeEventListener('abort', aborted);
|
|
1182
|
+
const finish = () => {
|
|
1183
|
+
if (settled)
|
|
1184
|
+
return;
|
|
1185
|
+
settled = true;
|
|
1186
|
+
cleanup();
|
|
1187
|
+
resolve();
|
|
1188
|
+
};
|
|
1189
|
+
const fail = (error) => {
|
|
1190
|
+
if (settled)
|
|
1191
|
+
return;
|
|
1192
|
+
settled = true;
|
|
1193
|
+
cleanup();
|
|
1194
|
+
reject(error);
|
|
1195
|
+
};
|
|
1196
|
+
const aborted = () => {
|
|
1197
|
+
try {
|
|
1198
|
+
signal.throwIfAborted();
|
|
1199
|
+
}
|
|
1200
|
+
catch (error) {
|
|
1201
|
+
fail(error);
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
signal.addEventListener('abort', aborted, { once: true });
|
|
1205
|
+
void agent.whenIdle().then(finish, fail);
|
|
1206
|
+
if (signal.aborted)
|
|
1207
|
+
aborted();
|
|
1208
|
+
});
|
|
1209
|
+
signal.throwIfAborted();
|
|
1210
|
+
if (agent.status === 'idle')
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Queue a synthetic completion turn only after the previous exact turn retired. rc.8 may still
|
|
1216
|
+
* serialize scoped tool schemas, but every call is denied before approval/execution while this
|
|
1217
|
+
* turn owns the Agent, so recovering prose cannot repeat an external side effect.
|
|
1218
|
+
*/
|
|
1219
|
+
async automaticCompletionTurn(agent, message, signal, expectedPreviousTurn, authorize, onClaimed) {
|
|
1220
|
+
await this.waitForAgentIdle(agent, signal);
|
|
1221
|
+
signal.throwIfAborted();
|
|
1222
|
+
let liftPreExecute;
|
|
1223
|
+
let liftRestriction;
|
|
1224
|
+
let liftGuard;
|
|
1225
|
+
let submitted = false;
|
|
1226
|
+
let exactTurnSettled = false;
|
|
1227
|
+
const releaseToolBarriers = () => {
|
|
1228
|
+
let firstError;
|
|
1229
|
+
if (liftGuard !== undefined) {
|
|
1230
|
+
try {
|
|
1231
|
+
liftGuard();
|
|
1232
|
+
liftGuard = undefined;
|
|
1233
|
+
}
|
|
1234
|
+
catch (error) {
|
|
1235
|
+
firstError ??= error;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
if (liftRestriction !== undefined) {
|
|
1239
|
+
try {
|
|
1240
|
+
liftRestriction();
|
|
1241
|
+
liftRestriction = undefined;
|
|
1242
|
+
}
|
|
1243
|
+
catch (error) {
|
|
1244
|
+
firstError ??= error;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
if (liftPreExecute !== undefined) {
|
|
1248
|
+
try {
|
|
1249
|
+
liftPreExecute();
|
|
1250
|
+
liftPreExecute = undefined;
|
|
1251
|
+
}
|
|
1252
|
+
catch (error) {
|
|
1253
|
+
firstError ??= error;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
if (firstError !== undefined)
|
|
1257
|
+
throw firstError;
|
|
1258
|
+
};
|
|
1259
|
+
try {
|
|
1260
|
+
liftPreExecute = agent.ctx.on('tools/pre-execute', () => Promise.resolve({
|
|
1261
|
+
kind: 'deny',
|
|
1262
|
+
reason: AUTOMATIC_COMPLETION_TOOL_DENIAL,
|
|
1263
|
+
}), { prepend: true });
|
|
1264
|
+
liftRestriction = agent.ctx.tools.restrict({ allow: [] });
|
|
1265
|
+
liftGuard = agent.ctx.tools.guard(() => AUTOMATIC_COMPLETION_TOOL_DENIAL);
|
|
1266
|
+
signal.throwIfAborted();
|
|
1267
|
+
if (agent.status !== 'idle' || agent.inbox.hasPending) {
|
|
1268
|
+
throw new AutomaticCompletionUnavailableError('assistant-delivery: automatic completion lost the idle scheduling boundary');
|
|
1269
|
+
}
|
|
1270
|
+
const latestBoundary = agent.session.events.findLast(event => event.type === 'turn/start' || event.type === 'turn/end');
|
|
1271
|
+
if (latestBoundary?.type !== 'turn/end' || latestBoundary.data.turn !== expectedPreviousTurn) {
|
|
1272
|
+
throw new AutomaticCompletionUnavailableError('assistant-delivery: another turn crossed the automatic completion boundary');
|
|
1273
|
+
}
|
|
1274
|
+
const authorization = authorize();
|
|
1275
|
+
if (authorization !== 'authorized')
|
|
1276
|
+
throw new AutomaticCompletionAuthorizationError(authorization);
|
|
1277
|
+
submitted = true;
|
|
1278
|
+
return await this.followupTurn(agent, message, onClaimed, () => {
|
|
1279
|
+
exactTurnSettled = true;
|
|
1280
|
+
releaseToolBarriers();
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
catch (error) {
|
|
1284
|
+
if (signal.aborted)
|
|
1285
|
+
signal.throwIfAborted();
|
|
1286
|
+
if (submitted || error instanceof AutomaticCompletionAuthorizationError
|
|
1287
|
+
|| error instanceof AutomaticCompletionUnavailableError)
|
|
1288
|
+
throw error;
|
|
1289
|
+
throw new AutomaticCompletionUnavailableError('assistant-delivery: automatic completion safety guard was unavailable', { cause: error });
|
|
1290
|
+
}
|
|
1291
|
+
finally {
|
|
1292
|
+
// A submitted message may already be pending or running even when followup() throws.
|
|
1293
|
+
// Keep the barriers attached to the Agent scope until exact settlement or handle teardown.
|
|
1294
|
+
if (!submitted || exactTurnSettled)
|
|
1295
|
+
releaseToolBarriers();
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1070
1298
|
async setupAgent(agentCtx, workspace, presetId, principal, selected, agentPresets) {
|
|
1071
1299
|
const agent = agentCtx.agent;
|
|
1072
1300
|
if (agent === undefined)
|
|
@@ -2382,10 +2610,31 @@ export class DshDeliveryRuntime {
|
|
|
2382
2610
|
if (authorizationFailure !== undefined)
|
|
2383
2611
|
return authorizationFailure;
|
|
2384
2612
|
}
|
|
2385
|
-
|
|
2613
|
+
let turnFrom = agent.session.events.length;
|
|
2614
|
+
const automaticCompletionTurns = new Map();
|
|
2615
|
+
const automaticCompletionToolViolations = new Set();
|
|
2386
2616
|
removeProgress = this.ctx.on('session/event', (session, event) => {
|
|
2387
2617
|
if (session !== agent.session)
|
|
2388
2618
|
return;
|
|
2619
|
+
if (event.type === 'step/start') {
|
|
2620
|
+
const automaticKind = automaticCompletionTurns.get(event.data.turn);
|
|
2621
|
+
if (automaticKind !== undefined) {
|
|
2622
|
+
publishProgress({
|
|
2623
|
+
kind: 'step',
|
|
2624
|
+
text: automaticKind === 'compact' ? '正在压缩并补全回答…' : '正在自动补全回答…',
|
|
2625
|
+
});
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
if (event.type === 'tool/call' && automaticCompletionTurns.has(event.data.turn)) {
|
|
2630
|
+
if (!automaticCompletionToolViolations.has(event.data.turn)) {
|
|
2631
|
+
automaticCompletionToolViolations.add(event.data.turn);
|
|
2632
|
+
agent.cancel({ kind: 'hook', reason: 'assistant-delivery-automatic-completion-tool-call' }, { keepInbox: true });
|
|
2633
|
+
}
|
|
2634
|
+
return;
|
|
2635
|
+
}
|
|
2636
|
+
if (event.type === 'tool/result' && automaticCompletionTurns.has(event.data.turn))
|
|
2637
|
+
return;
|
|
2389
2638
|
const update = deliveryProgressFromSessionEvent(event);
|
|
2390
2639
|
if (update !== undefined)
|
|
2391
2640
|
publishProgress(update);
|
|
@@ -2411,46 +2660,184 @@ export class DshDeliveryRuntime {
|
|
|
2411
2660
|
kind: 'delivery', channel: envelope.channel, account: envelope.account, eventId: envelope.eventId,
|
|
2412
2661
|
trust: 'untrusted',
|
|
2413
2662
|
} });
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
}
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2663
|
+
let deliveryTurn = await this.followupTurn(agent, message);
|
|
2664
|
+
let completionKind;
|
|
2665
|
+
let automaticTurns = 0;
|
|
2666
|
+
let assembledText = '';
|
|
2667
|
+
let bestEffortText = '';
|
|
2668
|
+
while (true) {
|
|
2669
|
+
const output = finalAssistant(agent.session.events, turnFrom, deliveryTurn);
|
|
2670
|
+
let addedText = false;
|
|
2671
|
+
if (output.text.trim() !== '') {
|
|
2672
|
+
if (completionKind === 'compact' || completionKind === 'regenerate') {
|
|
2673
|
+
const previousText = assembledText;
|
|
2674
|
+
addedText = output.text.trim() !== previousText.trim();
|
|
2675
|
+
if (addedText)
|
|
2676
|
+
assembledText = output.text;
|
|
2677
|
+
}
|
|
2678
|
+
else {
|
|
2679
|
+
const merged = mergeContinuationText(assembledText, output.text);
|
|
2680
|
+
assembledText = merged.text;
|
|
2681
|
+
addedText = merged.added;
|
|
2682
|
+
}
|
|
2683
|
+
bestEffortText = assembledText;
|
|
2684
|
+
}
|
|
2685
|
+
// Every exact segment crosses the persistence barrier before another paid model turn can start.
|
|
2686
|
+
if (!await sessions.flush(agent.session)) {
|
|
2687
|
+
publishProgress({ kind: 'failed', code: 'session-persistence-unavailable' });
|
|
2688
|
+
return { outcome: 'not-processed', failureCode: 'session-flush-failed', retryable: false };
|
|
2689
|
+
}
|
|
2690
|
+
signal.throwIfAborted();
|
|
2691
|
+
const cancelled = this.activeSessionControls.get(binding.sessionId)?.command;
|
|
2692
|
+
const automaticToolViolation = deliveryTurn !== undefined
|
|
2693
|
+
&& automaticCompletionToolViolations.has(deliveryTurn);
|
|
2694
|
+
if (cancelled !== undefined || (output.stopped && !automaticToolViolation)) {
|
|
2695
|
+
publishProgress({ kind: 'failed', code: cancelled === 'new' ? 'new-session' : 'user-stopped' });
|
|
2696
|
+
return { outcome: 'processed' };
|
|
2697
|
+
}
|
|
2698
|
+
const finalAuthorization = checkAuthorization();
|
|
2699
|
+
if (finalAuthorization !== 'authorized') {
|
|
2700
|
+
return finalAuthorization === 'revoked'
|
|
2701
|
+
? { outcome: 'not-processed', failureCode: 'inbound-authorization-revoked', retryable: false }
|
|
2702
|
+
: { outcome: 'not-processed', failureCode: 'inbound-authorization-check-failed', retryable: false };
|
|
2703
|
+
}
|
|
2704
|
+
const currentEmpty = output.text.trim() === '';
|
|
2705
|
+
const oversized = Buffer.byteLength(assembledText, 'utf8') > this.options.maxTextBytes;
|
|
2706
|
+
const stalledRecovery = completionKind !== undefined && output.completed && !currentEmpty && !addedText;
|
|
2707
|
+
// A tool-active turn with empty text is normal: the model's output was tool calls, not prose.
|
|
2708
|
+
// Do not treat it as an empty response needing recovery — that would inject a "do not call
|
|
2709
|
+
// tools" prompt and break the next turn.
|
|
2710
|
+
const emptyAfterToolsOnly = currentEmpty && output.hadToolActivity;
|
|
2711
|
+
const recoverable = automaticToolViolation || output.truncated
|
|
2712
|
+
|| (output.completed && ((currentEmpty && !emptyAfterToolsOnly) || oversized || stalledRecovery));
|
|
2713
|
+
if (recoverable && deliveryTurn !== undefined && !output.hasUnpairedToolCall
|
|
2714
|
+
&& automaticTurns < this.options.maxAutoContinuationTurns) {
|
|
2715
|
+
const isLastAutomaticTurn = automaticTurns + 1 >= this.options.maxAutoContinuationTurns;
|
|
2716
|
+
const nextKind = oversized
|
|
2717
|
+
? 'compact'
|
|
2718
|
+
: assembledText.trim() === ''
|
|
2719
|
+
? 'regenerate'
|
|
2720
|
+
: isLastAutomaticTurn
|
|
2721
|
+
? 'compact'
|
|
2722
|
+
: 'continue';
|
|
2723
|
+
automaticTurns += 1;
|
|
2724
|
+
turnFrom = agent.session.events.length;
|
|
2725
|
+
const completionMessage = createUserMessage({
|
|
2726
|
+
content: [{
|
|
2727
|
+
type: 'text',
|
|
2728
|
+
text: automaticCompletionPrompt(nextKind, this.options.maxTextBytes, this.options.maxOutputTokens),
|
|
2729
|
+
}],
|
|
2730
|
+
source: {
|
|
2731
|
+
kind: 'plugin',
|
|
2732
|
+
plugin: '@dsh-enhanced/assistant-delivery',
|
|
2733
|
+
form: 'notice',
|
|
2734
|
+
summary: `Automatic answer completion attempt ${automaticTurns}`,
|
|
2735
|
+
},
|
|
2736
|
+
});
|
|
2737
|
+
completionKind = nextKind;
|
|
2738
|
+
try {
|
|
2739
|
+
deliveryTurn = await this.automaticCompletionTurn(agent, completionMessage, signal, deliveryTurn, checkAuthorization, turn => automaticCompletionTurns.set(turn, nextKind));
|
|
2740
|
+
}
|
|
2741
|
+
catch (error) {
|
|
2742
|
+
if (error instanceof AutomaticCompletionAuthorizationError) {
|
|
2743
|
+
return error.state === 'revoked'
|
|
2744
|
+
? { outcome: 'not-processed', failureCode: 'inbound-authorization-revoked', retryable: false }
|
|
2745
|
+
: { outcome: 'not-processed', failureCode: 'inbound-authorization-check-failed', retryable: false };
|
|
2746
|
+
}
|
|
2747
|
+
if (!(error instanceof AutomaticCompletionUnavailableError))
|
|
2748
|
+
throw error;
|
|
2749
|
+
signal.throwIfAborted();
|
|
2750
|
+
const cancellation = this.activeSessionControls.get(binding.sessionId)?.command;
|
|
2751
|
+
if (cancellation !== undefined) {
|
|
2752
|
+
publishProgress({ kind: 'failed', code: cancellation === 'new' ? 'new-session' : 'user-stopped' });
|
|
2753
|
+
return { outcome: 'processed' };
|
|
2754
|
+
}
|
|
2755
|
+
const fallbackAuthorization = checkAuthorization();
|
|
2756
|
+
if (fallbackAuthorization !== 'authorized') {
|
|
2757
|
+
return fallbackAuthorization === 'revoked'
|
|
2758
|
+
? { outcome: 'not-processed', failureCode: 'inbound-authorization-revoked', retryable: false }
|
|
2759
|
+
: { outcome: 'not-processed', failureCode: 'inbound-authorization-check-failed', retryable: false };
|
|
2760
|
+
}
|
|
2761
|
+
const hasPartial = bestEffortText.trim() !== '';
|
|
2762
|
+
this.options.replyCommand(binding, envelope.eventId, {
|
|
2763
|
+
text: hasPartial
|
|
2764
|
+
? truncatedAgentReply(bestEffortText, this.options.maxTextBytes)
|
|
2765
|
+
: emptyAgentReply(this.options.maxTextBytes),
|
|
2766
|
+
format: 'markdown',
|
|
2767
|
+
});
|
|
2768
|
+
publishProgress({
|
|
2769
|
+
kind: 'failed',
|
|
2770
|
+
code: hasPartial ? AGENT_OUTPUT_TRUNCATED_CODE : AGENT_EMPTY_RESPONSE_CODE,
|
|
2771
|
+
});
|
|
2772
|
+
return { outcome: 'processed' };
|
|
2773
|
+
}
|
|
2774
|
+
continue;
|
|
2775
|
+
}
|
|
2776
|
+
if (automaticToolViolation) {
|
|
2777
|
+
const hasPartial = bestEffortText.trim() !== '';
|
|
2778
|
+
this.options.replyCommand(binding, envelope.eventId, {
|
|
2779
|
+
text: hasPartial
|
|
2780
|
+
? truncatedAgentReply(bestEffortText, this.options.maxTextBytes)
|
|
2781
|
+
: emptyAgentReply(this.options.maxTextBytes),
|
|
2782
|
+
format: 'markdown',
|
|
2783
|
+
});
|
|
2784
|
+
publishProgress({
|
|
2785
|
+
kind: 'failed',
|
|
2786
|
+
code: hasPartial ? AGENT_OUTPUT_TRUNCATED_CODE : AGENT_EMPTY_RESPONSE_CODE,
|
|
2787
|
+
});
|
|
2788
|
+
return { outcome: 'processed' };
|
|
2789
|
+
}
|
|
2790
|
+
if (output.truncated || oversized) {
|
|
2791
|
+
this.options.replyCommand(binding, envelope.eventId, {
|
|
2792
|
+
text: truncatedAgentReply(bestEffortText, this.options.maxTextBytes),
|
|
2793
|
+
format: 'markdown',
|
|
2794
|
+
});
|
|
2795
|
+
publishProgress({ kind: 'failed', code: AGENT_OUTPUT_TRUNCATED_CODE });
|
|
2796
|
+
return { outcome: 'processed' };
|
|
2797
|
+
}
|
|
2798
|
+
if (stalledRecovery) {
|
|
2799
|
+
this.options.replyCommand(binding, envelope.eventId, {
|
|
2800
|
+
text: truncatedAgentReply(bestEffortText, this.options.maxTextBytes),
|
|
2801
|
+
format: 'markdown',
|
|
2802
|
+
});
|
|
2803
|
+
publishProgress({ kind: 'failed', code: AGENT_OUTPUT_TRUNCATED_CODE });
|
|
2804
|
+
return { outcome: 'processed' };
|
|
2805
|
+
}
|
|
2806
|
+
if (!output.completed) {
|
|
2807
|
+
if (automaticTurns > 0 && bestEffortText.trim() !== '') {
|
|
2808
|
+
this.options.replyCommand(binding, envelope.eventId, {
|
|
2809
|
+
text: truncatedAgentReply(bestEffortText, this.options.maxTextBytes),
|
|
2810
|
+
format: 'markdown',
|
|
2811
|
+
});
|
|
2812
|
+
publishProgress({
|
|
2813
|
+
kind: 'failed',
|
|
2814
|
+
code: output.failureCode ?? AGENT_OUTPUT_TRUNCATED_CODE,
|
|
2815
|
+
});
|
|
2816
|
+
return { outcome: 'processed' };
|
|
2817
|
+
}
|
|
2818
|
+
publishProgress({ kind: 'failed', ...(output.failureCode === undefined ? {} : { code: output.failureCode }) });
|
|
2819
|
+
return { outcome: 'not-processed', failureCode: 'agent-turn-incomplete', retryable: false };
|
|
2820
|
+
}
|
|
2821
|
+
if (currentEmpty || assembledText.trim() === '') {
|
|
2822
|
+
const hasPartial = bestEffortText.trim() !== '';
|
|
2823
|
+
this.options.replyCommand(binding, envelope.eventId, {
|
|
2824
|
+
text: hasPartial
|
|
2825
|
+
? truncatedAgentReply(bestEffortText, this.options.maxTextBytes)
|
|
2826
|
+
: emptyAgentReply(this.options.maxTextBytes),
|
|
2827
|
+
format: 'markdown',
|
|
2828
|
+
});
|
|
2829
|
+
publishProgress({
|
|
2830
|
+
kind: 'failed',
|
|
2831
|
+
code: hasPartial ? AGENT_OUTPUT_TRUNCATED_CODE : AGENT_EMPTY_RESPONSE_CODE,
|
|
2832
|
+
});
|
|
2833
|
+
return { outcome: 'processed' };
|
|
2834
|
+
}
|
|
2835
|
+
break;
|
|
2450
2836
|
}
|
|
2837
|
+
signal.throwIfAborted();
|
|
2451
2838
|
// Agent answers are authored as Markdown (tables, bold, inline code), so request Markdown
|
|
2452
2839
|
// rendering; sending them as plain text shows the raw `|---|` and `**` syntax to the user.
|
|
2453
|
-
const learning = await this.options.replyCompletedPreferenceTurn(agent, binding, envelope, { text:
|
|
2840
|
+
const learning = await this.options.replyCompletedPreferenceTurn(agent, binding, envelope, { text: assembledText, format: 'markdown' });
|
|
2454
2841
|
if (learning === 'unknown') {
|
|
2455
2842
|
this.ctx.logger.warn('assistant-delivery: completed-turn preference projection is ambiguous');
|
|
2456
2843
|
}
|