kiokuko-dsh 0.2.38 → 0.2.39
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/dist/dsh/core/host.js +4 -4
- package/dist/dsh/core/host.js.map +1 -1
- package/dist/dsh/decisions/diagnostics.d.ts +4 -3
- package/dist/dsh/decisions/diagnostics.d.ts.map +1 -1
- package/dist/dsh/decisions/diagnostics.js +2 -2
- package/dist/dsh/decisions/diagnostics.js.map +1 -1
- package/dist/dsh/decisions/intent-assessment.d.ts +10 -4
- package/dist/dsh/decisions/intent-assessment.d.ts.map +1 -1
- package/dist/dsh/decisions/intent-assessment.js +26 -10
- package/dist/dsh/decisions/intent-assessment.js.map +1 -1
- package/dist/dsh/decisions/openai.d.ts.map +1 -1
- package/dist/dsh/decisions/openai.js +3 -0
- package/dist/dsh/decisions/openai.js.map +1 -1
- package/dist/dsh/host-adapter.js +4 -4
- package/dist/dsh/host-adapter.js.map +1 -1
- package/dist/dsh/lisp/sandbox.d.ts +1 -0
- package/dist/dsh/lisp/sandbox.d.ts.map +1 -1
- package/dist/dsh/lisp/sandbox.js +2 -2
- package/dist/dsh/lisp/sandbox.js.map +1 -1
- package/dist/dsh/on-demand-intake.d.ts +19 -4
- package/dist/dsh/on-demand-intake.d.ts.map +1 -1
- package/dist/dsh/on-demand-intake.js +154 -61
- package/dist/dsh/on-demand-intake.js.map +1 -1
- package/dist/dsh/task-startup.d.ts +4 -0
- package/dist/dsh/task-startup.d.ts.map +1 -1
- package/dist/dsh/task-startup.js +8 -2
- package/dist/dsh/task-startup.js.map +1 -1
- package/dist/locale/en.json +1 -1
- package/dist/locale/ja.json +1 -1
- package/dist/locale/ko.json +1 -1
- package/dist/locale/zh-CN.json +1 -1
- package/docs/micro-judge.md +1 -1
- package/docs/on-demand-intake.md +2 -0
- package/lisp/supervisor.mjs +90 -9
- package/package.json +1 -1
|
@@ -17,6 +17,7 @@ import { KIOKUKO_DSH_SOURCE_KIND, isKiokukoDshSource } from './plugin-source.js'
|
|
|
17
17
|
import { taskAllowedOperations, taskCanExecute, } from './task-state.js';
|
|
18
18
|
import { createRequestIntentContext, unresolvedRequestIntent, validateMainModelRequestIntent, requestIntentReferenceIds, requestIntentRequiredReferenceIds, requestIntentIsUnresolved, requestIntentMessageRefs, requestIntentTaskType, RequestIntentSubmissionSchema, } from './decisions/request-intent.js';
|
|
19
19
|
import { memoryOwnerKind, } from './memory-owner.js';
|
|
20
|
+
import { assessmentAllowsPreparation, mayConfirmRequestInterpretation } from './decisions/intent-assessment.js';
|
|
20
21
|
class RequestClarification extends Error {
|
|
21
22
|
assessment;
|
|
22
23
|
interpretation;
|
|
@@ -195,6 +196,7 @@ export class OnDemandIntake {
|
|
|
195
196
|
}
|
|
196
197
|
}
|
|
197
198
|
#turns = new Map();
|
|
199
|
+
#nativeConfirmations = new WeakMap();
|
|
198
200
|
#clarificationContexts = new WeakMap();
|
|
199
201
|
#clarifications = new Map();
|
|
200
202
|
#executions = new WeakMap();
|
|
@@ -249,20 +251,20 @@ export class OnDemandIntake {
|
|
|
249
251
|
let verified;
|
|
250
252
|
state.auxiliaryReceipt = await this.host.recovery?.defer({ source: { sessionId: state.input.agent.session.id, turn: state.input.turn,
|
|
251
253
|
origin: 'intent-assessment', sourceDigest: canonicalContentHash(state.task), contextDigest: digest },
|
|
252
|
-
current: () => this.recoveryCurrent(state) && state.assessment?.digest === digest && canonicalContentHash([state.requestIntentContext, decision]) === digest,
|
|
254
|
+
current: () => this.recoveryCurrent(state) && !this.#clarifications.has(state.session.id) && state.assessment?.digest === digest && canonicalContentHash([state.requestIntentContext, decision]) === digest,
|
|
253
255
|
check: async (signal, attempt) => {
|
|
254
256
|
await this.host.validate({ ...state.input, signal });
|
|
255
|
-
const result = await this.host.assess?.({ ...state.input, signal }, context, decision, attempt);
|
|
257
|
+
const result = await this.host.assess?.({ ...state.input, signal }, context, decision, attempt, this.hasNativeConfirmation(state, context, decision));
|
|
256
258
|
signal.throwIfAborted();
|
|
257
259
|
if (!result)
|
|
258
260
|
return { state: 'rejected', reason: 'assessment_disabled' };
|
|
259
|
-
if (result
|
|
261
|
+
if (assessmentAllowsPreparation(result)) {
|
|
260
262
|
verified = result;
|
|
261
|
-
return { state: 'verified', reason: 'semantic_rechecked' };
|
|
263
|
+
return { state: 'verified', reason: result.outcome === 'user_confirmed' ? 'native_user_confirmation_accepted' : 'semantic_rechecked' };
|
|
262
264
|
}
|
|
263
265
|
return { state: result.semantic === 'contradicted' ? 'rejected' : 'retry_wait', reason: result.semantic === 'contradicted' ? 'semantic_contradicted' : 'assessment_unresolved' };
|
|
264
266
|
}, verified: async () => {
|
|
265
|
-
if (verified && this.recoveryCurrent(state) && state.assessment?.digest === digest && canonicalContentHash([state.requestIntentContext, decision]) === digest) {
|
|
267
|
+
if (verified && this.recoveryCurrent(state) && !this.#clarifications.has(state.session.id) && state.assessment?.digest === digest && canonicalContentHash([state.requestIntentContext, decision]) === digest) {
|
|
266
268
|
state.assessment = { digest, result: verified };
|
|
267
269
|
await this.diagnose(state, { kind: 'assessment', assessment: verified });
|
|
268
270
|
}
|
|
@@ -400,7 +402,7 @@ export class OnDemandIntake {
|
|
|
400
402
|
messageRefs: requestIntentMessageRefs(input.messages, task),
|
|
401
403
|
});
|
|
402
404
|
try {
|
|
403
|
-
await this.host.prepareNativeCoding(input, task, context);
|
|
405
|
+
await this.host.prepareNativeCoding(input, task, context, this.nativeContinuation(input));
|
|
404
406
|
}
|
|
405
407
|
catch (error) {
|
|
406
408
|
input.signal.throwIfAborted();
|
|
@@ -495,6 +497,8 @@ export class OnDemandIntake {
|
|
|
495
497
|
messages: Object.freeze(structuredClone([...input.messages])),
|
|
496
498
|
},
|
|
497
499
|
task: humanTask(input.messages),
|
|
500
|
+
...(this.isClarificationContinuation(input)
|
|
501
|
+
? { clarificationPending: true } : {}),
|
|
498
502
|
humanContents: new Map(humanMessages(input.messages).map((message) => [
|
|
499
503
|
humanId(message),
|
|
500
504
|
canonicalContentHash(message),
|
|
@@ -538,7 +542,7 @@ export class OnDemandIntake {
|
|
|
538
542
|
messageRefs: requestIntentMessageRefs(state.input.messages, state.task),
|
|
539
543
|
});
|
|
540
544
|
this.assertCurrent(state);
|
|
541
|
-
await this.host.prepareNativeCoding?.(state.input, state.task, context);
|
|
545
|
+
await this.host.prepareNativeCoding?.(state.input, state.task, context, this.nativeContinuation(state.input));
|
|
542
546
|
this.assertCurrent(state);
|
|
543
547
|
state.nativeUnowned = true;
|
|
544
548
|
if (context) {
|
|
@@ -555,7 +559,7 @@ export class OnDemandIntake {
|
|
|
555
559
|
const context = await this.host.requestContext?.(state.input, state.task)
|
|
556
560
|
?? createRequestIntentContext({ task: state.task, messageRefs: requestIntentMessageRefs(state.input.messages, state.task) });
|
|
557
561
|
this.assertCurrent(state);
|
|
558
|
-
await this.host.prepareNativeCoding(state.input, state.task, context);
|
|
562
|
+
await this.host.prepareNativeCoding(state.input, state.task, context, this.nativeContinuation(state.input));
|
|
559
563
|
this.assertCurrent(state);
|
|
560
564
|
}
|
|
561
565
|
try {
|
|
@@ -787,6 +791,55 @@ export class OnDemandIntake {
|
|
|
787
791
|
current,
|
|
788
792
|
});
|
|
789
793
|
}
|
|
794
|
+
isClarificationContinuation(input) {
|
|
795
|
+
const messages = humanMessages(input.messages), known = this.#clarificationContexts.get(input.agent);
|
|
796
|
+
if (!known)
|
|
797
|
+
return false;
|
|
798
|
+
const sources = collectClarificationContext(messages.map(message => ({ id: humanId(message), text: humanTask([message]) })), known).sources;
|
|
799
|
+
return messages.some(message => {
|
|
800
|
+
const reply = known.get(humanId(message));
|
|
801
|
+
return reply && canonicalContentHash(reply.sources) === canonicalContentHash(sources);
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
/** Startup preferences are reused only from this exact native confirmation,
|
|
805
|
+
* never by matching a model-written "continue" or quoted answer. */
|
|
806
|
+
nativeContinuation(input) {
|
|
807
|
+
const messages = humanMessages(input.messages);
|
|
808
|
+
const clarification = collectClarificationContext(messages.map(message => ({ id: humanId(message), text: humanTask([message]) })), this.#clarificationContexts.get(input.agent) ?? new Map());
|
|
809
|
+
for (const message of messages) {
|
|
810
|
+
const receipt = this.#nativeConfirmations.get(input.agent)?.get(humanId(message));
|
|
811
|
+
if (receipt && receipt.session === input.agent.session && receipt.originalTurn < input.turn &&
|
|
812
|
+
receipt.contentDigest === canonicalContentHash(humanTask([message])) &&
|
|
813
|
+
receipt.sourcesDigest === canonicalContentHash(clarification.sources))
|
|
814
|
+
return { turn: receipt.originalTurn, task: receipt.originalTask };
|
|
815
|
+
}
|
|
816
|
+
return undefined;
|
|
817
|
+
}
|
|
818
|
+
confirmationScope(context) {
|
|
819
|
+
// Eligible confirmations adopt no proposals. Fresh unselected assistant
|
|
820
|
+
// prose must not invalidate the human answer; selected refs are bound in
|
|
821
|
+
// confirmationCandidate and still cannot enter this path.
|
|
822
|
+
return canonicalContentHash({ activeTask: context.activeTask ?? null,
|
|
823
|
+
activeGoal: context.activeGoal ?? null });
|
|
824
|
+
}
|
|
825
|
+
confirmationCandidate(candidate) {
|
|
826
|
+
return canonicalContentHash([candidate.relation, candidate.action, candidate.changeScope,
|
|
827
|
+
candidate.taskOperation ?? null, candidate.evidence.proposalRefs]);
|
|
828
|
+
}
|
|
829
|
+
hasNativeConfirmation(state, context, candidate) {
|
|
830
|
+
if (!mayConfirmRequestInterpretation(candidate))
|
|
831
|
+
return false;
|
|
832
|
+
const messages = humanMessages(state.input.messages);
|
|
833
|
+
const clarification = collectClarificationContext(messages.map(message => ({ id: humanId(message), text: humanTask([message]) })), this.#clarificationContexts.get(state.input.agent) ?? new Map());
|
|
834
|
+
return messages.some(message => {
|
|
835
|
+
const receipt = this.#nativeConfirmations.get(state.input.agent)?.get(humanId(message));
|
|
836
|
+
return !!receipt && receipt.session === state.input.agent.session &&
|
|
837
|
+
receipt.contentDigest === canonicalContentHash(humanTask([message])) &&
|
|
838
|
+
receipt.sourcesDigest === canonicalContentHash(clarification.sources) &&
|
|
839
|
+
receipt.scopeDigest === this.confirmationScope(context) &&
|
|
840
|
+
receipt.candidateDigest === this.confirmationCandidate(candidate);
|
|
841
|
+
});
|
|
842
|
+
}
|
|
790
843
|
async assess(state, decision) {
|
|
791
844
|
if (!this.host.assess ||
|
|
792
845
|
decision?.action === 'answer' ||
|
|
@@ -802,7 +855,7 @@ export class OnDemandIntake {
|
|
|
802
855
|
const started = performance.now(), deadline = AbortSignal.timeout(1500);
|
|
803
856
|
const signal = AbortSignal.any([state.input.signal, deadline]);
|
|
804
857
|
try {
|
|
805
|
-
result = await abortable(this.host.assess({ ...state.input, signal }, context, decision), signal);
|
|
858
|
+
result = await abortable(this.host.assess({ ...state.input, signal }, context, decision, undefined, this.hasNativeConfirmation(state, context, decision)), signal);
|
|
806
859
|
}
|
|
807
860
|
catch {
|
|
808
861
|
state.input.signal.throwIfAborted();
|
|
@@ -818,10 +871,10 @@ export class OnDemandIntake {
|
|
|
818
871
|
throw new Error('Assessment source changed');
|
|
819
872
|
state.assessment = { digest, result };
|
|
820
873
|
await this.diagnose(state, { kind: 'assessment', assessment: result });
|
|
821
|
-
if (result
|
|
874
|
+
if (!assessmentAllowsPreparation(result))
|
|
822
875
|
await this.deferAssessment(state, context, decision, digest);
|
|
823
876
|
}
|
|
824
|
-
return state.assessment.result
|
|
877
|
+
return assessmentAllowsPreparation(state.assessment.result);
|
|
825
878
|
}
|
|
826
879
|
async prepare(state, taskType, signal, requestIntent, explicitIntent = false) {
|
|
827
880
|
this.assertCurrent(state);
|
|
@@ -909,6 +962,7 @@ export class OnDemandIntake {
|
|
|
909
962
|
requestIntent: state.requestIntent ?? null,
|
|
910
963
|
});
|
|
911
964
|
state.status = 'prepared';
|
|
965
|
+
delete state.clarificationPending;
|
|
912
966
|
this.#clarifications.delete(state.input.agent.session.id);
|
|
913
967
|
delete state.confirmationResume;
|
|
914
968
|
})();
|
|
@@ -996,17 +1050,22 @@ export class OnDemandIntake {
|
|
|
996
1050
|
reason.message.startsWith('The requested change scope');
|
|
997
1051
|
const target = !!executionClarification(state.task, decision, context);
|
|
998
1052
|
const transition = context?.activeTask && !taskCanExecute(context.activeTask);
|
|
999
|
-
const
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1053
|
+
const confirmCurrent = reason.assessment && state.assessment?.result.semantic === 'insufficient' &&
|
|
1054
|
+
!!decision && mayConfirmRequestInterpretation(decision) && !(decision.relation === 'same_scope_continuation' && context?.proposals.length) && !target && !scope && !transition;
|
|
1055
|
+
const confirmLabel = 'この依頼の対象・操作・制約のまま進める';
|
|
1056
|
+
const question = confirmCurrent
|
|
1057
|
+
? `次の依頼について、対象と操作を判定しきれませんでした。\n\n${state.task}\n\n予定している操作: ${{ answer: '回答', research: '調査', verify: '検証', change: decision.changeScope === 'document' ? '文書の編集' : 'コード・テスト・設定の変更', operate: '操作', unknown: '未確定' }[decision.action]}。\nこの依頼に書かれた対象・操作・制約のまま進めてよいですか?違う場合は、具体的な対象と希望する操作を訂正してください。`
|
|
1058
|
+
: reason.assessment
|
|
1059
|
+
? target ? '依頼の対象を特定できませんでした。変更・確認する具体的なファイルや項目と、希望する操作を教えてください。' : '依頼の対象・操作・制約を判定しきれませんでした。今回の具体的な対象と希望する操作、以前の指示を変更する範囲を教えてください。'
|
|
1060
|
+
: transition
|
|
1061
|
+
? `現在のタスクは ${context.activeTask.state} です。続けるために、どの操作を希望しますか?(${taskAllowedOperations(context.activeTask).join(' / ')})`
|
|
1062
|
+
: target
|
|
1063
|
+
? 'どの具体的なファイル・項目・相手・操作を対象にしますか?'
|
|
1064
|
+
: scope
|
|
1065
|
+
? '変更するのは文章だけですか、それともコード・テスト・設定も含みますか?'
|
|
1066
|
+
: decision?.action === 'unknown'
|
|
1067
|
+
? 'その対象に、具体的に何をしてほしいですか?'
|
|
1068
|
+
: null;
|
|
1010
1069
|
// Internal relationship classification belongs to the model. An unresolved
|
|
1011
1070
|
// classifier is not evidence that the human omitted an action or target.
|
|
1012
1071
|
if (reason.interpretation || question === null)
|
|
@@ -1041,34 +1100,6 @@ export class OnDemandIntake {
|
|
|
1041
1100
|
detail: '原文照合または有限領域検査を利用できません。実行は準備されていません。',
|
|
1042
1101
|
};
|
|
1043
1102
|
}
|
|
1044
|
-
// An inconclusive detector is not evidence that the human omitted a target.
|
|
1045
|
-
// Return the submitted interpretation for repair; keep preparation denied.
|
|
1046
|
-
if (reason.assessment &&
|
|
1047
|
-
state.assessment?.result.semantic === 'insufficient' &&
|
|
1048
|
-
reason.candidate) {
|
|
1049
|
-
const assessment = state.assessment.result;
|
|
1050
|
-
const issues = assessment.semanticIssues ?? [];
|
|
1051
|
-
const unresolved = assessment.semanticChecks?.filter(check => issues.includes(check.criterion)) ?? [];
|
|
1052
|
-
const providerUncertain = issues.length > 0 && unresolved.length === issues.length && unresolved.every(check => check.status === 'abstained' && (check.reason === 'uncertain' || check.reason === 'tie'));
|
|
1053
|
-
return {
|
|
1054
|
-
prepared: false,
|
|
1055
|
-
status: 'assessment_rejected',
|
|
1056
|
-
semantic: state.assessment.result.semantic,
|
|
1057
|
-
semanticIssues: state.assessment.result.semanticIssues ?? [],
|
|
1058
|
-
semanticChecks: assessment.semanticChecks ?? [],
|
|
1059
|
-
originalTask: state.task,
|
|
1060
|
-
requestIntent: reason.candidate,
|
|
1061
|
-
currentReferences: context
|
|
1062
|
-
? requestIntentRequiredReferenceIds(context)
|
|
1063
|
-
: [],
|
|
1064
|
-
availableReferences: context ? requestIntentReferenceIds(context) : [],
|
|
1065
|
-
permissions: 'unchanged_native_policy',
|
|
1066
|
-
nextAction: providerUncertain ? 'report_assessment_uncertainty' : 'revise_request_intent_or_report_conflict',
|
|
1067
|
-
detail: providerUncertain
|
|
1068
|
-
? 'The independent provider abstained because of confidence or tied choices. This does not establish ambiguity in the user request or an incorrect candidate. Preserve the request and candidate and report the unresolved provider assessment. Do not resubmit an identical candidate, change its meaning merely to obtain acceptance, ask the human for internal classification fields, or bypass preparation. No work is prepared.'
|
|
1069
|
-
: 'The independent comparison did not establish the submitted candidate. Re-read the captured request, answers, active task and same-session plan. Cite an existing proposal only when the request adopts it. Correct the candidate or report the specific unresolved comparison; do not repeat an identical submission or ask the human to supply internal classification fields. Ask a concrete question only if the source itself leaves an action or target ambiguous. No work is prepared.',
|
|
1070
|
-
};
|
|
1071
|
-
}
|
|
1072
1103
|
const replies = this.#clarificationContexts.get(state.input.agent) ??
|
|
1073
1104
|
new Map();
|
|
1074
1105
|
const clarification = collectClarificationContext(humanMessages(state.input.messages).map((message) => ({
|
|
@@ -1156,6 +1187,7 @@ export class OnDemandIntake {
|
|
|
1156
1187
|
result: result,
|
|
1157
1188
|
operation: undefined,
|
|
1158
1189
|
};
|
|
1190
|
+
state.clarificationPending = true;
|
|
1159
1191
|
this.#clarifications.set(sessionId, pending);
|
|
1160
1192
|
await this.diagnose(state, {
|
|
1161
1193
|
kind: 'question',
|
|
@@ -1179,7 +1211,7 @@ export class OnDemandIntake {
|
|
|
1179
1211
|
{
|
|
1180
1212
|
id,
|
|
1181
1213
|
question,
|
|
1182
|
-
...(scope && !target && !transition
|
|
1214
|
+
...(confirmCurrent ? { options: [{ label: confirmLabel }, { label: '対象または操作を訂正する' }] } : scope && !target && !transition
|
|
1183
1215
|
? {
|
|
1184
1216
|
options: [
|
|
1185
1217
|
{ label: '文章のみ' },
|
|
@@ -1223,24 +1255,62 @@ export class OnDemandIntake {
|
|
|
1223
1255
|
};
|
|
1224
1256
|
return pending.result;
|
|
1225
1257
|
}
|
|
1226
|
-
|
|
1258
|
+
let answer = response.answers[0];
|
|
1259
|
+
let answerQuestionId = id, answerQuestion = question;
|
|
1260
|
+
if (confirmCurrent && answer?.id === id && !answer.custom?.trim() &&
|
|
1261
|
+
answer.selected.length === 1 && answer.selected[0] === '対象または操作を訂正する') {
|
|
1262
|
+
answerQuestionId = `${id}-correction`;
|
|
1263
|
+
answerQuestion = `次の依頼について、訂正後の具体的な対象(ファイル・項目など)と希望する操作を入力してください。変更しない範囲もあれば含めてください。\n\n${state.task}`;
|
|
1264
|
+
await this.diagnose(state, { kind: 'question', questionRef: answerQuestionId, reason: 'clarification_required' });
|
|
1265
|
+
const corrected = await abortable(this.host.questions.ask({ agent: state.input.agent, signal,
|
|
1266
|
+
questions: [{ id: answerQuestionId, question: answerQuestion }] }), signal);
|
|
1267
|
+
signal.throwIfAborted();
|
|
1268
|
+
this.assertCurrent(state);
|
|
1269
|
+
if (this.#clarifications.get(sessionId) !== pending || pending.status !== 'waiting')
|
|
1270
|
+
return result;
|
|
1271
|
+
await this.host.validate(state.input);
|
|
1272
|
+
const afterCorrection = this.host.requestContext
|
|
1273
|
+
? await this.host.requestContext(state.input, state.task)
|
|
1274
|
+
: (await this.host.interpret?.(state.input, state.task))?.context;
|
|
1275
|
+
signal.throwIfAborted();
|
|
1276
|
+
this.assertCurrent(state);
|
|
1277
|
+
if (context && (!afterCorrection || canonicalContentHash(afterCorrection) !== canonicalContentHash(context))) {
|
|
1278
|
+
if (afterCorrection) {
|
|
1279
|
+
state.requestIntentContext = afterCorrection;
|
|
1280
|
+
state.requestIntent = unresolvedRequestIntent(afterCorrection, 'clarification_context_changed');
|
|
1281
|
+
state.requestIntentContextDigest = canonicalContentHash(afterCorrection);
|
|
1282
|
+
}
|
|
1283
|
+
pending.result = { ...result, status: 'clarification_context_changed',
|
|
1284
|
+
detail: 'The source changed while awaiting the correction. No answer was queued; refresh the current request.' };
|
|
1285
|
+
return pending.result;
|
|
1286
|
+
}
|
|
1287
|
+
answer = corrected.answers[0];
|
|
1288
|
+
}
|
|
1227
1289
|
const value = answer?.custom?.trim() || answer?.selected[0]?.trim();
|
|
1228
|
-
if (answer?.id !==
|
|
1290
|
+
if (answer?.id !== answerQuestionId || !value || value.length > 8192)
|
|
1229
1291
|
return result;
|
|
1230
|
-
// Preserve
|
|
1231
|
-
//
|
|
1232
|
-
//
|
|
1292
|
+
// Preserve native sources and answers once, then recapture the reply.
|
|
1293
|
+
// Only the exact affirmative selection below creates a scoped receipt;
|
|
1294
|
+
// a free-text correction is fresh user evidence, never an admission.
|
|
1233
1295
|
const answerMessageId = randomUUID();
|
|
1234
1296
|
const reply = clarificationReply(clarification, {
|
|
1235
1297
|
id: answerMessageId,
|
|
1236
|
-
questionId:
|
|
1237
|
-
question,
|
|
1298
|
+
questionId: answerQuestionId,
|
|
1299
|
+
question: answerQuestion,
|
|
1238
1300
|
value,
|
|
1239
1301
|
scope: answerScope,
|
|
1240
1302
|
});
|
|
1241
1303
|
const content = reply.content;
|
|
1242
1304
|
signal.throwIfAborted();
|
|
1243
1305
|
this.assertCurrent(state);
|
|
1306
|
+
if (confirmCurrent && answerQuestionId === id && decision && context && !answer.custom?.trim() &&
|
|
1307
|
+
answer.selected.length === 1 && answer.selected[0] === confirmLabel) {
|
|
1308
|
+
const receipts = this.#nativeConfirmations.get(state.input.agent) ?? new Map();
|
|
1309
|
+
receipts.set(answerMessageId, { session: state.input.agent.session, originalTurn: state.input.turn, originalTask: state.task,
|
|
1310
|
+
contentDigest: canonicalContentHash(content), sourcesDigest: canonicalContentHash(clarification.sources),
|
|
1311
|
+
scopeDigest: this.confirmationScope(context), candidateDigest: this.confirmationCandidate(decision) });
|
|
1312
|
+
this.#nativeConfirmations.set(state.input.agent, receipts);
|
|
1313
|
+
}
|
|
1244
1314
|
replies.set(answerMessageId, reply.context);
|
|
1245
1315
|
this.#clarificationContexts.set(state.input.agent, replies);
|
|
1246
1316
|
state.input.agent.followup({
|
|
@@ -1251,12 +1321,14 @@ export class OnDemandIntake {
|
|
|
1251
1321
|
});
|
|
1252
1322
|
await this.diagnose(state, {
|
|
1253
1323
|
kind: 'answer',
|
|
1254
|
-
questionRef:
|
|
1324
|
+
questionRef: answerQuestionId,
|
|
1255
1325
|
answerRef: answerMessageId,
|
|
1256
1326
|
});
|
|
1257
1327
|
pending.status = 'queued';
|
|
1258
1328
|
pending.result = {
|
|
1259
1329
|
...result,
|
|
1330
|
+
questionId: answerQuestionId,
|
|
1331
|
+
question: answerQuestion,
|
|
1260
1332
|
status: 'clarification_answer_queued',
|
|
1261
1333
|
answer: value,
|
|
1262
1334
|
answerRef: answerMessageId,
|
|
@@ -1357,7 +1429,10 @@ export class OnDemandIntake {
|
|
|
1357
1429
|
if (prior.relation !== bound.relation || prior.action !== bound.action) {
|
|
1358
1430
|
throw new Error('Main-model interpretation conflicts with the configured decision provider');
|
|
1359
1431
|
}
|
|
1360
|
-
|
|
1432
|
+
// The explicit submission is a current main-model interpretation, even
|
|
1433
|
+
// when its classification agrees with earlier provider extraction.
|
|
1434
|
+
// Preserve the explicit submission's provenance; this is not user confirmation.
|
|
1435
|
+
return bound;
|
|
1361
1436
|
}
|
|
1362
1437
|
return bound;
|
|
1363
1438
|
}
|
|
@@ -1533,6 +1608,7 @@ export class OnDemandIntake {
|
|
|
1533
1608
|
return (!this.#stopped &&
|
|
1534
1609
|
!execution.signal.aborted &&
|
|
1535
1610
|
UNOWNED_NATIVE_TOOLS.has(execution.name) &&
|
|
1611
|
+
(!state?.clarificationPending || NATIVE_EVIDENCE_READS.has(execution.name) || execution.name === 'skill') &&
|
|
1536
1612
|
execution.parent === undefined &&
|
|
1537
1613
|
!!state &&
|
|
1538
1614
|
state.nativeUnowned === true &&
|
|
@@ -1543,9 +1619,11 @@ export class OnDemandIntake {
|
|
|
1543
1619
|
proof.execute === this.definition(execution)?.execute &&
|
|
1544
1620
|
proof.nativeUnownedDigest === canonicalContentHash(execution.arguments));
|
|
1545
1621
|
}
|
|
1546
|
-
/**
|
|
1622
|
+
/** Ordinary native Bash follows native policy; an issued clarification cannot be bypassed by switching tools. */
|
|
1547
1623
|
nativeCommand(execution) {
|
|
1548
1624
|
const state = this.bound(execution), actual = this.definition(execution), proof = this.#executions.get(execution);
|
|
1625
|
+
if (state?.clarificationPending)
|
|
1626
|
+
return false;
|
|
1549
1627
|
if (!this.options.onlyWhenDeferred &&
|
|
1550
1628
|
!this.#stopped &&
|
|
1551
1629
|
actual &&
|
|
@@ -1576,6 +1654,15 @@ export class OnDemandIntake {
|
|
|
1576
1654
|
proof.execute === actual.execute &&
|
|
1577
1655
|
proof.nativeCommandDigest === canonicalContentHash(execution.arguments)));
|
|
1578
1656
|
}
|
|
1657
|
+
/** Known observational/cancellation Lisp contracts stay usable while waiting.
|
|
1658
|
+
* Live evaluation is effect-capable; do not inspect or guess its code string. */
|
|
1659
|
+
pendingLispEffect(execution) {
|
|
1660
|
+
if (['lisp_status', 'lisp_hot_status', 'lisp_cancel'].includes(execution.name))
|
|
1661
|
+
return false;
|
|
1662
|
+
const args = execution.arguments;
|
|
1663
|
+
return !(execution.name === 'lisp_inspect' && execution.parent === undefined &&
|
|
1664
|
+
typeof args?.resultOperationId === 'string' && args.ref === undefined);
|
|
1665
|
+
}
|
|
1579
1666
|
/** Delegate real root Lisp tools to their own effect/lease/approval authority. */
|
|
1580
1667
|
nativeLisp(execution) {
|
|
1581
1668
|
const state = this.bound(execution), actual = this.definition(execution), proof = this.#executions.get(execution);
|
|
@@ -1587,6 +1674,7 @@ export class OnDemandIntake {
|
|
|
1587
1674
|
state.status !== 'answer' ||
|
|
1588
1675
|
state.input.signal.aborted ||
|
|
1589
1676
|
!actual ||
|
|
1677
|
+
(state.clarificationPending && this.pendingLispEffect(execution)) ||
|
|
1590
1678
|
!isLispImplementation(actual.execute) ||
|
|
1591
1679
|
(execution.agent && this.host.nativeChild?.(execution.agent)))
|
|
1592
1680
|
return false;
|
|
@@ -1861,6 +1949,11 @@ export class OnDemandIntake {
|
|
|
1861
1949
|
}
|
|
1862
1950
|
try {
|
|
1863
1951
|
this.assertCurrent(state);
|
|
1952
|
+
if (state.clarificationPending && (execution.name === 'bash' || isLispNativeCommand(execution, actual.execute) ||
|
|
1953
|
+
(isLispImplementation(actual.execute) && this.pendingLispEffect(execution)) ||
|
|
1954
|
+
(state.nativeUnowned && UNOWNED_NATIVE_TOOLS.has(execution.name) &&
|
|
1955
|
+
!NATIVE_EVIDENCE_READS.has(execution.name) && execution.name !== 'skill')))
|
|
1956
|
+
return denied('The current request is awaiting clarification. Native commands cannot bypass that question; resolve it and prepare the exact request first.');
|
|
1864
1957
|
if (state.confirmationResume &&
|
|
1865
1958
|
execution.name !== TASK_PREPARE_TOOL &&
|
|
1866
1959
|
this.host.ready(state.input.agent, state.input.turn))
|