@dsh-enhanced/assistant-delivery 0.1.31 → 0.1.34
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 +13 -1
- package/lib/acceptance.d.ts +11 -3
- package/lib/acceptance.d.ts.map +1 -1
- package/lib/agent-runtime.d.ts +15 -0
- package/lib/agent-runtime.d.ts.map +1 -1
- package/lib/agent-runtime.js +270 -47
- package/lib/agent-runtime.js.map +1 -1
- package/lib/foreground-observation.d.ts +32 -0
- package/lib/foreground-observation.d.ts.map +1 -0
- package/lib/foreground-observation.js +13 -0
- package/lib/foreground-observation.js.map +1 -0
- package/lib/index.d.ts +6 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/native-web-owner.d.ts.map +1 -1
- package/lib/native-web-owner.js +3 -3
- package/lib/native-web-owner.js.map +1 -1
- package/lib/reply-replay.d.ts +52 -0
- package/lib/reply-replay.d.ts.map +1 -0
- package/lib/reply-replay.js +122 -0
- package/lib/reply-replay.js.map +1 -0
- package/lib/service.d.ts +115 -2
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +483 -94
- package/lib/service.js.map +1 -1
- package/lib/session-lease-runtime.d.ts +2 -1
- package/lib/session-lease-runtime.d.ts.map +1 -1
- package/lib/session-lease-runtime.js +3 -3
- package/lib/session-lease-runtime.js.map +1 -1
- package/lib/sqlite.d.ts +1 -1
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +146 -3
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +151 -10
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +520 -7
- package/lib/store.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +14 -10
package/lib/agent-runtime.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { mkdir, stat } from 'node:fs/promises';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
4
|
import { installModelSelection, } from '@deepseek-ai/dsh-agent';
|
|
4
5
|
import { contentHasImage, createUserMessage, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
|
|
6
|
+
import { TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools';
|
|
5
7
|
import { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
|
|
6
8
|
import { isAppendSurfaceEvent, SessionId } from '@deepseek-ai/dsh-session';
|
|
7
9
|
import { setApprovalPolicy, } from '@deepseek-ai/dsh-user-approval';
|
|
@@ -328,6 +330,13 @@ export function deliveryProgressFromSessionEvent(event) {
|
|
|
328
330
|
if (event.type === 'assistant/message') {
|
|
329
331
|
return undefined;
|
|
330
332
|
}
|
|
333
|
+
if (event.type === 'approval/asked') {
|
|
334
|
+
// approval/asked is appended before the review waterfall runs, so this label
|
|
335
|
+
// must stay accurate for both the (usually fast) automatic review and a real
|
|
336
|
+
// human escalation (which additionally renders a separate allow/reject card).
|
|
337
|
+
// A tool name here is policy-controlled, but bound it like every other field.
|
|
338
|
+
return { kind: 'step', text: `正在确认工具「${boundedProgressText(event.data.toolName)}」的调用权限…` };
|
|
339
|
+
}
|
|
331
340
|
if (event.type === 'tool/call') {
|
|
332
341
|
if (event.data.name === 'ask_user_question') {
|
|
333
342
|
return { kind: 'step', text: '等待您的回答…' };
|
|
@@ -864,6 +873,12 @@ function sessionResumeFailureCode(error) {
|
|
|
864
873
|
return 'session-persistence-corrupt';
|
|
865
874
|
return 'session-resume-unavailable';
|
|
866
875
|
}
|
|
876
|
+
function sessionFormatUnsupportedReply() {
|
|
877
|
+
return {
|
|
878
|
+
text: '旧会话格式无法由当前 Host 恢复,记录已保留。请使用 /new 开启会话。',
|
|
879
|
+
format: 'plain',
|
|
880
|
+
};
|
|
881
|
+
}
|
|
867
882
|
function sessionFingerprint(value) {
|
|
868
883
|
return createHash('sha256').update(value).digest('hex').slice(0, 8);
|
|
869
884
|
}
|
|
@@ -970,13 +985,14 @@ function automaticCompletionPrompt(kind, maxTextBytes, maxOutputTokens) {
|
|
|
970
985
|
}
|
|
971
986
|
function finalAssistant(events, from, expectedTurn) {
|
|
972
987
|
if (expectedTurn === undefined) {
|
|
973
|
-
return { text: '', completed: false, truncated: false, stopped: false, hasUnpairedToolCall: false };
|
|
988
|
+
return { text: '', completed: false, truncated: false, stopped: false, hasUnpairedToolCall: false, hadToolCall: false };
|
|
974
989
|
}
|
|
975
990
|
let text = '';
|
|
976
991
|
let completed = false;
|
|
977
992
|
let truncated = false;
|
|
978
993
|
let stopped = false;
|
|
979
994
|
const unpairedToolCalls = new Set();
|
|
995
|
+
let hadToolCall = false;
|
|
980
996
|
let failureCode;
|
|
981
997
|
for (const event of events.slice(from)) {
|
|
982
998
|
if (event.type === 'assistant/message' && event.data.turn === expectedTurn) {
|
|
@@ -984,6 +1000,7 @@ function finalAssistant(events, from, expectedTurn) {
|
|
|
984
1000
|
.map(block => block.type === 'text' ? block.text : '').join('');
|
|
985
1001
|
for (const block of event.data.message.content) {
|
|
986
1002
|
if (block.type === 'tool-call') {
|
|
1003
|
+
hadToolCall = true;
|
|
987
1004
|
unpairedToolCalls.add(String(block.id));
|
|
988
1005
|
}
|
|
989
1006
|
}
|
|
@@ -1019,6 +1036,7 @@ function finalAssistant(events, from, expectedTurn) {
|
|
|
1019
1036
|
truncated,
|
|
1020
1037
|
stopped,
|
|
1021
1038
|
hasUnpairedToolCall: unpairedToolCalls.size > 0,
|
|
1039
|
+
hadToolCall,
|
|
1022
1040
|
...(failureCode === undefined ? {} : { failureCode }),
|
|
1023
1041
|
};
|
|
1024
1042
|
}
|
|
@@ -1026,6 +1044,58 @@ function isScheduledGoalRuntimeContext(source) {
|
|
|
1026
1044
|
return (source.kind === 'plugin' && source.plugin === '@deepseek-ai/dsh-system-prompt' && source.form === 'snapshot')
|
|
1027
1045
|
|| (source.kind === 'skill-catalog' && source.form === 'catalog');
|
|
1028
1046
|
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Trusted Host clarification injected after the human deliberately cancels a
|
|
1049
|
+
* tool call before it dispatched.
|
|
1050
|
+
*
|
|
1051
|
+
* Why this exists: when a turn is aborted by the human, the agent loop persists
|
|
1052
|
+
* a `tool/result` pair whose ONLY model-visible text is the bare technical
|
|
1053
|
+
* string "Error: tool call aborted before dispatch" (`isError: true`). The
|
|
1054
|
+
* accurate structured facts — `error.code = ABORTED_BEFORE_DISPATCH` and the
|
|
1055
|
+
* `turn/end` reason `{ aborted, reason: { user } }` — are not surface events,
|
|
1056
|
+
* so the model never sees them. A string of those error-looking results makes a
|
|
1057
|
+
* later no-op message (e.g. "hi") look like "retry the fetch the system kept
|
|
1058
|
+
* interrupting", causing the model to re-fire the cancelled tool unprompted.
|
|
1059
|
+
*
|
|
1060
|
+
* This text is a FIXED Host constant. It never interpolates message content,
|
|
1061
|
+
* tool names, or arguments, so untrusted input cannot flow into the trusted
|
|
1062
|
+
* snapshot. The predicate keys only off structured, Host-owned fields.
|
|
1063
|
+
*/
|
|
1064
|
+
const USER_CANCELLED_TOOL_NOTICE = [
|
|
1065
|
+
'The most recent turn was stopped deliberately by the human (they pressed stop or declined the tool permission).',
|
|
1066
|
+
'Any earlier tool result reading "Error: tool call aborted before dispatch" is that human cancellation, NOT a tool or system failure and NOT an instruction to retry.',
|
|
1067
|
+
'Do not retry, resume, restart, or continue any tool call or task that was interrupted by that stop.',
|
|
1068
|
+
'Respond only to the human\'s latest message on its own terms. Resume the earlier work only if the human explicitly asks for it again.',
|
|
1069
|
+
].join(' ');
|
|
1070
|
+
/** Whether a `turn/end` reason is a human-initiated abort (stop, /new, or a declined approval). */
|
|
1071
|
+
function isUserAbortedTurnEnd(reason) {
|
|
1072
|
+
return typeof reason === 'object' && reason !== null
|
|
1073
|
+
&& reason.kind === 'aborted'
|
|
1074
|
+
&& typeof reason.reason === 'object'
|
|
1075
|
+
&& reason.reason?.kind === 'user';
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Returns the fixed Host clarification when the most recent turn boundary is a
|
|
1079
|
+
* human abort whose turn contains an aborted-before-dispatch tool result;
|
|
1080
|
+
* otherwise `''`. Pure over the supplied events so the projection dedups stable
|
|
1081
|
+
* text across repeated no-op messages and flips to empty once a turn completes.
|
|
1082
|
+
*/
|
|
1083
|
+
function userCancelledToolNotice(events) {
|
|
1084
|
+
let lastEnd;
|
|
1085
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
1086
|
+
const event = events[index];
|
|
1087
|
+
if (event?.type === 'turn/end') {
|
|
1088
|
+
lastEnd = event;
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (lastEnd === undefined || !isUserAbortedTurnEnd(lastEnd.data.reason))
|
|
1093
|
+
return '';
|
|
1094
|
+
const abortedBeforeDispatch = events.some(event => event.type === 'tool/result'
|
|
1095
|
+
&& event.data.turn === lastEnd.data.turn
|
|
1096
|
+
&& event.data.error?.code === TOOL_ABORTED_BEFORE_DISPATCH);
|
|
1097
|
+
return abortedBeforeDispatch ? USER_CANCELLED_TOOL_NOTICE : '';
|
|
1098
|
+
}
|
|
1029
1099
|
export class DshDeliveryRuntime {
|
|
1030
1100
|
ctx;
|
|
1031
1101
|
policy;
|
|
@@ -1385,6 +1455,7 @@ export class DshDeliveryRuntime {
|
|
|
1385
1455
|
aborted();
|
|
1386
1456
|
return {
|
|
1387
1457
|
settle: beginSettling,
|
|
1458
|
+
pinActiveArmedGoal: pinArmedGoal,
|
|
1388
1459
|
teardownTimeoutMs,
|
|
1389
1460
|
isQuiescent,
|
|
1390
1461
|
dispose: () => {
|
|
@@ -1658,8 +1729,8 @@ export class DshDeliveryRuntime {
|
|
|
1658
1729
|
releaseToolBarriers();
|
|
1659
1730
|
}
|
|
1660
1731
|
}
|
|
1661
|
-
async setupAgent(agentCtx, workspace, presetId, principal, selected, agentPresets, initiator = 'external') {
|
|
1662
|
-
const agent = agentCtx.agent;
|
|
1732
|
+
async setupAgent(agentCtx, preparedAgent, workspace, presetId, principal, selected, agentPresets, initiator = 'external') {
|
|
1733
|
+
const agent = preparedAgent ?? agentCtx.agent;
|
|
1663
1734
|
if (agent === undefined)
|
|
1664
1735
|
throw new Error('assistant-delivery: unpublished Agent identity is missing');
|
|
1665
1736
|
if (agent.session.header.cwd !== workspace || agent.session.header.agentPreset !== presetId) {
|
|
@@ -1668,6 +1739,36 @@ export class DshDeliveryRuntime {
|
|
|
1668
1739
|
const unbind = this.policy.bindInitiator(agent, initiator, principal);
|
|
1669
1740
|
agentCtx.effect(() => unbind, `assistant-delivery.${initiator}-initiator`);
|
|
1670
1741
|
installModelSelection(agentCtx, { current: selected, assembled: undefined });
|
|
1742
|
+
// The trusted post-cancellation clarification is installed LAZILY and only
|
|
1743
|
+
// for a session whose history actually contains a human-aborted turn with an
|
|
1744
|
+
// aborted-before-dispatch tool result. Merely registering a runtime context
|
|
1745
|
+
// flips the system-prompt assembly out of its empty baseline and perturbs the
|
|
1746
|
+
// cold-resume/lease-recovery accounting for EVERY ordinary session (a
|
|
1747
|
+
// permission /stop compensation run has no such tool result, yet regressed
|
|
1748
|
+
// when the context was registered unconditionally). Keep the exact baseline —
|
|
1749
|
+
// no contribution installed at all — until the Host-owned predicate matches.
|
|
1750
|
+
let cancelNoticeInstalled = false;
|
|
1751
|
+
const installUserCancelledToolNotice = () => {
|
|
1752
|
+
if (cancelNoticeInstalled)
|
|
1753
|
+
return;
|
|
1754
|
+
if (!userCancelledToolNotice(agent.session.snapshotEvents()))
|
|
1755
|
+
return;
|
|
1756
|
+
cancelNoticeInstalled = true;
|
|
1757
|
+
agentCtx.inject(['systemPrompt'], promptCtx => promptCtx.systemPrompt.context({
|
|
1758
|
+
name: 'assistant-delivery:user-cancelled-tool-notice',
|
|
1759
|
+
order: 260,
|
|
1760
|
+
text: () => userCancelledToolNotice(agent.session.snapshotEvents()),
|
|
1761
|
+
}));
|
|
1762
|
+
};
|
|
1763
|
+
// A cold resume may already carry the abort (the real Feishu sequence did).
|
|
1764
|
+
installUserCancelledToolNotice();
|
|
1765
|
+
// A live abort only becomes visible once its turn/end + tool/result land.
|
|
1766
|
+
agentCtx.on('session/event', (eventSession, event) => {
|
|
1767
|
+
if (eventSession !== agent.session)
|
|
1768
|
+
return;
|
|
1769
|
+
if (event.type === 'turn/end' || event.type === 'tool/result')
|
|
1770
|
+
installUserCancelledToolNotice();
|
|
1771
|
+
});
|
|
1671
1772
|
await agentPresets?.mount(agentCtx, presetId);
|
|
1672
1773
|
}
|
|
1673
1774
|
/**
|
|
@@ -1858,8 +1959,8 @@ export class DshDeliveryRuntime {
|
|
|
1858
1959
|
model: selected.model,
|
|
1859
1960
|
maxTokens: this.options.maxOutputTokens,
|
|
1860
1961
|
},
|
|
1861
|
-
setup: async (agentCtx) => {
|
|
1862
|
-
await this.setupAgent(agentCtx, binding.workspace, presetId, externalPrincipalId(binding.principal), selected, agentPresets);
|
|
1962
|
+
setup: async (agentCtx, preparedAgent) => {
|
|
1963
|
+
await this.setupAgent(agentCtx, preparedAgent, binding.workspace, presetId, externalPrincipalId(binding.principal), selected, agentPresets);
|
|
1863
1964
|
},
|
|
1864
1965
|
});
|
|
1865
1966
|
signal.throwIfAborted();
|
|
@@ -2177,8 +2278,8 @@ export class DshDeliveryRuntime {
|
|
|
2177
2278
|
await ensureWorkspace(workspace);
|
|
2178
2279
|
const selected = agentSelection(toModelRoute(this.options.getModelSelection(input.envelope.conversation)
|
|
2179
2280
|
?? { provider: this.options.provider, model: this.options.model }));
|
|
2180
|
-
const setup = async (agentCtx) => {
|
|
2181
|
-
await this.setupAgent(agentCtx, workspace, presetId, externalPrincipalId(input.envelope.principal), selected, agentPresets);
|
|
2281
|
+
const setup = async (agentCtx, preparedAgent) => {
|
|
2282
|
+
await this.setupAgent(agentCtx, preparedAgent, workspace, presetId, externalPrincipalId(input.envelope.principal), selected, agentPresets);
|
|
2182
2283
|
};
|
|
2183
2284
|
const lease = this.sessionLeases.open({ kind: 'construction', sessionId: String(id),
|
|
2184
2285
|
conversation: input.envelope.conversation, principal: input.envelope.principal,
|
|
@@ -2292,17 +2393,18 @@ export class DshDeliveryRuntime {
|
|
|
2292
2393
|
resumeSessionId: SessionId(binding.sessionId),
|
|
2293
2394
|
signal: control.controller.signal,
|
|
2294
2395
|
agentOptions: { provider: selected.provider, model: selected.model, maxTokens: this.options.maxOutputTokens },
|
|
2295
|
-
setup: async (agentCtx) => {
|
|
2396
|
+
setup: async (agentCtx, preparedAgent) => {
|
|
2296
2397
|
// Restored inbox entries may wake immediately on Session publication.
|
|
2297
2398
|
// Install the background-only gate before that publication boundary.
|
|
2298
|
-
|
|
2399
|
+
const agent = preparedAgent ?? agentCtx.agent;
|
|
2400
|
+
if (agent === undefined)
|
|
2299
2401
|
throw new Error('assistant-delivery: wake Agent missing');
|
|
2300
|
-
releaseFences = this.installScheduledGoalFences(
|
|
2301
|
-
await this.setupAgent(agentCtx, binding.workspace, presetId, input.attestation.principalId, selected, presets, 'background');
|
|
2402
|
+
releaseFences = this.installScheduledGoalFences(agent, input);
|
|
2403
|
+
await this.setupAgent(agentCtx, agent, binding.workspace, presetId, input.attestation.principalId, selected, presets, 'background');
|
|
2302
2404
|
const llm = this.ctx.get('llm');
|
|
2303
2405
|
if (llm === undefined)
|
|
2304
2406
|
throw new Error('assistant-delivery: llm service is required');
|
|
2305
|
-
requireAdapterToolCallProtocol(llm, selected.provider, selected.model, presetId, agentCtx.tools.schemas(agentCtx.agent).length);
|
|
2407
|
+
requireAdapterToolCallProtocol(llm, selected.provider, selected.model, presetId, agentCtx.tools.schemas(preparedAgent ?? agentCtx.agent).length);
|
|
2306
2408
|
},
|
|
2307
2409
|
});
|
|
2308
2410
|
const agent = handle.agent;
|
|
@@ -2414,6 +2516,39 @@ export class DshDeliveryRuntime {
|
|
|
2414
2516
|
input.assertCurrent(agent, phase);
|
|
2415
2517
|
}
|
|
2416
2518
|
installScheduledGoalFences(agent, input) {
|
|
2519
|
+
let pendingGoalStep;
|
|
2520
|
+
const nativeGoalStep = (source) => {
|
|
2521
|
+
const native = this.ctx.get('goals')?.get(agent);
|
|
2522
|
+
const round = source.round;
|
|
2523
|
+
if (source.kind !== 'goal' || String(source.goalId) !== input.native.goalId
|
|
2524
|
+
|| source.revision !== input.native.revision + 1 || typeof round !== 'number' || !Number.isSafeInteger(round) || round < 1
|
|
2525
|
+
|| native === undefined || native.phase !== 'active' || String(native.id) !== input.native.goalId
|
|
2526
|
+
|| native.revision !== source.revision || round !== native.roundsStarted + 1)
|
|
2527
|
+
return undefined;
|
|
2528
|
+
return { turn: 0, step: 0, goalId: String(source.goalId), revision: source.revision, round };
|
|
2529
|
+
};
|
|
2530
|
+
const durableTurnMessages = (turn) => {
|
|
2531
|
+
const events = agent.session.snapshotEvents();
|
|
2532
|
+
const start = events.findLast(event => event.type === 'turn/start');
|
|
2533
|
+
if (start?.type !== 'turn/start' || start.data.turn !== turn
|
|
2534
|
+
|| events.some(event => event.seq > start.seq && event.type === 'turn/end')) {
|
|
2535
|
+
throw new Error('assistant-delivery: exact native goal turn required');
|
|
2536
|
+
}
|
|
2537
|
+
return events.filter(event => event.seq > start.seq && event.type === 'user/message'
|
|
2538
|
+
&& !isScheduledGoalRuntimeContext(event.data.source));
|
|
2539
|
+
};
|
|
2540
|
+
const pendingCurrent = (turn, step) => {
|
|
2541
|
+
const pending = pendingGoalStep;
|
|
2542
|
+
if (pending === undefined || pending.turn !== turn || pending.step !== step)
|
|
2543
|
+
return undefined;
|
|
2544
|
+
const native = this.ctx.get('goals')?.get(agent);
|
|
2545
|
+
// Only bridge the gap before the accepted goal input is durably folded.
|
|
2546
|
+
// Once any input appears, the ordinary snapshot fence must verify it.
|
|
2547
|
+
if (native === undefined || native.phase !== 'active' || String(native.id) !== pending.goalId
|
|
2548
|
+
|| native.revision !== pending.revision || native.roundsStarted !== pending.round - 1)
|
|
2549
|
+
return undefined;
|
|
2550
|
+
return pending;
|
|
2551
|
+
};
|
|
2417
2552
|
const goalTurn = () => {
|
|
2418
2553
|
const events = agent.session.snapshotEvents();
|
|
2419
2554
|
const start = events.findLast(event => event.type === 'turn/start');
|
|
@@ -2439,23 +2574,57 @@ export class DshDeliveryRuntime {
|
|
|
2439
2574
|
catch { }
|
|
2440
2575
|
throw new Error('assistant-delivery: scheduled goal wake authorization changed');
|
|
2441
2576
|
};
|
|
2442
|
-
const
|
|
2443
|
-
|
|
2444
|
-
|
|
2577
|
+
const clearPending = () => { pendingGoalStep = undefined; };
|
|
2578
|
+
input.signal.addEventListener('abort', clearPending, { once: true });
|
|
2579
|
+
const preStep = agent.ctx.on('agent/pre-step', async (payload, next) => {
|
|
2580
|
+
// A pre-step decision is the sole new-Host bridge between the claimed
|
|
2581
|
+
// inbox item and its later durable append. Never carry it into another
|
|
2582
|
+
// boundary, rejection, replacement, or retry.
|
|
2583
|
+
clearPending();
|
|
2584
|
+
const offered = payload.messages.filter(message => !isScheduledGoalRuntimeContext(message.source));
|
|
2585
|
+
const offeredMessage = offered.length === 1 ? structuredClone(offered[0]) : undefined;
|
|
2586
|
+
const decision = await next();
|
|
2587
|
+
if (payload.signal.aborted || input.signal.aborted || decision.kind !== 'enter')
|
|
2588
|
+
return decision;
|
|
2589
|
+
const entered = decision.messages.filter(message => !isScheduledGoalRuntimeContext(message.source));
|
|
2590
|
+
if (offeredMessage === undefined || entered.length !== 1
|
|
2591
|
+
|| typeof offeredMessage.id !== 'string' || offeredMessage.id.length === 0
|
|
2592
|
+
|| !isDeepStrictEqual(entered[0], offeredMessage))
|
|
2593
|
+
return decision;
|
|
2594
|
+
const candidate = nativeGoalStep(entered[0].source);
|
|
2595
|
+
if (candidate === undefined)
|
|
2596
|
+
return decision;
|
|
2597
|
+
pendingGoalStep = { ...candidate, turn: payload.turn, step: payload.step };
|
|
2598
|
+
return decision;
|
|
2599
|
+
}, { prepend: true });
|
|
2600
|
+
const request = agent.ctx.on('agent/request', async (payload, next) => {
|
|
2601
|
+
const assertRequest = () => {
|
|
2602
|
+
payload.signal.throwIfAborted();
|
|
2603
|
+
if (durableTurnMessages(payload.turn).length > 0)
|
|
2604
|
+
goalTurn();
|
|
2605
|
+
else if (pendingCurrent(payload.turn, payload.step) === undefined)
|
|
2606
|
+
throw new Error('assistant-delivery: native goal step not admitted');
|
|
2445
2607
|
this.assertScheduledGoal(input, agent, 'running');
|
|
2446
|
-
}
|
|
2447
|
-
catch {
|
|
2448
|
-
return reject();
|
|
2449
|
-
}
|
|
2450
|
-
const result = await next();
|
|
2608
|
+
};
|
|
2451
2609
|
try {
|
|
2452
|
-
|
|
2453
|
-
|
|
2610
|
+
try {
|
|
2611
|
+
assertRequest();
|
|
2612
|
+
}
|
|
2613
|
+
catch {
|
|
2614
|
+
return reject();
|
|
2615
|
+
}
|
|
2616
|
+
const result = await next();
|
|
2617
|
+
try {
|
|
2618
|
+
assertRequest();
|
|
2619
|
+
}
|
|
2620
|
+
catch {
|
|
2621
|
+
return reject();
|
|
2622
|
+
}
|
|
2623
|
+
return result;
|
|
2454
2624
|
}
|
|
2455
|
-
|
|
2456
|
-
|
|
2625
|
+
finally {
|
|
2626
|
+
clearPending();
|
|
2457
2627
|
}
|
|
2458
|
-
return result;
|
|
2459
2628
|
}, { prepend: true });
|
|
2460
2629
|
const preExecute = agent.ctx.on('tools/pre-execute', async (_payload, next) => {
|
|
2461
2630
|
try {
|
|
@@ -2485,7 +2654,7 @@ export class DshDeliveryRuntime {
|
|
|
2485
2654
|
return 'assistant-delivery: scheduled goal wake authorization changed';
|
|
2486
2655
|
}
|
|
2487
2656
|
});
|
|
2488
|
-
return () => { request(); preExecute(); guard(); };
|
|
2657
|
+
return () => { input.signal.removeEventListener('abort', clearPending); clearPending(); preStep(); request(); preExecute(); guard(); };
|
|
2489
2658
|
}
|
|
2490
2659
|
waitForScheduledGoalTerminal(agent, input, signal) {
|
|
2491
2660
|
return new Promise(resolve => {
|
|
@@ -2988,8 +3157,8 @@ export class DshDeliveryRuntime {
|
|
|
2988
3157
|
model: selected.model,
|
|
2989
3158
|
maxTokens: this.options.maxOutputTokens,
|
|
2990
3159
|
},
|
|
2991
|
-
setup: async (agentCtx) => {
|
|
2992
|
-
await this.setupAgent(agentCtx, binding.workspace, presetId, externalPrincipalId(binding.principal), selected, agentPresets);
|
|
3160
|
+
setup: async (agentCtx, preparedAgent) => {
|
|
3161
|
+
await this.setupAgent(agentCtx, preparedAgent, binding.workspace, presetId, externalPrincipalId(binding.principal), selected, agentPresets);
|
|
2993
3162
|
},
|
|
2994
3163
|
});
|
|
2995
3164
|
resumed = true;
|
|
@@ -3301,6 +3470,8 @@ export class DshDeliveryRuntime {
|
|
|
3301
3470
|
let handle;
|
|
3302
3471
|
let dispatched = false;
|
|
3303
3472
|
let acceptance;
|
|
3473
|
+
let foregroundExecution;
|
|
3474
|
+
let removeAcceptanceRequest;
|
|
3304
3475
|
let acceptanceSucceeded = false;
|
|
3305
3476
|
let goalContinuation;
|
|
3306
3477
|
let goalContinuationQuiescent = true;
|
|
@@ -3329,12 +3500,12 @@ export class DshDeliveryRuntime {
|
|
|
3329
3500
|
handle = await this.resumeAgent({ resumeSessionId: SessionId(binding.sessionId), signal,
|
|
3330
3501
|
agentOptions: { provider: selected.provider, model: selected.model,
|
|
3331
3502
|
maxTokens: this.options.maxOutputTokens },
|
|
3332
|
-
setup: async (agentCtx) => {
|
|
3333
|
-
await this.setupAgent(agentCtx, binding.workspace, presetId, externalPrincipalId(binding.principal), selected, agentPresets);
|
|
3503
|
+
setup: async (agentCtx, preparedAgent) => {
|
|
3504
|
+
await this.setupAgent(agentCtx, preparedAgent, binding.workspace, presetId, externalPrincipalId(binding.principal), selected, agentPresets);
|
|
3334
3505
|
const llm = this.ctx.get('llm');
|
|
3335
3506
|
if (llm === undefined)
|
|
3336
3507
|
throw new Error('assistant-delivery: llm service is required');
|
|
3337
|
-
requireAdapterToolCallProtocol(llm, selected.provider, selected.model, presetId, agentCtx.tools.schemas(agentCtx.agent).length);
|
|
3508
|
+
requireAdapterToolCallProtocol(llm, selected.provider, selected.model, presetId, agentCtx.tools.schemas(preparedAgent ?? agentCtx.agent).length);
|
|
3338
3509
|
} });
|
|
3339
3510
|
const agent = handle.agent;
|
|
3340
3511
|
await this.reconcileNativeFullPermissionReviewer(agent.session);
|
|
@@ -3398,6 +3569,20 @@ export class DshDeliveryRuntime {
|
|
|
3398
3569
|
// host driver is otherwise free to claim its first round at idle.
|
|
3399
3570
|
goalContinuation = this.waitForNativeGoalContinuation(binding, envelope, agent, signal);
|
|
3400
3571
|
acceptance = this.options.prepareForegroundTaskAcceptance(binding, envelope);
|
|
3572
|
+
foregroundExecution = this.options.beginForegroundTaskExecution(binding, envelope);
|
|
3573
|
+
if (acceptance !== undefined || foregroundExecution !== undefined)
|
|
3574
|
+
removeAcceptanceRequest = this.ctx.on('session/event', (session, event) => {
|
|
3575
|
+
if (session !== agent.session || event.type !== 'request/header')
|
|
3576
|
+
return;
|
|
3577
|
+
// `agent/request` exposes the candidate route. The native loop writes
|
|
3578
|
+
// this header only after `llm.prepareCall()` has applied adapter
|
|
3579
|
+
// defaults such as reasoning effort, which is the route actually sent
|
|
3580
|
+
// to the provider.
|
|
3581
|
+
if (foregroundExecution !== undefined)
|
|
3582
|
+
this.options.recordForegroundExecutionModelSelection(foregroundExecution, event.data.header.config);
|
|
3583
|
+
if (acceptance !== undefined)
|
|
3584
|
+
this.options.recordForegroundTaskModelSelection(acceptance, event.data.header.config);
|
|
3585
|
+
});
|
|
3401
3586
|
markDispatching();
|
|
3402
3587
|
// Once the durable marker exists, even a synchronous followup failure is ambiguous:
|
|
3403
3588
|
// implementations may enqueue before throwing, so no retry is safe.
|
|
@@ -3561,18 +3746,28 @@ export class DshDeliveryRuntime {
|
|
|
3561
3746
|
return { outcome: 'not-processed', failureCode: 'agent-turn-incomplete', retryable: false };
|
|
3562
3747
|
}
|
|
3563
3748
|
if (currentEmpty || assembledText.trim() === '') {
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3749
|
+
// A tool-only foreground turn can arm a native goal (typically
|
|
3750
|
+
// `goal_create`); the host round driver then continues with its own
|
|
3751
|
+
// rounds and this turn has no user-facing reply text. It must not be
|
|
3752
|
+
// classified as an empty response, which would dispose the armed
|
|
3753
|
+
// continuation fence and cancel the in-flight goal round.
|
|
3754
|
+
if (output.hadToolCall && goalContinuation?.pinActiveArmedGoal()) {
|
|
3755
|
+
// Fall through to the continuation fence below instead of replying.
|
|
3756
|
+
}
|
|
3757
|
+
else {
|
|
3758
|
+
const hasPartial = bestEffortText.trim() !== '';
|
|
3759
|
+
this.options.replyCommand(binding, envelope.eventId, {
|
|
3760
|
+
text: hasPartial
|
|
3761
|
+
? truncatedAgentReply(bestEffortText, this.options.maxTextBytes)
|
|
3762
|
+
: emptyAgentReply(this.options.maxTextBytes),
|
|
3763
|
+
format: 'markdown',
|
|
3764
|
+
});
|
|
3765
|
+
publishProgress({
|
|
3766
|
+
kind: 'failed',
|
|
3767
|
+
code: hasPartial ? AGENT_OUTPUT_TRUNCATED_CODE : AGENT_EMPTY_RESPONSE_CODE,
|
|
3768
|
+
});
|
|
3769
|
+
return { outcome: 'processed' };
|
|
3770
|
+
}
|
|
3576
3771
|
}
|
|
3577
3772
|
break;
|
|
3578
3773
|
}
|
|
@@ -3580,10 +3775,12 @@ export class DshDeliveryRuntime {
|
|
|
3580
3775
|
// The foreground reply has already crossed its durable reply path.
|
|
3581
3776
|
// Retain the same Agent afterwards only for the native goal driver; its
|
|
3582
3777
|
// unknown result must never turn the original reply into a claim that
|
|
3583
|
-
// the business goal was achieved.
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3778
|
+
// the business goal was achieved. A tool-only turn has no reply text.
|
|
3779
|
+
if (assembledText.trim() !== '') {
|
|
3780
|
+
const learning = await this.options.replyCompletedPreferenceTurn(agent, binding, envelope, { text: assembledText, format: 'markdown' });
|
|
3781
|
+
if (learning === 'unknown') {
|
|
3782
|
+
this.ctx.logger.warn('assistant-delivery: completed-turn preference projection is ambiguous');
|
|
3783
|
+
}
|
|
3587
3784
|
}
|
|
3588
3785
|
if (goalContinuation !== undefined) {
|
|
3589
3786
|
goalContinuationQuiescent = await goalContinuation.settle();
|
|
@@ -3609,6 +3806,21 @@ export class DshDeliveryRuntime {
|
|
|
3609
3806
|
if (causedByDurableIdentity(error)) {
|
|
3610
3807
|
return { outcome: 'not-processed', failureCode: 'agent-identity-mismatch', retryable: false };
|
|
3611
3808
|
}
|
|
3809
|
+
if (errorChainHasName(error, 'SessionFormatUnsupportedError')) {
|
|
3810
|
+
this.ctx.logger.warn(`assistant-delivery: session-format-unsupported for session ${sessionFingerprint(binding.sessionId)}`);
|
|
3811
|
+
try {
|
|
3812
|
+
this.options.replyCommand(binding, envelope.eventId, sessionFormatUnsupportedReply());
|
|
3813
|
+
// The durable reply is the only user effect. Dead-letter the original
|
|
3814
|
+
// inbound event so a legacy format cannot consume the normal retry
|
|
3815
|
+
// budget or produce a second notification.
|
|
3816
|
+
return { outcome: 'not-processed', failureCode: 'session-format-unsupported', retryable: false };
|
|
3817
|
+
}
|
|
3818
|
+
catch {
|
|
3819
|
+
// Do not mark a terminal diagnosis without its owner-authorized,
|
|
3820
|
+
// durable reply; Coordinator may retry this pre-dispatch failure.
|
|
3821
|
+
return { outcome: 'not-processed', failureCode: 'session-format-unsupported-notice-failed', retryable: true };
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3612
3824
|
if (error instanceof ImageCapabilityAdmissionError) {
|
|
3613
3825
|
try {
|
|
3614
3826
|
this.options.replyCommand(binding, envelope.eventId, imageCapabilityReply(error));
|
|
@@ -3632,6 +3844,7 @@ export class DshDeliveryRuntime {
|
|
|
3632
3844
|
}
|
|
3633
3845
|
finally {
|
|
3634
3846
|
removeAbort?.();
|
|
3847
|
+
removeAcceptanceRequest?.();
|
|
3635
3848
|
removeProgress?.();
|
|
3636
3849
|
progressOpen = false;
|
|
3637
3850
|
let disposed = false;
|
|
@@ -3641,6 +3854,16 @@ export class DshDeliveryRuntime {
|
|
|
3641
3854
|
finally {
|
|
3642
3855
|
acceptanceSucceeded = acceptanceSucceeded && disposed && !signal.aborted
|
|
3643
3856
|
&& (goalContinuation?.isQuiescent() ?? true);
|
|
3857
|
+
if (foregroundExecution !== undefined) {
|
|
3858
|
+
try {
|
|
3859
|
+
this.options.completeForegroundTaskExecution(foregroundExecution, {
|
|
3860
|
+
status: acceptanceSucceeded ? 'succeeded' : 'unknown', quiescent: acceptanceSucceeded,
|
|
3861
|
+
});
|
|
3862
|
+
}
|
|
3863
|
+
catch (error) {
|
|
3864
|
+
this.ctx.logger.warn(`assistant-delivery: foreground execution completion failed: ${String(error)}`);
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3644
3867
|
if (acceptance !== undefined) {
|
|
3645
3868
|
try {
|
|
3646
3869
|
await this.options.completeForegroundTaskAcceptance(acceptance, {
|