@dsh-enhanced/assistant-delivery 0.1.19 → 0.1.20
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 +6 -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 +429 -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,85 @@ 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 };
|
|
970
|
+
}
|
|
907
971
|
let text = '';
|
|
908
972
|
let completed = false;
|
|
909
973
|
let truncated = false;
|
|
910
974
|
let stopped = false;
|
|
975
|
+
const unpairedToolCalls = new Set();
|
|
911
976
|
let failureCode;
|
|
912
977
|
for (const event of events.slice(from)) {
|
|
913
978
|
if (event.type === 'assistant/message' && event.data.turn === expectedTurn) {
|
|
914
979
|
text = event.data.message.content.filter(block => block.type === 'text')
|
|
915
980
|
.map(block => block.type === 'text' ? block.text : '').join('');
|
|
981
|
+
for (const block of event.data.message.content) {
|
|
982
|
+
if (block.type === 'tool-call')
|
|
983
|
+
unpairedToolCalls.add(String(block.id));
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
if (event.type === 'tool/result' && event.data.turn === expectedTurn) {
|
|
987
|
+
const block = event.data.message.content.find(value => value.type === 'tool-result');
|
|
988
|
+
const callId = block?.type === 'tool-result'
|
|
989
|
+
? block.toolCallId
|
|
990
|
+
: event.data.message.source?.callId;
|
|
991
|
+
if (callId !== undefined)
|
|
992
|
+
unpairedToolCalls.delete(String(callId));
|
|
916
993
|
}
|
|
917
994
|
if (event.type === 'turn/end' && event.data.turn === expectedTurn) {
|
|
918
995
|
const reason = event.data.reason;
|
|
@@ -931,7 +1008,14 @@ function finalAssistant(events, from, expectedTurn) {
|
|
|
931
1008
|
failureCode = completed || truncated || typeof error?.code !== 'string' ? undefined : error.code;
|
|
932
1009
|
}
|
|
933
1010
|
}
|
|
934
|
-
return {
|
|
1011
|
+
return {
|
|
1012
|
+
text,
|
|
1013
|
+
completed,
|
|
1014
|
+
truncated,
|
|
1015
|
+
stopped,
|
|
1016
|
+
hasUnpairedToolCall: unpairedToolCalls.size > 0,
|
|
1017
|
+
...(failureCode === undefined ? {} : { failureCode }),
|
|
1018
|
+
};
|
|
935
1019
|
}
|
|
936
1020
|
export class DshDeliveryRuntime {
|
|
937
1021
|
ctx;
|
|
@@ -1023,7 +1107,7 @@ export class DshDeliveryRuntime {
|
|
|
1023
1107
|
}
|
|
1024
1108
|
}
|
|
1025
1109
|
/** Wait for only the turn that claims Delivery's exact message identity. */
|
|
1026
|
-
followupTurn(agent, message) {
|
|
1110
|
+
followupTurn(agent, message, onClaimed, onSettled) {
|
|
1027
1111
|
return new Promise((resolve, reject) => {
|
|
1028
1112
|
let claimedTurn;
|
|
1029
1113
|
let settled = false;
|
|
@@ -1047,15 +1131,32 @@ export class DshDeliveryRuntime {
|
|
|
1047
1131
|
reject(error);
|
|
1048
1132
|
};
|
|
1049
1133
|
const removeClaimed = this.ctx.on('agent/inbox/claimed', event => {
|
|
1050
|
-
if (event.agent === agent && event.message.id === message.id)
|
|
1134
|
+
if (event.agent === agent && event.message.id === message.id) {
|
|
1051
1135
|
claimedTurn = event.turn;
|
|
1136
|
+
onClaimed?.(event.turn);
|
|
1137
|
+
}
|
|
1052
1138
|
});
|
|
1053
1139
|
const removeDiscarded = this.ctx.on('agent/inbox/discarded', event => {
|
|
1054
|
-
if (event.agent
|
|
1055
|
-
|
|
1140
|
+
if (event.agent !== agent || event.message.id !== message.id)
|
|
1141
|
+
return;
|
|
1142
|
+
try {
|
|
1143
|
+
onSettled?.();
|
|
1144
|
+
}
|
|
1145
|
+
catch (error) {
|
|
1146
|
+
fail(error);
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
settle(undefined);
|
|
1056
1150
|
});
|
|
1057
1151
|
const removeSessionEvent = this.ctx.on('session/event', (session, event) => {
|
|
1058
1152
|
if (session === agent.session && event.type === 'turn/end' && event.data.turn === claimedTurn) {
|
|
1153
|
+
try {
|
|
1154
|
+
onSettled?.();
|
|
1155
|
+
}
|
|
1156
|
+
catch (error) {
|
|
1157
|
+
fail(error);
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1059
1160
|
settle(claimedTurn);
|
|
1060
1161
|
}
|
|
1061
1162
|
});
|
|
@@ -1067,6 +1168,129 @@ export class DshDeliveryRuntime {
|
|
|
1067
1168
|
}
|
|
1068
1169
|
});
|
|
1069
1170
|
}
|
|
1171
|
+
/** Wait only for scheduling quiescence; exact turn ownership still comes from followupTurn(). */
|
|
1172
|
+
async waitForAgentIdle(agent, signal) {
|
|
1173
|
+
signal.throwIfAborted();
|
|
1174
|
+
while (true) {
|
|
1175
|
+
await new Promise((resolve, reject) => {
|
|
1176
|
+
let settled = false;
|
|
1177
|
+
const cleanup = () => signal.removeEventListener('abort', aborted);
|
|
1178
|
+
const finish = () => {
|
|
1179
|
+
if (settled)
|
|
1180
|
+
return;
|
|
1181
|
+
settled = true;
|
|
1182
|
+
cleanup();
|
|
1183
|
+
resolve();
|
|
1184
|
+
};
|
|
1185
|
+
const fail = (error) => {
|
|
1186
|
+
if (settled)
|
|
1187
|
+
return;
|
|
1188
|
+
settled = true;
|
|
1189
|
+
cleanup();
|
|
1190
|
+
reject(error);
|
|
1191
|
+
};
|
|
1192
|
+
const aborted = () => {
|
|
1193
|
+
try {
|
|
1194
|
+
signal.throwIfAborted();
|
|
1195
|
+
}
|
|
1196
|
+
catch (error) {
|
|
1197
|
+
fail(error);
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
signal.addEventListener('abort', aborted, { once: true });
|
|
1201
|
+
void agent.whenIdle().then(finish, fail);
|
|
1202
|
+
if (signal.aborted)
|
|
1203
|
+
aborted();
|
|
1204
|
+
});
|
|
1205
|
+
signal.throwIfAborted();
|
|
1206
|
+
if (agent.status === 'idle')
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Queue a synthetic completion turn only after the previous exact turn retired. rc.8 may still
|
|
1212
|
+
* serialize scoped tool schemas, but every call is denied before approval/execution while this
|
|
1213
|
+
* turn owns the Agent, so recovering prose cannot repeat an external side effect.
|
|
1214
|
+
*/
|
|
1215
|
+
async automaticCompletionTurn(agent, message, signal, expectedPreviousTurn, authorize, onClaimed) {
|
|
1216
|
+
await this.waitForAgentIdle(agent, signal);
|
|
1217
|
+
signal.throwIfAborted();
|
|
1218
|
+
let liftPreExecute;
|
|
1219
|
+
let liftRestriction;
|
|
1220
|
+
let liftGuard;
|
|
1221
|
+
let submitted = false;
|
|
1222
|
+
let exactTurnSettled = false;
|
|
1223
|
+
const releaseToolBarriers = () => {
|
|
1224
|
+
let firstError;
|
|
1225
|
+
if (liftGuard !== undefined) {
|
|
1226
|
+
try {
|
|
1227
|
+
liftGuard();
|
|
1228
|
+
liftGuard = undefined;
|
|
1229
|
+
}
|
|
1230
|
+
catch (error) {
|
|
1231
|
+
firstError ??= error;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
if (liftRestriction !== undefined) {
|
|
1235
|
+
try {
|
|
1236
|
+
liftRestriction();
|
|
1237
|
+
liftRestriction = undefined;
|
|
1238
|
+
}
|
|
1239
|
+
catch (error) {
|
|
1240
|
+
firstError ??= error;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
if (liftPreExecute !== undefined) {
|
|
1244
|
+
try {
|
|
1245
|
+
liftPreExecute();
|
|
1246
|
+
liftPreExecute = undefined;
|
|
1247
|
+
}
|
|
1248
|
+
catch (error) {
|
|
1249
|
+
firstError ??= error;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
if (firstError !== undefined)
|
|
1253
|
+
throw firstError;
|
|
1254
|
+
};
|
|
1255
|
+
try {
|
|
1256
|
+
liftPreExecute = agent.ctx.on('tools/pre-execute', () => Promise.resolve({
|
|
1257
|
+
kind: 'deny',
|
|
1258
|
+
reason: AUTOMATIC_COMPLETION_TOOL_DENIAL,
|
|
1259
|
+
}), { prepend: true });
|
|
1260
|
+
liftRestriction = agent.ctx.tools.restrict({ allow: [] });
|
|
1261
|
+
liftGuard = agent.ctx.tools.guard(() => AUTOMATIC_COMPLETION_TOOL_DENIAL);
|
|
1262
|
+
signal.throwIfAborted();
|
|
1263
|
+
if (agent.status !== 'idle' || agent.inbox.hasPending) {
|
|
1264
|
+
throw new AutomaticCompletionUnavailableError('assistant-delivery: automatic completion lost the idle scheduling boundary');
|
|
1265
|
+
}
|
|
1266
|
+
const latestBoundary = agent.session.events.findLast(event => event.type === 'turn/start' || event.type === 'turn/end');
|
|
1267
|
+
if (latestBoundary?.type !== 'turn/end' || latestBoundary.data.turn !== expectedPreviousTurn) {
|
|
1268
|
+
throw new AutomaticCompletionUnavailableError('assistant-delivery: another turn crossed the automatic completion boundary');
|
|
1269
|
+
}
|
|
1270
|
+
const authorization = authorize();
|
|
1271
|
+
if (authorization !== 'authorized')
|
|
1272
|
+
throw new AutomaticCompletionAuthorizationError(authorization);
|
|
1273
|
+
submitted = true;
|
|
1274
|
+
return await this.followupTurn(agent, message, onClaimed, () => {
|
|
1275
|
+
exactTurnSettled = true;
|
|
1276
|
+
releaseToolBarriers();
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
catch (error) {
|
|
1280
|
+
if (signal.aborted)
|
|
1281
|
+
signal.throwIfAborted();
|
|
1282
|
+
if (submitted || error instanceof AutomaticCompletionAuthorizationError
|
|
1283
|
+
|| error instanceof AutomaticCompletionUnavailableError)
|
|
1284
|
+
throw error;
|
|
1285
|
+
throw new AutomaticCompletionUnavailableError('assistant-delivery: automatic completion safety guard was unavailable', { cause: error });
|
|
1286
|
+
}
|
|
1287
|
+
finally {
|
|
1288
|
+
// A submitted message may already be pending or running even when followup() throws.
|
|
1289
|
+
// Keep the barriers attached to the Agent scope until exact settlement or handle teardown.
|
|
1290
|
+
if (!submitted || exactTurnSettled)
|
|
1291
|
+
releaseToolBarriers();
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1070
1294
|
async setupAgent(agentCtx, workspace, presetId, principal, selected, agentPresets) {
|
|
1071
1295
|
const agent = agentCtx.agent;
|
|
1072
1296
|
if (agent === undefined)
|
|
@@ -2382,10 +2606,31 @@ export class DshDeliveryRuntime {
|
|
|
2382
2606
|
if (authorizationFailure !== undefined)
|
|
2383
2607
|
return authorizationFailure;
|
|
2384
2608
|
}
|
|
2385
|
-
|
|
2609
|
+
let turnFrom = agent.session.events.length;
|
|
2610
|
+
const automaticCompletionTurns = new Map();
|
|
2611
|
+
const automaticCompletionToolViolations = new Set();
|
|
2386
2612
|
removeProgress = this.ctx.on('session/event', (session, event) => {
|
|
2387
2613
|
if (session !== agent.session)
|
|
2388
2614
|
return;
|
|
2615
|
+
if (event.type === 'step/start') {
|
|
2616
|
+
const automaticKind = automaticCompletionTurns.get(event.data.turn);
|
|
2617
|
+
if (automaticKind !== undefined) {
|
|
2618
|
+
publishProgress({
|
|
2619
|
+
kind: 'step',
|
|
2620
|
+
text: automaticKind === 'compact' ? '正在压缩并补全回答…' : '正在自动补全回答…',
|
|
2621
|
+
});
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
if (event.type === 'tool/call' && automaticCompletionTurns.has(event.data.turn)) {
|
|
2626
|
+
if (!automaticCompletionToolViolations.has(event.data.turn)) {
|
|
2627
|
+
automaticCompletionToolViolations.add(event.data.turn);
|
|
2628
|
+
agent.cancel({ kind: 'hook', reason: 'assistant-delivery-automatic-completion-tool-call' }, { keepInbox: true });
|
|
2629
|
+
}
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
if (event.type === 'tool/result' && automaticCompletionTurns.has(event.data.turn))
|
|
2633
|
+
return;
|
|
2389
2634
|
const update = deliveryProgressFromSessionEvent(event);
|
|
2390
2635
|
if (update !== undefined)
|
|
2391
2636
|
publishProgress(update);
|
|
@@ -2411,46 +2656,180 @@ export class DshDeliveryRuntime {
|
|
|
2411
2656
|
kind: 'delivery', channel: envelope.channel, account: envelope.account, eventId: envelope.eventId,
|
|
2412
2657
|
trust: 'untrusted',
|
|
2413
2658
|
} });
|
|
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
|
-
|
|
2659
|
+
let deliveryTurn = await this.followupTurn(agent, message);
|
|
2660
|
+
let completionKind;
|
|
2661
|
+
let automaticTurns = 0;
|
|
2662
|
+
let assembledText = '';
|
|
2663
|
+
let bestEffortText = '';
|
|
2664
|
+
while (true) {
|
|
2665
|
+
const output = finalAssistant(agent.session.events, turnFrom, deliveryTurn);
|
|
2666
|
+
let addedText = false;
|
|
2667
|
+
if (output.text.trim() !== '') {
|
|
2668
|
+
if (completionKind === 'compact' || completionKind === 'regenerate') {
|
|
2669
|
+
const previousText = assembledText;
|
|
2670
|
+
addedText = output.text.trim() !== previousText.trim();
|
|
2671
|
+
if (addedText)
|
|
2672
|
+
assembledText = output.text;
|
|
2673
|
+
}
|
|
2674
|
+
else {
|
|
2675
|
+
const merged = mergeContinuationText(assembledText, output.text);
|
|
2676
|
+
assembledText = merged.text;
|
|
2677
|
+
addedText = merged.added;
|
|
2678
|
+
}
|
|
2679
|
+
bestEffortText = assembledText;
|
|
2680
|
+
}
|
|
2681
|
+
// Every exact segment crosses the persistence barrier before another paid model turn can start.
|
|
2682
|
+
if (!await sessions.flush(agent.session)) {
|
|
2683
|
+
publishProgress({ kind: 'failed', code: 'session-persistence-unavailable' });
|
|
2684
|
+
return { outcome: 'not-processed', failureCode: 'session-flush-failed', retryable: false };
|
|
2685
|
+
}
|
|
2686
|
+
signal.throwIfAborted();
|
|
2687
|
+
const cancelled = this.activeSessionControls.get(binding.sessionId)?.command;
|
|
2688
|
+
const automaticToolViolation = deliveryTurn !== undefined
|
|
2689
|
+
&& automaticCompletionToolViolations.has(deliveryTurn);
|
|
2690
|
+
if (cancelled !== undefined || (output.stopped && !automaticToolViolation)) {
|
|
2691
|
+
publishProgress({ kind: 'failed', code: cancelled === 'new' ? 'new-session' : 'user-stopped' });
|
|
2692
|
+
return { outcome: 'processed' };
|
|
2693
|
+
}
|
|
2694
|
+
const finalAuthorization = checkAuthorization();
|
|
2695
|
+
if (finalAuthorization !== 'authorized') {
|
|
2696
|
+
return finalAuthorization === 'revoked'
|
|
2697
|
+
? { outcome: 'not-processed', failureCode: 'inbound-authorization-revoked', retryable: false }
|
|
2698
|
+
: { outcome: 'not-processed', failureCode: 'inbound-authorization-check-failed', retryable: false };
|
|
2699
|
+
}
|
|
2700
|
+
const currentEmpty = output.text.trim() === '';
|
|
2701
|
+
const oversized = Buffer.byteLength(assembledText, 'utf8') > this.options.maxTextBytes;
|
|
2702
|
+
const stalledRecovery = completionKind !== undefined && output.completed && !currentEmpty && !addedText;
|
|
2703
|
+
const recoverable = automaticToolViolation || output.truncated
|
|
2704
|
+
|| (output.completed && (currentEmpty || oversized || stalledRecovery));
|
|
2705
|
+
if (recoverable && deliveryTurn !== undefined && !output.hasUnpairedToolCall
|
|
2706
|
+
&& automaticTurns < this.options.maxAutoContinuationTurns) {
|
|
2707
|
+
const isLastAutomaticTurn = automaticTurns + 1 >= this.options.maxAutoContinuationTurns;
|
|
2708
|
+
const nextKind = oversized
|
|
2709
|
+
? 'compact'
|
|
2710
|
+
: assembledText.trim() === ''
|
|
2711
|
+
? 'regenerate'
|
|
2712
|
+
: isLastAutomaticTurn
|
|
2713
|
+
? 'compact'
|
|
2714
|
+
: 'continue';
|
|
2715
|
+
automaticTurns += 1;
|
|
2716
|
+
turnFrom = agent.session.events.length;
|
|
2717
|
+
const completionMessage = createUserMessage({
|
|
2718
|
+
content: [{
|
|
2719
|
+
type: 'text',
|
|
2720
|
+
text: automaticCompletionPrompt(nextKind, this.options.maxTextBytes, this.options.maxOutputTokens),
|
|
2721
|
+
}],
|
|
2722
|
+
source: {
|
|
2723
|
+
kind: 'plugin',
|
|
2724
|
+
plugin: '@dsh-enhanced/assistant-delivery',
|
|
2725
|
+
form: 'notice',
|
|
2726
|
+
summary: `Automatic answer completion attempt ${automaticTurns}`,
|
|
2727
|
+
},
|
|
2728
|
+
});
|
|
2729
|
+
completionKind = nextKind;
|
|
2730
|
+
try {
|
|
2731
|
+
deliveryTurn = await this.automaticCompletionTurn(agent, completionMessage, signal, deliveryTurn, checkAuthorization, turn => automaticCompletionTurns.set(turn, nextKind));
|
|
2732
|
+
}
|
|
2733
|
+
catch (error) {
|
|
2734
|
+
if (error instanceof AutomaticCompletionAuthorizationError) {
|
|
2735
|
+
return error.state === 'revoked'
|
|
2736
|
+
? { outcome: 'not-processed', failureCode: 'inbound-authorization-revoked', retryable: false }
|
|
2737
|
+
: { outcome: 'not-processed', failureCode: 'inbound-authorization-check-failed', retryable: false };
|
|
2738
|
+
}
|
|
2739
|
+
if (!(error instanceof AutomaticCompletionUnavailableError))
|
|
2740
|
+
throw error;
|
|
2741
|
+
signal.throwIfAborted();
|
|
2742
|
+
const cancellation = this.activeSessionControls.get(binding.sessionId)?.command;
|
|
2743
|
+
if (cancellation !== undefined) {
|
|
2744
|
+
publishProgress({ kind: 'failed', code: cancellation === 'new' ? 'new-session' : 'user-stopped' });
|
|
2745
|
+
return { outcome: 'processed' };
|
|
2746
|
+
}
|
|
2747
|
+
const fallbackAuthorization = checkAuthorization();
|
|
2748
|
+
if (fallbackAuthorization !== 'authorized') {
|
|
2749
|
+
return fallbackAuthorization === 'revoked'
|
|
2750
|
+
? { outcome: 'not-processed', failureCode: 'inbound-authorization-revoked', retryable: false }
|
|
2751
|
+
: { outcome: 'not-processed', failureCode: 'inbound-authorization-check-failed', retryable: false };
|
|
2752
|
+
}
|
|
2753
|
+
const hasPartial = bestEffortText.trim() !== '';
|
|
2754
|
+
this.options.replyCommand(binding, envelope.eventId, {
|
|
2755
|
+
text: hasPartial
|
|
2756
|
+
? truncatedAgentReply(bestEffortText, this.options.maxTextBytes)
|
|
2757
|
+
: emptyAgentReply(this.options.maxTextBytes),
|
|
2758
|
+
format: 'markdown',
|
|
2759
|
+
});
|
|
2760
|
+
publishProgress({
|
|
2761
|
+
kind: 'failed',
|
|
2762
|
+
code: hasPartial ? AGENT_OUTPUT_TRUNCATED_CODE : AGENT_EMPTY_RESPONSE_CODE,
|
|
2763
|
+
});
|
|
2764
|
+
return { outcome: 'processed' };
|
|
2765
|
+
}
|
|
2766
|
+
continue;
|
|
2767
|
+
}
|
|
2768
|
+
if (automaticToolViolation) {
|
|
2769
|
+
const hasPartial = bestEffortText.trim() !== '';
|
|
2770
|
+
this.options.replyCommand(binding, envelope.eventId, {
|
|
2771
|
+
text: hasPartial
|
|
2772
|
+
? truncatedAgentReply(bestEffortText, this.options.maxTextBytes)
|
|
2773
|
+
: emptyAgentReply(this.options.maxTextBytes),
|
|
2774
|
+
format: 'markdown',
|
|
2775
|
+
});
|
|
2776
|
+
publishProgress({
|
|
2777
|
+
kind: 'failed',
|
|
2778
|
+
code: hasPartial ? AGENT_OUTPUT_TRUNCATED_CODE : AGENT_EMPTY_RESPONSE_CODE,
|
|
2779
|
+
});
|
|
2780
|
+
return { outcome: 'processed' };
|
|
2781
|
+
}
|
|
2782
|
+
if (output.truncated || oversized) {
|
|
2783
|
+
this.options.replyCommand(binding, envelope.eventId, {
|
|
2784
|
+
text: truncatedAgentReply(bestEffortText, this.options.maxTextBytes),
|
|
2785
|
+
format: 'markdown',
|
|
2786
|
+
});
|
|
2787
|
+
publishProgress({ kind: 'failed', code: AGENT_OUTPUT_TRUNCATED_CODE });
|
|
2788
|
+
return { outcome: 'processed' };
|
|
2789
|
+
}
|
|
2790
|
+
if (stalledRecovery) {
|
|
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 (!output.completed) {
|
|
2799
|
+
if (automaticTurns > 0 && bestEffortText.trim() !== '') {
|
|
2800
|
+
this.options.replyCommand(binding, envelope.eventId, {
|
|
2801
|
+
text: truncatedAgentReply(bestEffortText, this.options.maxTextBytes),
|
|
2802
|
+
format: 'markdown',
|
|
2803
|
+
});
|
|
2804
|
+
publishProgress({
|
|
2805
|
+
kind: 'failed',
|
|
2806
|
+
code: output.failureCode ?? AGENT_OUTPUT_TRUNCATED_CODE,
|
|
2807
|
+
});
|
|
2808
|
+
return { outcome: 'processed' };
|
|
2809
|
+
}
|
|
2810
|
+
publishProgress({ kind: 'failed', ...(output.failureCode === undefined ? {} : { code: output.failureCode }) });
|
|
2811
|
+
return { outcome: 'not-processed', failureCode: 'agent-turn-incomplete', retryable: false };
|
|
2812
|
+
}
|
|
2813
|
+
if (currentEmpty || assembledText.trim() === '') {
|
|
2814
|
+
const hasPartial = bestEffortText.trim() !== '';
|
|
2815
|
+
this.options.replyCommand(binding, envelope.eventId, {
|
|
2816
|
+
text: hasPartial
|
|
2817
|
+
? truncatedAgentReply(bestEffortText, this.options.maxTextBytes)
|
|
2818
|
+
: emptyAgentReply(this.options.maxTextBytes),
|
|
2819
|
+
format: 'markdown',
|
|
2820
|
+
});
|
|
2821
|
+
publishProgress({
|
|
2822
|
+
kind: 'failed',
|
|
2823
|
+
code: hasPartial ? AGENT_OUTPUT_TRUNCATED_CODE : AGENT_EMPTY_RESPONSE_CODE,
|
|
2824
|
+
});
|
|
2825
|
+
return { outcome: 'processed' };
|
|
2826
|
+
}
|
|
2827
|
+
break;
|
|
2450
2828
|
}
|
|
2829
|
+
signal.throwIfAborted();
|
|
2451
2830
|
// Agent answers are authored as Markdown (tables, bold, inline code), so request Markdown
|
|
2452
2831
|
// 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:
|
|
2832
|
+
const learning = await this.options.replyCompletedPreferenceTurn(agent, binding, envelope, { text: assembledText, format: 'markdown' });
|
|
2454
2833
|
if (learning === 'unknown') {
|
|
2455
2834
|
this.ctx.logger.warn('assistant-delivery: completed-turn preference projection is ambiguous');
|
|
2456
2835
|
}
|