@dsh-enhanced/assistant-delivery 0.1.18 → 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 +10 -0
- package/lib/agent-runtime.d.ts.map +1 -1
- package/lib/agent-runtime.js +478 -35
- 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 +3 -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 });
|
|
@@ -854,22 +863,140 @@ function sessionResumeFailureCode(error) {
|
|
|
854
863
|
function sessionFingerprint(value) {
|
|
855
864
|
return createHash('sha256').update(value).digest('hex').slice(0, 8);
|
|
856
865
|
}
|
|
866
|
+
const AGENT_OUTPUT_TRUNCATED_CODE = 'agent-output-truncated';
|
|
867
|
+
const AGENT_EMPTY_RESPONSE_CODE = 'agent-empty-response';
|
|
868
|
+
const AUTOMATIC_COMPLETION_TOOL_DENIAL = 'assistant-delivery: automatic answer completion disables tool execution';
|
|
869
|
+
function utf8Prefix(value, maxBytes) {
|
|
870
|
+
const bytes = Buffer.from(value, 'utf8');
|
|
871
|
+
if (bytes.length <= maxBytes)
|
|
872
|
+
return value;
|
|
873
|
+
let end = maxBytes;
|
|
874
|
+
while (end > 0 && (bytes[end] & 0xc0) === 0x80)
|
|
875
|
+
end -= 1;
|
|
876
|
+
return bytes.subarray(0, end).toString('utf8');
|
|
877
|
+
}
|
|
878
|
+
function fittingNotice(candidates, maxBytes) {
|
|
879
|
+
return candidates.find(value => Buffer.byteLength(value, 'utf8') <= maxBytes) ?? '!';
|
|
880
|
+
}
|
|
881
|
+
function truncatedAgentReply(text, maxBytes) {
|
|
882
|
+
const notice = fittingNotice([
|
|
883
|
+
'⚠️ 本次回答未能完整生成;以下内容可能不完整。',
|
|
884
|
+
'本次回答未完整生成。',
|
|
885
|
+
'回答不完整。',
|
|
886
|
+
'retry',
|
|
887
|
+
'!',
|
|
888
|
+
], maxBytes);
|
|
889
|
+
if (text.trim() === '')
|
|
890
|
+
return fittingNotice([
|
|
891
|
+
'本次未能生成可发送的完整回答。请稍后重试或缩小问题范围。',
|
|
892
|
+
'本次未能生成完整回答,请重试。',
|
|
893
|
+
'未生成回答,请重试。',
|
|
894
|
+
'retry',
|
|
895
|
+
'!',
|
|
896
|
+
], maxBytes);
|
|
897
|
+
const separator = '\n\n> ';
|
|
898
|
+
const suffix = `${separator}${notice}`;
|
|
899
|
+
const prefixBudget = maxBytes - Buffer.byteLength(suffix, 'utf8');
|
|
900
|
+
if (prefixBudget <= 0)
|
|
901
|
+
return notice;
|
|
902
|
+
const prefix = utf8Prefix(text, prefixBudget);
|
|
903
|
+
return prefix.trim() === '' ? notice : `${prefix}${suffix}`;
|
|
904
|
+
}
|
|
905
|
+
function emptyAgentReply(maxBytes) {
|
|
906
|
+
return fittingNotice([
|
|
907
|
+
'模型本轮未生成可发送的回答。请重试;如果问题较长,也可以拆成更小的步骤。',
|
|
908
|
+
'模型未生成回答,请重试。',
|
|
909
|
+
'请重试。',
|
|
910
|
+
'retry',
|
|
911
|
+
'!',
|
|
912
|
+
], maxBytes);
|
|
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
|
+
}
|
|
857
967
|
function finalAssistant(events, from, expectedTurn) {
|
|
858
|
-
if (expectedTurn === undefined)
|
|
859
|
-
return { text: '', completed: false, stopped: false };
|
|
968
|
+
if (expectedTurn === undefined) {
|
|
969
|
+
return { text: '', completed: false, truncated: false, stopped: false, hasUnpairedToolCall: false };
|
|
970
|
+
}
|
|
860
971
|
let text = '';
|
|
861
972
|
let completed = false;
|
|
973
|
+
let truncated = false;
|
|
862
974
|
let stopped = false;
|
|
975
|
+
const unpairedToolCalls = new Set();
|
|
863
976
|
let failureCode;
|
|
864
977
|
for (const event of events.slice(from)) {
|
|
865
978
|
if (event.type === 'assistant/message' && event.data.turn === expectedTurn) {
|
|
866
979
|
text = event.data.message.content.filter(block => block.type === 'text')
|
|
867
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));
|
|
868
993
|
}
|
|
869
994
|
if (event.type === 'turn/end' && event.data.turn === expectedTurn) {
|
|
870
995
|
const reason = event.data.reason;
|
|
871
996
|
completed = typeof reason === 'object' && reason !== null && 'kind' in reason
|
|
872
|
-
&&
|
|
997
|
+
&& reason.kind === 'completed';
|
|
998
|
+
truncated = typeof reason === 'object' && reason !== null && 'kind' in reason
|
|
999
|
+
&& reason.kind === 'max-tokens';
|
|
873
1000
|
stopped = typeof reason === 'object' && reason !== null
|
|
874
1001
|
&& reason.kind === 'aborted'
|
|
875
1002
|
&& typeof reason.reason === 'object'
|
|
@@ -878,10 +1005,17 @@ function finalAssistant(events, from, expectedTurn) {
|
|
|
878
1005
|
const error = typeof reason === 'object' && reason !== null && 'error' in reason
|
|
879
1006
|
? reason.error
|
|
880
1007
|
: undefined;
|
|
881
|
-
failureCode = completed || typeof error?.code !== 'string' ? undefined : error.code;
|
|
1008
|
+
failureCode = completed || truncated || typeof error?.code !== 'string' ? undefined : error.code;
|
|
882
1009
|
}
|
|
883
1010
|
}
|
|
884
|
-
return {
|
|
1011
|
+
return {
|
|
1012
|
+
text,
|
|
1013
|
+
completed,
|
|
1014
|
+
truncated,
|
|
1015
|
+
stopped,
|
|
1016
|
+
hasUnpairedToolCall: unpairedToolCalls.size > 0,
|
|
1017
|
+
...(failureCode === undefined ? {} : { failureCode }),
|
|
1018
|
+
};
|
|
885
1019
|
}
|
|
886
1020
|
export class DshDeliveryRuntime {
|
|
887
1021
|
ctx;
|
|
@@ -973,7 +1107,7 @@ export class DshDeliveryRuntime {
|
|
|
973
1107
|
}
|
|
974
1108
|
}
|
|
975
1109
|
/** Wait for only the turn that claims Delivery's exact message identity. */
|
|
976
|
-
followupTurn(agent, message) {
|
|
1110
|
+
followupTurn(agent, message, onClaimed, onSettled) {
|
|
977
1111
|
return new Promise((resolve, reject) => {
|
|
978
1112
|
let claimedTurn;
|
|
979
1113
|
let settled = false;
|
|
@@ -997,15 +1131,32 @@ export class DshDeliveryRuntime {
|
|
|
997
1131
|
reject(error);
|
|
998
1132
|
};
|
|
999
1133
|
const removeClaimed = this.ctx.on('agent/inbox/claimed', event => {
|
|
1000
|
-
if (event.agent === agent && event.message.id === message.id)
|
|
1134
|
+
if (event.agent === agent && event.message.id === message.id) {
|
|
1001
1135
|
claimedTurn = event.turn;
|
|
1136
|
+
onClaimed?.(event.turn);
|
|
1137
|
+
}
|
|
1002
1138
|
});
|
|
1003
1139
|
const removeDiscarded = this.ctx.on('agent/inbox/discarded', event => {
|
|
1004
|
-
if (event.agent
|
|
1005
|
-
|
|
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);
|
|
1006
1150
|
});
|
|
1007
1151
|
const removeSessionEvent = this.ctx.on('session/event', (session, event) => {
|
|
1008
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
|
+
}
|
|
1009
1160
|
settle(claimedTurn);
|
|
1010
1161
|
}
|
|
1011
1162
|
});
|
|
@@ -1017,6 +1168,129 @@ export class DshDeliveryRuntime {
|
|
|
1017
1168
|
}
|
|
1018
1169
|
});
|
|
1019
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
|
+
}
|
|
1020
1294
|
async setupAgent(agentCtx, workspace, presetId, principal, selected, agentPresets) {
|
|
1021
1295
|
const agent = agentCtx.agent;
|
|
1022
1296
|
if (agent === undefined)
|
|
@@ -2332,10 +2606,31 @@ export class DshDeliveryRuntime {
|
|
|
2332
2606
|
if (authorizationFailure !== undefined)
|
|
2333
2607
|
return authorizationFailure;
|
|
2334
2608
|
}
|
|
2335
|
-
|
|
2609
|
+
let turnFrom = agent.session.events.length;
|
|
2610
|
+
const automaticCompletionTurns = new Map();
|
|
2611
|
+
const automaticCompletionToolViolations = new Set();
|
|
2336
2612
|
removeProgress = this.ctx.on('session/event', (session, event) => {
|
|
2337
2613
|
if (session !== agent.session)
|
|
2338
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;
|
|
2339
2634
|
const update = deliveryProgressFromSessionEvent(event);
|
|
2340
2635
|
if (update !== undefined)
|
|
2341
2636
|
publishProgress(update);
|
|
@@ -2361,34 +2656,182 @@ export class DshDeliveryRuntime {
|
|
|
2361
2656
|
kind: 'delivery', channel: envelope.channel, account: envelope.account, eventId: envelope.eventId,
|
|
2362
2657
|
trust: 'untrusted',
|
|
2363
2658
|
} });
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
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;
|
|
2384
2828
|
}
|
|
2829
|
+
signal.throwIfAborted();
|
|
2385
2830
|
// Agent answers are authored as Markdown (tables, bold, inline code), so request Markdown
|
|
2386
2831
|
// rendering; sending them as plain text shows the raw `|---|` and `**` syntax to the user.
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
this.ctx.logger.warn('assistant-delivery: completed-turn preference projection is ambiguous');
|
|
2391
|
-
}
|
|
2832
|
+
const learning = await this.options.replyCompletedPreferenceTurn(agent, binding, envelope, { text: assembledText, format: 'markdown' });
|
|
2833
|
+
if (learning === 'unknown') {
|
|
2834
|
+
this.ctx.logger.warn('assistant-delivery: completed-turn preference projection is ambiguous');
|
|
2392
2835
|
}
|
|
2393
2836
|
publishProgress({ kind: 'completed' });
|
|
2394
2837
|
return { outcome: 'processed' };
|