kiokuko-dsh 0.2.13 → 0.2.15
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.d.ts.map +1 -1
- package/dist/dsh/core/host.js +6 -3
- package/dist/dsh/core/host.js.map +1 -1
- package/dist/dsh/core/tasks.d.ts.map +1 -1
- package/dist/dsh/core/tasks.js +4 -4
- package/dist/dsh/core/tasks.js.map +1 -1
- package/dist/dsh/decisions/request-intent.d.ts +2 -0
- package/dist/dsh/decisions/request-intent.d.ts.map +1 -1
- package/dist/dsh/decisions/request-intent.js +16 -7
- package/dist/dsh/decisions/request-intent.js.map +1 -1
- package/dist/dsh/host-adapter/admission.d.ts.map +1 -1
- package/dist/dsh/host-adapter/admission.js +14 -1
- package/dist/dsh/host-adapter/admission.js.map +1 -1
- package/dist/dsh/host-adapter/lifecycle.d.ts +2 -1
- package/dist/dsh/host-adapter/lifecycle.d.ts.map +1 -1
- package/dist/dsh/host-adapter/lifecycle.js +2 -0
- package/dist/dsh/host-adapter/lifecycle.js.map +1 -1
- package/dist/dsh/host-adapter.d.ts.map +1 -1
- package/dist/dsh/host-adapter.js +5 -1
- package/dist/dsh/host-adapter.js.map +1 -1
- package/dist/dsh/on-demand-intake.d.ts +6 -0
- package/dist/dsh/on-demand-intake.d.ts.map +1 -1
- package/dist/dsh/on-demand-intake.js +239 -72
- package/dist/dsh/on-demand-intake.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/on-demand-intake.md +10 -0
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { abortable } from './boundary-worker.js';
|
|
1
2
|
import { isLispImplementation, isLispNativeCommand, withRootLispAdmission, rememberLispConversation } from './lisp-tool-identity.js';
|
|
2
3
|
import { ExecutionSelectionPending } from './execution-pending.js';
|
|
3
4
|
import { executionClarification } from './intake-clarification.js';
|
|
@@ -6,9 +7,11 @@ import { z } from 'zod';
|
|
|
6
7
|
import { TASK_TYPES } from '../akinator/types.js';
|
|
7
8
|
import { canonicalContentHash } from '../serialization/validate.js';
|
|
8
9
|
import { KIOKUKO_DSH_SOURCE_KIND } from './plugin-source.js';
|
|
9
|
-
import { taskAllowedOperations } from './task-state.js';
|
|
10
|
-
import { createRequestIntentContext, unresolvedRequestIntent, validateMainModelRequestIntent, requestIntentReferenceIds, requestIntentMessageRefs, requestIntentTaskType, RequestIntentSubmissionSchema } from './decisions/request-intent.js';
|
|
10
|
+
import { taskAllowedOperations, taskCanExecute } from './task-state.js';
|
|
11
|
+
import { createRequestIntentContext, unresolvedRequestIntent, validateMainModelRequestIntent, requestIntentReferenceIds, requestIntentRequiredReferenceIds, requestIntentIsUnresolved, requestIntentMessageRefs, requestIntentTaskType, RequestIntentSubmissionSchema } from './decisions/request-intent.js';
|
|
11
12
|
import { memoryOwnerKind } from './memory-owner.js';
|
|
13
|
+
class RequestClarification extends Error {
|
|
14
|
+
}
|
|
12
15
|
export const TASK_PREPARE_TOOL = 'prepare_requested_work';
|
|
13
16
|
export const ON_DEMAND_GUIDANCE = 'The original user request may be answered now without selecting a task category. Ordinary native Bash needs no memory, Goal or prepare_requested_work round trip, including when optional workflow metadata exists; native permissions and human Stop remain authoritative. Registered root Lisp tools delegate to the Lisp runtime without an optional preparation round trip; foreground processes use native Bash permissions, sandbox, approval and Stop. Explicitly prepared or protected workflow effects retain their task, Plan and coding-choice checks. Owned or explicitly enrolled work retains its authority checks. Preserve the original utterance, every requested action, negation, quotation, target uncertainty, and current-session references; never replace it with a model rewrite. Use only the supplied active Goal and proposal references from this exact native session, never another chat. Classify relation as new, same_scope_continuation, scope_change, or unknown, and action as answer, research, verify, change, operate, or unknown; do not infer either from a fixed phrase or tool name. “Check how it behaves” is verification/research even when the next tool is Bash. This interpretation grants no native permission. When the decision provider is unavailable, abstains, or returns unknown, interpret the same context in the main model; ask the user only if the action or referenced target remains unclear, and preserve active work while waiting. Native read, glob, grep, web_search and web_fetch demands can gather evidence without preparation. Do not call prepare_requested_work merely to read a plan or inspect files. prepare_requested_work accepts either the legacy taskType alone or a requestIntent with exact evidenceRefs from the supplied context. taskOperation is optional and describes a transition of an existing activeTask, not starting the requested work itself. Omit taskOperation when activeTask is null or the request does not ask for a state transition; it never accepts a rewritten task or target and never changes native permissions. Installed Skill reads need no preparation and do not activate execution. Do not prepare work merely to load a skill. Plan, Goal, cancellation, memory-owner and execution ownership still apply. If only run_code is exposed, use the exact preparation-only carrier returned by this tool contract; the host intercepts it without evaluating code.';
|
|
14
17
|
const ACTION_TYPES = TASK_TYPES.filter(type => type !== 'chat');
|
|
@@ -35,7 +38,7 @@ function eventTurn(event) {
|
|
|
35
38
|
}
|
|
36
39
|
/** Tool names select advice only; admission and native permissions remain authoritative. */
|
|
37
40
|
function automaticPreparationType(intent, requestIntent, name) {
|
|
38
|
-
if (requestIntent
|
|
41
|
+
if (requestIntent !== undefined && requestIntentIsUnresolved(requestIntent)) {
|
|
39
42
|
// Gathering evidence grants no change/process authority. An active Goal
|
|
40
43
|
// still requires its reference to be resolved before opening another run.
|
|
41
44
|
return ['read', 'glob', 'grep', 'web_search', 'web_fetch'].includes(name) ? 'research' : undefined;
|
|
@@ -49,6 +52,7 @@ function automaticPreparationType(intent, requestIntent, name) {
|
|
|
49
52
|
return intent.taskType;
|
|
50
53
|
return ['read', 'glob', 'grep', 'web_search', 'web_fetch'].includes(name) ? 'research' : undefined;
|
|
51
54
|
}
|
|
55
|
+
const NATIVE_EVIDENCE_READS = new Set(['read', 'glob', 'grep', 'web_search', 'web_fetch']);
|
|
52
56
|
const UNOWNED_NATIVE_TOOLS = new Set(['bash', 'read', 'glob', 'grep', 'web_search', 'web_fetch', 'skill']);
|
|
53
57
|
function denied(reason) { return { kind: 'deny', reason }; }
|
|
54
58
|
/** A protocol carrier, not JavaScript evaluation. Only this exact JSON-literal call is recognized. */
|
|
@@ -79,6 +83,7 @@ export class OnDemandIntake {
|
|
|
79
83
|
host;
|
|
80
84
|
options;
|
|
81
85
|
#turns = new Map();
|
|
86
|
+
#clarifications = new Map();
|
|
82
87
|
#executions = new WeakMap();
|
|
83
88
|
#disposers = [];
|
|
84
89
|
#pending = new Set();
|
|
@@ -260,6 +265,10 @@ export class OnDemandIntake {
|
|
|
260
265
|
catch (error) {
|
|
261
266
|
input.signal.throwIfAborted();
|
|
262
267
|
}
|
|
268
|
+
const pendingClarification = this.#clarifications.get(sessionId);
|
|
269
|
+
if (pendingClarification?.agent === input.agent && state.requestIntent && !requestIntentIsUnresolved(state.requestIntent)
|
|
270
|
+
&& !executionClarification(state.task, state.requestIntent, state.requestIntentContext))
|
|
271
|
+
this.#clarifications.delete(sessionId);
|
|
263
272
|
state.memoryOwner = memoryOwnerKind(state.requestIntent, state.intent.taskType);
|
|
264
273
|
this.assertCurrent(state);
|
|
265
274
|
if (previous?.input.turn !== input.turn)
|
|
@@ -288,7 +297,7 @@ export class OnDemandIntake {
|
|
|
288
297
|
return false;
|
|
289
298
|
}
|
|
290
299
|
const canContinue = state.requestIntent?.relation === 'same_scope_continuation'
|
|
291
|
-
&& state.requestIntent
|
|
300
|
+
&& !requestIntentIsUnresolved(state.requestIntent) && state.requestIntent.action !== 'answer';
|
|
292
301
|
if (previous?.input.turn !== input.turn && state.task && canContinue
|
|
293
302
|
&& await this.host.existing(input, state.requestIntent)) {
|
|
294
303
|
// Persisted ownership is only an acknowledgement when this exact native
|
|
@@ -302,13 +311,15 @@ export class OnDemandIntake {
|
|
|
302
311
|
// Eager mode still binds known work before generation. Classifier uncertainty
|
|
303
312
|
// instead reaches ordinary reasoning, with no execution grant or purpose UI.
|
|
304
313
|
const requestIntentUnresolved = state.requestIntent !== undefined
|
|
305
|
-
&& (state.requestIntent
|
|
314
|
+
&& requestIntentIsUnresolved(state.requestIntent);
|
|
306
315
|
const requestAsAnswer = state.requestIntent?.action === 'answer';
|
|
307
316
|
const knownIntent = this.host.interpret === undefined
|
|
308
317
|
? !state.intent.deferInference && state.intent.taskType !== undefined && state.intent.taskType !== 'chat'
|
|
309
318
|
: state.requestIntent !== undefined && !requestIntentUnresolved && !requestAsAnswer
|
|
310
319
|
&& state.requestIntent.relation !== 'unknown' && state.requestIntent.action !== 'unknown';
|
|
311
|
-
|
|
320
|
+
const missingExecutionDetail = executionClarification(state.task, state.requestIntent, state.requestIntentContext)
|
|
321
|
+
|| state.requestIntentContext?.activeTask && !taskCanExecute(state.requestIntentContext.activeTask);
|
|
322
|
+
if (this.options.onlyWhenDeferred && knownIntent && !missingExecutionDetail) {
|
|
312
323
|
this.#turns.delete(sessionId);
|
|
313
324
|
state.status = 'closed';
|
|
314
325
|
controller.abort(new Error('Known intent continues through eager admission'));
|
|
@@ -375,9 +386,19 @@ export class OnDemandIntake {
|
|
|
375
386
|
if (state.selectionPending)
|
|
376
387
|
throw new ExecutionSelectionPending();
|
|
377
388
|
const resolved = requestIntent ?? state.requestIntent;
|
|
389
|
+
if (resolved?.action === 'change' && resolved.changeScope === 'unknown') {
|
|
390
|
+
throw new RequestClarification('The requested change scope is unresolved. Specify implementation or document using the current request evidence before preparing work.');
|
|
391
|
+
}
|
|
392
|
+
if (resolved && requestIntentIsUnresolved(resolved) && (state.requestIntentContext?.activeGoal || state.requestIntentContext?.activeTask)) {
|
|
393
|
+
throw new RequestClarification('Clarify the request before preparing work');
|
|
394
|
+
}
|
|
395
|
+
const activeTask = state.requestIntentContext?.activeTask;
|
|
396
|
+
if (activeTask && resolved?.relation === 'same_scope_continuation' && !taskCanExecute(activeTask) && !resolved.taskOperation) {
|
|
397
|
+
throw new RequestClarification('The current task requires an explicit user-selected state operation before execution.');
|
|
398
|
+
}
|
|
378
399
|
const clarification = executionClarification(state.task, resolved, state.requestIntentContext);
|
|
379
400
|
if (clarification)
|
|
380
|
-
throw new
|
|
401
|
+
throw new RequestClarification(clarification);
|
|
381
402
|
if (!ACTION_TYPES.includes(taskType))
|
|
382
403
|
throw new Error('Answer conversational questions directly; prepare_requested_work is for requested work');
|
|
383
404
|
if (state.status === 'prepared' && state.confirmationResume && explicitIntent
|
|
@@ -437,9 +458,104 @@ export class OnDemandIntake {
|
|
|
437
458
|
delete state.preparation;
|
|
438
459
|
}
|
|
439
460
|
}
|
|
461
|
+
/** A question is a waiting state, never an execution grant. */
|
|
462
|
+
awaitingClarification(agent) {
|
|
463
|
+
const pending = agent.session && this.#clarifications.get(agent.session.id);
|
|
464
|
+
if (this.#stopped || pending?.agent !== agent)
|
|
465
|
+
return false;
|
|
466
|
+
const boundary = agent.session?.snapshotEvents?.().filter(event => event.type === 'turn/end').at(-1);
|
|
467
|
+
const data = boundary?.data;
|
|
468
|
+
if (data?.turn !== undefined && data.turn >= pending.turn && ['aborted', 'error', 'max-tokens', 'blocked'].includes(data.reason?.kind ?? '')) {
|
|
469
|
+
this.#clarifications.delete(agent.session.id);
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
async clarify(state, executionSignal, reason) {
|
|
475
|
+
this.assertCurrent(state);
|
|
476
|
+
const signal = AbortSignal.any([executionSignal, state.input.signal, state.controller.signal]);
|
|
477
|
+
const context = state.requestIntentContext;
|
|
478
|
+
const sessionId = state.input.agent.session.id;
|
|
479
|
+
const scope = state.requestIntent?.action === 'change' && state.requestIntent.changeScope === 'unknown'
|
|
480
|
+
|| reason.message.startsWith('The requested change scope');
|
|
481
|
+
const target = !!executionClarification(state.task, state.requestIntent, context);
|
|
482
|
+
const transition = context?.activeTask && !taskCanExecute(context.activeTask);
|
|
483
|
+
const question = transition ? `現在のタスクは ${context.activeTask.state} です。続けるために、どの操作を希望しますか?(${taskAllowedOperations(context.activeTask).join(' / ')})`
|
|
484
|
+
: target ? 'どの具体的なファイル・項目・相手・操作を対象にしますか?'
|
|
485
|
+
: scope ? '変更するのは文章だけですか、それともコード・テスト・設定も含みますか?'
|
|
486
|
+
: state.requestIntent?.action === 'unknown' ? 'その対象に、具体的に何をしてほしいですか?'
|
|
487
|
+
: '今回の依頼は現在の作業の続きですか、それとも別の作業ですか?';
|
|
488
|
+
const key = canonicalContentHash({ context: context ?? null, task: state.task, humanEvidence: [...state.humanContents], question });
|
|
489
|
+
const previous = this.#clarifications.get(sessionId);
|
|
490
|
+
if (previous?.agent === state.input.agent && previous.key === key)
|
|
491
|
+
return previous.operation ?? previous.result;
|
|
492
|
+
const id = `request-clarification-${key.slice(0, 20)}`;
|
|
493
|
+
const result = { prepared: false, status: 'awaiting_clarification', question, questionId: id, originalTask: state.task,
|
|
494
|
+
permissions: 'unchanged_native_policy', nextAction: this.host.questions ? 'wait_for_user' : 'ask_user_once' };
|
|
495
|
+
const pending = { agent: state.input.agent, turn: state.input.turn, key, status: 'waiting', result: result,
|
|
496
|
+
operation: undefined };
|
|
497
|
+
this.#clarifications.set(sessionId, pending);
|
|
498
|
+
if (!this.host.questions || !state.input.agent.followup)
|
|
499
|
+
return result;
|
|
500
|
+
const operation = (async () => {
|
|
501
|
+
try {
|
|
502
|
+
const response = await abortable(this.host.questions.ask({ agent: state.input.agent, signal,
|
|
503
|
+
questions: [{ id, question, ...(scope && !target && !transition ? { options: [{ label: '文章のみ' }, { label: 'コード・テスト・設定を含む' }] } : {}) }] }), signal);
|
|
504
|
+
signal.throwIfAborted();
|
|
505
|
+
this.assertCurrent(state);
|
|
506
|
+
await this.host.validate(state.input);
|
|
507
|
+
signal.throwIfAborted();
|
|
508
|
+
this.assertCurrent(state);
|
|
509
|
+
const current = await this.host.interpret?.(state.input, state.task);
|
|
510
|
+
signal.throwIfAborted();
|
|
511
|
+
this.assertCurrent(state);
|
|
512
|
+
if (context && (!current || canonicalContentHash(current.context) !== canonicalContentHash(context))) {
|
|
513
|
+
if (current) {
|
|
514
|
+
state.requestIntentContext = current.context;
|
|
515
|
+
state.requestIntent = current.decision;
|
|
516
|
+
state.requestIntentContextDigest = canonicalContentHash(current.context);
|
|
517
|
+
}
|
|
518
|
+
pending.result = { ...result, ...(current ? { currentReferences: requestIntentRequiredReferenceIds(current.context) } : {}), status: 'clarification_context_changed', detail: 'The task or current evidence changed while awaiting the answer. Refresh the current request before asking again.' };
|
|
519
|
+
return pending.result;
|
|
520
|
+
}
|
|
521
|
+
const answer = response.answers[0];
|
|
522
|
+
const value = answer?.custom?.trim() || answer?.selected[0]?.trim();
|
|
523
|
+
if (answer?.id !== id || !value || value.length > 8192)
|
|
524
|
+
return result;
|
|
525
|
+
// Both texts originate from the human: preserve the original verbatim,
|
|
526
|
+
// label the actual UI answer separately, and re-enter ordinary native
|
|
527
|
+
// capture. The reply itself is never mapped to execution authority.
|
|
528
|
+
const content = `Original user request (unchanged):\n${state.task}\n\nUser answer to clarification "${question}":\n${value}`;
|
|
529
|
+
signal.throwIfAborted();
|
|
530
|
+
this.assertCurrent(state);
|
|
531
|
+
state.input.agent.followup({ id: randomUUID(), role: 'user', source: { kind: 'user' }, content: [{ type: 'text', text: content }] });
|
|
532
|
+
pending.status = 'queued';
|
|
533
|
+
pending.result = { ...result, status: 'clarification_answer_queued', answer: value };
|
|
534
|
+
return pending.result;
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
if (signal.aborted) {
|
|
538
|
+
if (this.#clarifications.get(sessionId) === pending)
|
|
539
|
+
this.#clarifications.delete(sessionId);
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
// A dismissed/unavailable question surface remains waiting, never approved.
|
|
543
|
+
return result;
|
|
544
|
+
}
|
|
545
|
+
finally {
|
|
546
|
+
pending.operation = undefined;
|
|
547
|
+
}
|
|
548
|
+
})();
|
|
549
|
+
pending.operation = operation;
|
|
550
|
+
return this.watch(operation);
|
|
551
|
+
}
|
|
440
552
|
bindIntent(state, submission) {
|
|
441
|
-
if (!submission)
|
|
553
|
+
if (!submission) {
|
|
554
|
+
if (state.requestIntent?.action === 'change' && state.requestIntent.changeScope === 'unknown') {
|
|
555
|
+
throw new RequestClarification('The requested change scope is unresolved. Specify implementation or document using the current request evidence before preparing work.');
|
|
556
|
+
}
|
|
442
557
|
return state.requestIntent;
|
|
558
|
+
}
|
|
443
559
|
if (!state.requestIntentContext || state.requestIntentContextDigest !== canonicalContentHash(state.requestIntentContext)) {
|
|
444
560
|
throw new Error('Request interpretation context is stale');
|
|
445
561
|
}
|
|
@@ -449,7 +565,7 @@ export class OnDemandIntake {
|
|
|
449
565
|
case 'no_active_goal': throw new Error(`Request relation ${submission.relation} cannot be used without an active Goal. ${NO_ACTIVE_GOAL_GUIDANCE}`);
|
|
450
566
|
case 'missing_reference': throw new Error('Request interpretation is missing required current message or active Goal references. Use the exact current evidenceRefs from the supplied context.');
|
|
451
567
|
case 'unknown_reference': throw new Error('Request interpretation references another request, stale revision, or unknown proposal. Use only the exact current evidenceRefs from the supplied context.');
|
|
452
|
-
case 'unresolved_intent': throw new
|
|
568
|
+
case 'unresolved_intent': throw new RequestClarification('Request relation or action is unresolved. Interpret the original request and current context; clarify with the user if its action or target remains unclear.');
|
|
453
569
|
case 'invalid_submission': throw new Error('Invalid request interpretation. Supply a relation, action and unique exact current evidenceRefs.');
|
|
454
570
|
case 'invalid_task_operation': {
|
|
455
571
|
const task = state.requestIntentContext.activeTask;
|
|
@@ -461,6 +577,9 @@ export class OnDemandIntake {
|
|
|
461
577
|
}
|
|
462
578
|
}
|
|
463
579
|
const bound = result.decision;
|
|
580
|
+
if (bound.action === 'change' && bound.changeScope === 'unknown') {
|
|
581
|
+
throw new RequestClarification('The requested change scope is unresolved. Specify implementation or document using the current request evidence before preparing work.');
|
|
582
|
+
}
|
|
464
583
|
const prior = state.requestIntent;
|
|
465
584
|
if (prior?.source === 'decision-provider' && prior.relation !== 'unknown' && prior.action !== 'unknown') {
|
|
466
585
|
if (prior.relation !== bound.relation || prior.action !== bound.action) {
|
|
@@ -497,12 +616,14 @@ export class OnDemandIntake {
|
|
|
497
616
|
const interpretation = state?.requestIntent;
|
|
498
617
|
const intentText = context ? `\n\nCurrent request references and same-session work (host-bound, advisory only):\n${JSON.stringify({
|
|
499
618
|
references: requestIntentReferenceIds(context),
|
|
619
|
+
requiredReferences: requestIntentRequiredReferenceIds(context),
|
|
500
620
|
activeGoal: context.activeGoal ?? null,
|
|
501
621
|
activeTask: context.activeTask ?? null,
|
|
502
|
-
proposals: context.proposals.map(({ id, revision, digest, text }) => ({ id, revision, digest, text
|
|
622
|
+
proposals: context.proposals.map(({ id, revision, digest, text }) => ({ id, revision, digest, text,
|
|
623
|
+
evidenceRef: `proposal:${canonicalContentHash([id, revision, digest])}` })),
|
|
503
624
|
decision: interpretation ? { relation: interpretation.relation, action: interpretation.action, source: interpretation.source,
|
|
504
625
|
policyVersion: interpretation.policyVersion, evidence: interpretation.evidence, unresolvedReason: interpretation.unresolvedReason ?? null } : null,
|
|
505
|
-
})}\
|
|
626
|
+
})}\nInclude every requiredReferences entry in requestIntent.evidenceRefs. Add a proposal evidenceRef only when the current user request actually adopts or refers to that proposal; do not copy all proposals automatically. Use only these exact references. The original human message remains unchanged.` : '';
|
|
506
627
|
const text = ON_DEMAND_GUIDANCE + intentText + (context && !context.activeGoal && !context.activeTask ? `\n${NO_ACTIVE_GOAL_GUIDANCE}` : '');
|
|
507
628
|
return [...messages, { id: randomUUID(), role: 'user', content: [{ type: 'text', text }],
|
|
508
629
|
source: { kind: KIOKUKO_DSH_SOURCE_KIND, form: 'snapshot', sections: [{ name: 'on-demand-intake', text }] } }];
|
|
@@ -625,53 +746,60 @@ export class OnDemandIntake {
|
|
|
625
746
|
if (state.preparationCalls.has(execution.callId) || !(state.status === 'answer' || state.status === 'prepared' && this.host.ready(state.input.agent, state.input.turn)))
|
|
626
747
|
throw new Error('Task preparation was already consumed for this turn');
|
|
627
748
|
state.preparationCalls.add(execution.callId);
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
749
|
+
try {
|
|
750
|
+
if (state.existing) {
|
|
751
|
+
this.assertCurrent(state);
|
|
752
|
+
execution.signal.throwIfAborted();
|
|
753
|
+
const boundIntent = this.bindIntent(state, value.requestIntent);
|
|
754
|
+
if (boundIntent?.relation === 'same_scope_continuation' && !requestIntentIsUnresolved(boundIntent) && boundIntent.action !== 'answer') {
|
|
755
|
+
const intentType = requestIntentTaskType(boundIntent, state.taskType ?? state.intent.taskType ?? value.taskType);
|
|
756
|
+
if (value.taskType !== undefined && intentType !== undefined && value.taskType !== intentType) {
|
|
757
|
+
throw new Error('The advisory taskType conflicts with the bound request action');
|
|
758
|
+
}
|
|
759
|
+
state.requestIntent = boundIntent;
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
if (!boundIntent || requestIntentIsUnresolved(boundIntent) || boundIntent.action === 'answer') {
|
|
763
|
+
throw new RequestClarification('Clarify whether this request continues, changes, or starts work before preparing it');
|
|
764
|
+
}
|
|
765
|
+
const intentType = requestIntentTaskType(boundIntent, state.taskType ?? state.intent.taskType ?? value.taskType);
|
|
766
|
+
if (!intentType || intentType === 'chat' || value.taskType !== undefined && value.taskType !== intentType) {
|
|
767
|
+
throw new Error('The advisory taskType conflicts with the bound request action');
|
|
768
|
+
}
|
|
769
|
+
state.existing = false;
|
|
770
|
+
state.status = 'answer';
|
|
771
|
+
state.memoryOwner = memoryOwnerKind(boundIntent, intentType);
|
|
772
|
+
await this.prepare(state, intentType, execution.signal, boundIntent, value.requestIntent !== undefined);
|
|
636
773
|
}
|
|
637
|
-
state.requestIntent = boundIntent;
|
|
638
774
|
}
|
|
639
775
|
else {
|
|
640
|
-
|
|
641
|
-
|
|
776
|
+
const suppliedIntent = this.bindIntent(state, value.requestIntent);
|
|
777
|
+
const currentIntent = suppliedIntent ?? state.requestIntent;
|
|
778
|
+
const unresolved = currentIntent !== undefined && requestIntentIsUnresolved(currentIntent);
|
|
779
|
+
if (unresolved && (state.requestIntentContext?.activeGoal || state.requestIntentContext?.activeTask || value.requestIntent !== undefined || value.taskType === undefined)) {
|
|
780
|
+
throw new RequestClarification('Clarify the request before preparing work');
|
|
781
|
+
}
|
|
782
|
+
if (state.requestIntentContext?.activeGoal && currentIntent === undefined) {
|
|
783
|
+
throw new RequestClarification('Clarify whether this request continues, changes, or starts the active Goal');
|
|
642
784
|
}
|
|
643
|
-
const intentType = requestIntentTaskType(
|
|
644
|
-
if (
|
|
785
|
+
const intentType = unresolved ? undefined : requestIntentTaskType(currentIntent, state.taskType ?? state.intent.taskType ?? value.taskType);
|
|
786
|
+
if (value.taskType !== undefined && intentType !== undefined && value.taskType !== intentType) {
|
|
645
787
|
throw new Error('The advisory taskType conflicts with the bound request action');
|
|
646
788
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
789
|
+
const taskType = intentType ?? value.taskType;
|
|
790
|
+
if (!taskType || taskType === 'chat' || !ACTION_TYPES.includes(taskType))
|
|
791
|
+
throw new RequestClarification('Answer ordinary questions directly; clarify unknown actions before preparing work');
|
|
792
|
+
state.memoryOwner = memoryOwnerKind(currentIntent, taskType);
|
|
793
|
+
await this.prepare(state, taskType, execution.signal, currentIntent, value.requestIntent !== undefined);
|
|
651
794
|
}
|
|
795
|
+
return { prepared: true, taskType: state.taskType ?? value.taskType ?? requestIntentTaskType(state.requestIntent), originalTask: state.task,
|
|
796
|
+
...(state.requestIntent ? { requestIntent: state.requestIntent } : {}), memoryOwner: state.memoryOwner, permissions: 'unchanged_native_policy' };
|
|
652
797
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
if (unresolved && (state.requestIntentContext?.activeGoal || value.requestIntent !== undefined || value.taskType === undefined)) {
|
|
658
|
-
throw new Error('Clarify the request before preparing work');
|
|
659
|
-
}
|
|
660
|
-
if (state.requestIntentContext?.activeGoal && currentIntent === undefined) {
|
|
661
|
-
throw new Error('Clarify whether this request continues, changes, or starts the active Goal');
|
|
662
|
-
}
|
|
663
|
-
const intentType = unresolved ? undefined : requestIntentTaskType(currentIntent, state.taskType ?? state.intent.taskType ?? value.taskType);
|
|
664
|
-
if (value.taskType !== undefined && intentType !== undefined && value.taskType !== intentType) {
|
|
665
|
-
throw new Error('The advisory taskType conflicts with the bound request action');
|
|
666
|
-
}
|
|
667
|
-
const taskType = intentType ?? value.taskType;
|
|
668
|
-
if (!taskType || taskType === 'chat' || !ACTION_TYPES.includes(taskType))
|
|
669
|
-
throw new Error('Answer ordinary questions directly; clarify unknown actions before preparing work');
|
|
670
|
-
state.memoryOwner = memoryOwnerKind(currentIntent, taskType);
|
|
671
|
-
await this.prepare(state, taskType, execution.signal, currentIntent, value.requestIntent !== undefined);
|
|
798
|
+
catch (error) {
|
|
799
|
+
if (!(error instanceof RequestClarification))
|
|
800
|
+
throw error;
|
|
801
|
+
return this.clarify(state, execution.signal, error);
|
|
672
802
|
}
|
|
673
|
-
return { prepared: true, taskType: state.taskType ?? value.taskType ?? requestIntentTaskType(state.requestIntent), originalTask: state.task,
|
|
674
|
-
...(state.requestIntent ? { requestIntent: state.requestIntent } : {}), memoryOwner: state.memoryOwner, permissions: 'unchanged_native_policy' };
|
|
675
803
|
} };
|
|
676
804
|
this.#disposers.push(tools.register(definition));
|
|
677
805
|
this.#disposers.push(context.on('tools/pre-execute', async (execution, next) => {
|
|
@@ -725,6 +853,17 @@ export class OnDemandIntake {
|
|
|
725
853
|
const decision = await next();
|
|
726
854
|
return decision?.kind === 'cancel' ? denied('Native tool policy cancelled this call') : decision;
|
|
727
855
|
}
|
|
856
|
+
// Evidence gathering does not decide the request's action or scope.
|
|
857
|
+
// In particular, an inherited owner must not turn a read into an
|
|
858
|
+
// immutable research preparation before the main model can interpret it.
|
|
859
|
+
if (state.status === 'answer' && !state.nativeUnowned && execution.parent === undefined && NATIVE_EVIDENCE_READS.has(execution.name)) {
|
|
860
|
+
const clarification = executionClarification(state.task, state.requestIntent, state.requestIntentContext);
|
|
861
|
+
if (clarification)
|
|
862
|
+
return denied(clarification);
|
|
863
|
+
this.#executions.set(execution, { turn: state, execute: actual.execute, evidenceReadDigest: canonicalContentHash(execution.arguments) });
|
|
864
|
+
const decision = await next();
|
|
865
|
+
return decision?.kind === 'cancel' ? denied('Native tool policy cancelled this call') : decision;
|
|
866
|
+
}
|
|
728
867
|
if (state.nativeUnowned && UNOWNED_NATIVE_TOOLS.has(execution.name) && execution.parent === undefined) {
|
|
729
868
|
if (await this.host.nativeOwnership?.(state.input) !== 'unowned') {
|
|
730
869
|
state.nativeUnowned = false;
|
|
@@ -774,6 +913,15 @@ export class OnDemandIntake {
|
|
|
774
913
|
return decision?.kind === 'cancel' ? denied('Native tool policy cancelled this call') : decision;
|
|
775
914
|
}
|
|
776
915
|
catch (error) {
|
|
916
|
+
if (error instanceof RequestClarification) {
|
|
917
|
+
try {
|
|
918
|
+
await this.clarify(state, execution.signal, error);
|
|
919
|
+
return denied('Awaiting user clarification; no work was executed. Continue through the clarified human request.');
|
|
920
|
+
}
|
|
921
|
+
catch (cancelled) {
|
|
922
|
+
return denied(cancelled instanceof Error ? cancelled.message : 'Clarification cancelled');
|
|
923
|
+
}
|
|
924
|
+
}
|
|
777
925
|
return denied(error instanceof Error ? error.message : 'Execution preparation failed');
|
|
778
926
|
}
|
|
779
927
|
}, { prepend: true }));
|
|
@@ -809,6 +957,12 @@ export class OnDemandIntake {
|
|
|
809
957
|
throw new Error('Native Lisp identity or arguments changed');
|
|
810
958
|
return withRootLispAdmission(execution, () => this.nativeLisp(execution), next);
|
|
811
959
|
}
|
|
960
|
+
if (proof.evidenceReadDigest !== undefined) {
|
|
961
|
+
if (state.status !== 'answer' || execution.parent !== undefined || !NATIVE_EVIDENCE_READS.has(execution.name)
|
|
962
|
+
|| canonicalContentHash(execution.arguments) !== proof.evidenceReadDigest)
|
|
963
|
+
throw new Error('Native evidence read identity or arguments changed');
|
|
964
|
+
return next();
|
|
965
|
+
}
|
|
812
966
|
if (proof.nativeUnownedDigest !== undefined) {
|
|
813
967
|
if (!this.unownedNative(execution) || await this.host.nativeOwnership?.(state.input) !== 'unowned') {
|
|
814
968
|
state.nativeUnowned = false;
|
|
@@ -824,28 +978,36 @@ export class OnDemandIntake {
|
|
|
824
978
|
|| !carrier || canonicalContentHash(carrier) !== proof.carrierDigest)
|
|
825
979
|
throw new Error('Preparation carrier was already consumed or changed');
|
|
826
980
|
state.preparationCalls.add(execution.callId);
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
&&
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
981
|
+
try {
|
|
982
|
+
const requestIntent = this.bindIntent(state, carrier.requestIntent);
|
|
983
|
+
const clarification = executionClarification(state.task, requestIntent, state.requestIntentContext);
|
|
984
|
+
if (clarification)
|
|
985
|
+
throw new RequestClarification(clarification);
|
|
986
|
+
const unresolved = requestIntent !== undefined && requestIntentIsUnresolved(requestIntent);
|
|
987
|
+
const legacyTaskTypeAllowed = carrier.taskType !== undefined && !state.requestIntentContext?.activeGoal && !state.requestIntentContext?.activeTask
|
|
988
|
+
&& (state.requestIntentContext?.proposals.length ?? 0) === 0;
|
|
989
|
+
if (unresolved && !legacyTaskTypeAllowed)
|
|
990
|
+
throw new RequestClarification('Clarify the requested action and scope before preparing work');
|
|
991
|
+
const intentType = unresolved ? undefined : requestIntentTaskType(requestIntent, state.taskType ?? state.intent.taskType ?? carrier.taskType);
|
|
992
|
+
const taskType = intentType ?? carrier.taskType;
|
|
993
|
+
if (carrier.taskType !== undefined && taskType !== carrier.taskType)
|
|
994
|
+
throw new Error('The advisory taskType conflicts with the bound request action');
|
|
995
|
+
if (!taskType || taskType === 'chat') {
|
|
996
|
+
throw new RequestClarification('Clarify the requested action and scope before preparing work');
|
|
997
|
+
}
|
|
998
|
+
state.memoryOwner = memoryOwnerKind(requestIntent, taskType);
|
|
999
|
+
await this.prepare(state, taskType, execution.signal, requestIntent, carrier.requestIntent !== undefined);
|
|
1000
|
+
const result = { prepared: true, taskType: state.taskType, originalTask: state.task,
|
|
1001
|
+
...(state.requestIntent ? { requestIntent: state.requestIntent } : {}), memoryOwner: state.memoryOwner,
|
|
1002
|
+
permissions: 'unchanged_native_policy', programExecuted: false };
|
|
1003
|
+
return { isError: false, value: { logs: ['Host intake prepared. No program was evaluated.'], result }, content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
1004
|
+
}
|
|
1005
|
+
catch (error) {
|
|
1006
|
+
if (!(error instanceof RequestClarification))
|
|
1007
|
+
throw error;
|
|
1008
|
+
const result = { ...await this.clarify(state, execution.signal, error), programExecuted: false };
|
|
1009
|
+
return { isError: false, value: { logs: ['Awaiting user clarification. No program was evaluated.'], result }, content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
1010
|
+
}
|
|
849
1011
|
}, { prepend: true }));
|
|
850
1012
|
this.#disposers.push(tools.guard(execution => {
|
|
851
1013
|
if (this.#stopped || execution.signal.aborted)
|
|
@@ -870,6 +1032,10 @@ export class OnDemandIntake {
|
|
|
870
1032
|
return this.nativeCommand(execution) ? undefined : 'Native command identity or arguments changed';
|
|
871
1033
|
if (proof.nativeLispDigest !== undefined)
|
|
872
1034
|
return this.nativeLisp(execution) ? undefined : 'Native Lisp identity or arguments changed';
|
|
1035
|
+
if (proof.evidenceReadDigest !== undefined)
|
|
1036
|
+
return state.status === 'answer' && execution.parent === undefined
|
|
1037
|
+
&& NATIVE_EVIDENCE_READS.has(execution.name) && canonicalContentHash(execution.arguments) === proof.evidenceReadDigest
|
|
1038
|
+
? undefined : 'Native evidence read identity or arguments changed';
|
|
873
1039
|
if (proof.nativeUnownedDigest !== undefined)
|
|
874
1040
|
return this.unownedNative(execution) ? undefined : 'Native unowned execution proof changed';
|
|
875
1041
|
if (proof.carrier)
|
|
@@ -899,8 +1065,9 @@ export class OnDemandIntake {
|
|
|
899
1065
|
state.controller.abort(new Error('Native agent disposed'));
|
|
900
1066
|
state.status = 'closed';
|
|
901
1067
|
this.#turns.delete(agent.session.id);
|
|
1068
|
+
this.#clarifications.delete(agent.session.id);
|
|
902
1069
|
}
|
|
903
|
-
stop() { this.#stopped = true; this.#lifecycle.abort(new Error('On-demand intake stopped')); }
|
|
1070
|
+
stop() { this.#clarifications.clear(); this.#stopped = true; this.#lifecycle.abort(new Error('On-demand intake stopped')); }
|
|
904
1071
|
async drain() { while (this.#pending.size)
|
|
905
1072
|
await Promise.allSettled([...this.#pending]); }
|
|
906
1073
|
async dispose() { this.stop(); await this.drain(); for (const dispose of this.#disposers.reverse())
|