kiokuko-dsh 0.2.14 → 0.2.16

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.
Files changed (56) hide show
  1. package/dist/dsh/coding-workflow/host.d.ts.map +1 -1
  2. package/dist/dsh/coding-workflow/host.js +9 -1
  3. package/dist/dsh/coding-workflow/host.js.map +1 -1
  4. package/dist/dsh/config.d.ts +3 -3
  5. package/dist/dsh/config.d.ts.map +1 -1
  6. package/dist/dsh/config.js +8 -1
  7. package/dist/dsh/config.js.map +1 -1
  8. package/dist/dsh/core/host.d.ts.map +1 -1
  9. package/dist/dsh/core/host.js +15 -5
  10. package/dist/dsh/core/host.js.map +1 -1
  11. package/dist/dsh/core/tasks.d.ts.map +1 -1
  12. package/dist/dsh/core/tasks.js +4 -4
  13. package/dist/dsh/core/tasks.js.map +1 -1
  14. package/dist/dsh/decisions/request-intent.d.ts +2 -0
  15. package/dist/dsh/decisions/request-intent.d.ts.map +1 -1
  16. package/dist/dsh/decisions/request-intent.js +16 -7
  17. package/dist/dsh/decisions/request-intent.js.map +1 -1
  18. package/dist/dsh/host-adapter/admission.d.ts.map +1 -1
  19. package/dist/dsh/host-adapter/admission.js +26 -2
  20. package/dist/dsh/host-adapter/admission.js.map +1 -1
  21. package/dist/dsh/host-adapter/lifecycle.d.ts +2 -1
  22. package/dist/dsh/host-adapter/lifecycle.d.ts.map +1 -1
  23. package/dist/dsh/host-adapter/lifecycle.js +2 -0
  24. package/dist/dsh/host-adapter/lifecycle.js.map +1 -1
  25. package/dist/dsh/host-adapter.d.ts.map +1 -1
  26. package/dist/dsh/host-adapter.js +15 -4
  27. package/dist/dsh/host-adapter.js.map +1 -1
  28. package/dist/dsh/index.d.ts +2 -2
  29. package/dist/dsh/intake-gate.js +3 -3
  30. package/dist/dsh/intake-gate.js.map +1 -1
  31. package/dist/dsh/intake-questions.d.ts +5 -1
  32. package/dist/dsh/intake-questions.d.ts.map +1 -1
  33. package/dist/dsh/intake-questions.js +3 -2
  34. package/dist/dsh/intake-questions.js.map +1 -1
  35. package/dist/dsh/lisp/coding-choice.d.ts +5 -0
  36. package/dist/dsh/lisp/coding-choice.d.ts.map +1 -1
  37. package/dist/dsh/lisp/coding-choice.js.map +1 -1
  38. package/dist/dsh/lisp/coding-preparation.d.ts.map +1 -1
  39. package/dist/dsh/lisp/coding-preparation.js +4 -1
  40. package/dist/dsh/lisp/coding-preparation.js.map +1 -1
  41. package/dist/dsh/lisp/surface.js +1 -1
  42. package/dist/dsh/lisp/surface.js.map +1 -1
  43. package/dist/dsh/modules/enno.d.ts +3 -3
  44. package/dist/dsh/on-demand-intake.d.ts +7 -1
  45. package/dist/dsh/on-demand-intake.d.ts.map +1 -1
  46. package/dist/dsh/on-demand-intake.js +219 -74
  47. package/dist/dsh/on-demand-intake.js.map +1 -1
  48. package/dist/locale/en.json +1 -1
  49. package/dist/locale/ja.json +1 -1
  50. package/dist/locale/ko.json +1 -1
  51. package/dist/locale/zh-CN.json +1 -1
  52. package/docs/akinator-memory.md +5 -3
  53. package/docs/coding-workflow.md +2 -0
  54. package/docs/on-demand-intake.md +10 -0
  55. package/docs/skill-compiler.md +4 -1
  56. 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?.relation === 'unknown' || requestIntent?.action === 'unknown') {
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;
@@ -80,6 +83,7 @@ export class OnDemandIntake {
80
83
  host;
81
84
  options;
82
85
  #turns = new Map();
86
+ #clarifications = new Map();
83
87
  #executions = new WeakMap();
84
88
  #disposers = [];
85
89
  #pending = new Set();
@@ -231,10 +235,10 @@ export class OnDemandIntake {
231
235
  // classification. Never manufacture an owned task merely to run Bash.
232
236
  if (!this.options.onlyWhenDeferred && await this.host.nativeOwnership?.(state.input) === 'unowned') {
233
237
  this.assertCurrent(state);
234
- await this.host.prepareNativeCoding?.(state.input, state.task);
238
+ const context = createRequestIntentContext({ task: state.task, messageRefs: requestIntentMessageRefs(state.input.messages, state.task) });
239
+ await this.host.prepareNativeCoding?.(state.input, state.task, context);
235
240
  this.assertCurrent(state);
236
241
  state.nativeUnowned = true;
237
- const context = createRequestIntentContext({ task: state.task, messageRefs: requestIntentMessageRefs(state.input.messages, state.task) });
238
242
  if (context) {
239
243
  state.requestIntentContext = context;
240
244
  state.requestIntentContextDigest = canonicalContentHash(context);
@@ -261,6 +265,10 @@ export class OnDemandIntake {
261
265
  catch (error) {
262
266
  input.signal.throwIfAborted();
263
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);
264
272
  state.memoryOwner = memoryOwnerKind(state.requestIntent, state.intent.taskType);
265
273
  this.assertCurrent(state);
266
274
  if (previous?.input.turn !== input.turn)
@@ -289,7 +297,7 @@ export class OnDemandIntake {
289
297
  return false;
290
298
  }
291
299
  const canContinue = state.requestIntent?.relation === 'same_scope_continuation'
292
- && state.requestIntent.action !== 'answer' && state.requestIntent.action !== 'unknown';
300
+ && !requestIntentIsUnresolved(state.requestIntent) && state.requestIntent.action !== 'answer';
293
301
  if (previous?.input.turn !== input.turn && state.task && canContinue
294
302
  && await this.host.existing(input, state.requestIntent)) {
295
303
  // Persisted ownership is only an acknowledgement when this exact native
@@ -303,13 +311,15 @@ export class OnDemandIntake {
303
311
  // Eager mode still binds known work before generation. Classifier uncertainty
304
312
  // instead reaches ordinary reasoning, with no execution grant or purpose UI.
305
313
  const requestIntentUnresolved = state.requestIntent !== undefined
306
- && (state.requestIntent.relation === 'unknown' || state.requestIntent.action === 'unknown');
314
+ && requestIntentIsUnresolved(state.requestIntent);
307
315
  const requestAsAnswer = state.requestIntent?.action === 'answer';
308
316
  const knownIntent = this.host.interpret === undefined
309
317
  ? !state.intent.deferInference && state.intent.taskType !== undefined && state.intent.taskType !== 'chat'
310
318
  : state.requestIntent !== undefined && !requestIntentUnresolved && !requestAsAnswer
311
319
  && state.requestIntent.relation !== 'unknown' && state.requestIntent.action !== 'unknown';
312
- if (this.options.onlyWhenDeferred && knownIntent) {
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) {
313
323
  this.#turns.delete(sessionId);
314
324
  state.status = 'closed';
315
325
  controller.abort(new Error('Known intent continues through eager admission'));
@@ -376,9 +386,19 @@ export class OnDemandIntake {
376
386
  if (state.selectionPending)
377
387
  throw new ExecutionSelectionPending();
378
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
+ }
379
399
  const clarification = executionClarification(state.task, resolved, state.requestIntentContext);
380
400
  if (clarification)
381
- throw new Error(clarification);
401
+ throw new RequestClarification(clarification);
382
402
  if (!ACTION_TYPES.includes(taskType))
383
403
  throw new Error('Answer conversational questions directly; prepare_requested_work is for requested work');
384
404
  if (state.status === 'prepared' && state.confirmationResume && explicitIntent
@@ -438,9 +458,104 @@ export class OnDemandIntake {
438
458
  delete state.preparation;
439
459
  }
440
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
+ }
441
552
  bindIntent(state, submission) {
442
- 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
+ }
443
557
  return state.requestIntent;
558
+ }
444
559
  if (!state.requestIntentContext || state.requestIntentContextDigest !== canonicalContentHash(state.requestIntentContext)) {
445
560
  throw new Error('Request interpretation context is stale');
446
561
  }
@@ -450,7 +565,7 @@ export class OnDemandIntake {
450
565
  case 'no_active_goal': throw new Error(`Request relation ${submission.relation} cannot be used without an active Goal. ${NO_ACTIVE_GOAL_GUIDANCE}`);
451
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.');
452
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.');
453
- case 'unresolved_intent': throw new Error('Request relation or action is unresolved. Interpret the original request and current context; clarify with the user if its action or target remains unclear.');
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.');
454
569
  case 'invalid_submission': throw new Error('Invalid request interpretation. Supply a relation, action and unique exact current evidenceRefs.');
455
570
  case 'invalid_task_operation': {
456
571
  const task = state.requestIntentContext.activeTask;
@@ -462,6 +577,9 @@ export class OnDemandIntake {
462
577
  }
463
578
  }
464
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
+ }
465
583
  const prior = state.requestIntent;
466
584
  if (prior?.source === 'decision-provider' && prior.relation !== 'unknown' && prior.action !== 'unknown') {
467
585
  if (prior.relation !== bound.relation || prior.action !== bound.action) {
@@ -498,12 +616,14 @@ export class OnDemandIntake {
498
616
  const interpretation = state?.requestIntent;
499
617
  const intentText = context ? `\n\nCurrent request references and same-session work (host-bound, advisory only):\n${JSON.stringify({
500
618
  references: requestIntentReferenceIds(context),
619
+ requiredReferences: requestIntentRequiredReferenceIds(context),
501
620
  activeGoal: context.activeGoal ?? null,
502
621
  activeTask: context.activeTask ?? null,
503
- 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])}` })),
504
624
  decision: interpretation ? { relation: interpretation.relation, action: interpretation.action, source: interpretation.source,
505
625
  policyVersion: interpretation.policyVersion, evidence: interpretation.evidence, unresolvedReason: interpretation.unresolvedReason ?? null } : null,
506
- })}\nUse only these exact references in requestIntent.evidenceRefs. The original human message remains unchanged.` : '';
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.` : '';
507
627
  const text = ON_DEMAND_GUIDANCE + intentText + (context && !context.activeGoal && !context.activeTask ? `\n${NO_ACTIVE_GOAL_GUIDANCE}` : '');
508
628
  return [...messages, { id: randomUUID(), role: 'user', content: [{ type: 'text', text }],
509
629
  source: { kind: KIOKUKO_DSH_SOURCE_KIND, form: 'snapshot', sections: [{ name: 'on-demand-intake', text }] } }];
@@ -626,53 +746,60 @@ export class OnDemandIntake {
626
746
  if (state.preparationCalls.has(execution.callId) || !(state.status === 'answer' || state.status === 'prepared' && this.host.ready(state.input.agent, state.input.turn)))
627
747
  throw new Error('Task preparation was already consumed for this turn');
628
748
  state.preparationCalls.add(execution.callId);
629
- if (state.existing) {
630
- this.assertCurrent(state);
631
- execution.signal.throwIfAborted();
632
- const boundIntent = this.bindIntent(state, value.requestIntent);
633
- if (boundIntent?.relation === 'same_scope_continuation' && boundIntent.action !== 'answer' && boundIntent.action !== 'unknown') {
634
- const intentType = requestIntentTaskType(boundIntent, state.taskType ?? state.intent.taskType ?? value.taskType);
635
- if (value.taskType !== undefined && intentType !== undefined && value.taskType !== intentType) {
636
- throw new Error('The advisory taskType conflicts with the bound request action');
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);
637
773
  }
638
- state.requestIntent = boundIntent;
639
774
  }
640
775
  else {
641
- if (!boundIntent || boundIntent.relation === 'unknown' || boundIntent.action === 'unknown' || boundIntent.action === 'answer') {
642
- throw new Error('Clarify whether this request continues, changes, or starts work before preparing it');
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');
643
781
  }
644
- const intentType = requestIntentTaskType(boundIntent, state.taskType ?? state.intent.taskType ?? value.taskType);
645
- if (!intentType || intentType === 'chat' || value.taskType !== undefined && value.taskType !== intentType) {
782
+ if (state.requestIntentContext?.activeGoal && currentIntent === undefined) {
783
+ throw new RequestClarification('Clarify whether this request continues, changes, or starts the active Goal');
784
+ }
785
+ const intentType = unresolved ? undefined : requestIntentTaskType(currentIntent, state.taskType ?? state.intent.taskType ?? value.taskType);
786
+ if (value.taskType !== undefined && intentType !== undefined && value.taskType !== intentType) {
646
787
  throw new Error('The advisory taskType conflicts with the bound request action');
647
788
  }
648
- state.existing = false;
649
- state.status = 'answer';
650
- state.memoryOwner = memoryOwnerKind(boundIntent, intentType);
651
- await this.prepare(state, intentType, execution.signal, boundIntent, value.requestIntent !== undefined);
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);
652
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' };
653
797
  }
654
- else {
655
- const suppliedIntent = this.bindIntent(state, value.requestIntent);
656
- const currentIntent = suppliedIntent ?? state.requestIntent;
657
- const unresolved = currentIntent?.relation === 'unknown' || currentIntent?.action === 'unknown';
658
- if (unresolved && (state.requestIntentContext?.activeGoal || value.requestIntent !== undefined || value.taskType === undefined)) {
659
- throw new Error('Clarify the request before preparing work');
660
- }
661
- if (state.requestIntentContext?.activeGoal && currentIntent === undefined) {
662
- throw new Error('Clarify whether this request continues, changes, or starts the active Goal');
663
- }
664
- const intentType = unresolved ? undefined : requestIntentTaskType(currentIntent, state.taskType ?? state.intent.taskType ?? value.taskType);
665
- if (value.taskType !== undefined && intentType !== undefined && value.taskType !== intentType) {
666
- throw new Error('The advisory taskType conflicts with the bound request action');
667
- }
668
- const taskType = intentType ?? value.taskType;
669
- if (!taskType || taskType === 'chat' || !ACTION_TYPES.includes(taskType))
670
- throw new Error('Answer ordinary questions directly; clarify unknown actions before preparing work');
671
- state.memoryOwner = memoryOwnerKind(currentIntent, taskType);
672
- 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);
673
802
  }
674
- return { prepared: true, taskType: state.taskType ?? value.taskType ?? requestIntentTaskType(state.requestIntent), originalTask: state.task,
675
- ...(state.requestIntent ? { requestIntent: state.requestIntent } : {}), memoryOwner: state.memoryOwner, permissions: 'unchanged_native_policy' };
676
803
  } };
677
804
  this.#disposers.push(tools.register(definition));
678
805
  this.#disposers.push(context.on('tools/pre-execute', async (execution, next) => {
@@ -786,6 +913,15 @@ export class OnDemandIntake {
786
913
  return decision?.kind === 'cancel' ? denied('Native tool policy cancelled this call') : decision;
787
914
  }
788
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
+ }
789
925
  return denied(error instanceof Error ? error.message : 'Execution preparation failed');
790
926
  }
791
927
  }, { prepend: true }));
@@ -842,28 +978,36 @@ export class OnDemandIntake {
842
978
  || !carrier || canonicalContentHash(carrier) !== proof.carrierDigest)
843
979
  throw new Error('Preparation carrier was already consumed or changed');
844
980
  state.preparationCalls.add(execution.callId);
845
- const requestIntent = this.bindIntent(state, carrier.requestIntent);
846
- const clarification = executionClarification(state.task, requestIntent, state.requestIntentContext);
847
- if (clarification)
848
- throw new Error(clarification);
849
- const unresolved = requestIntent?.relation === 'unknown' || requestIntent?.action === 'unknown';
850
- const legacyTaskTypeAllowed = carrier.taskType !== undefined && !state.requestIntentContext?.activeGoal
851
- && (state.requestIntentContext?.proposals.length ?? 0) === 0;
852
- if (unresolved && !legacyTaskTypeAllowed)
853
- throw new Error('Clarify the requested action and scope before preparing work');
854
- const intentType = unresolved ? undefined : requestIntentTaskType(requestIntent, state.taskType ?? state.intent.taskType ?? carrier.taskType);
855
- const taskType = intentType ?? carrier.taskType;
856
- if (carrier.taskType !== undefined && taskType !== carrier.taskType)
857
- throw new Error('The advisory taskType conflicts with the bound request action');
858
- if (!taskType || taskType === 'chat') {
859
- throw new Error('Clarify the requested action and scope before preparing work');
860
- }
861
- state.memoryOwner = memoryOwnerKind(requestIntent, taskType);
862
- await this.prepare(state, taskType, execution.signal, requestIntent, carrier.requestIntent !== undefined);
863
- const result = { prepared: true, taskType: state.taskType, originalTask: state.task,
864
- ...(state.requestIntent ? { requestIntent: state.requestIntent } : {}), memoryOwner: state.memoryOwner,
865
- permissions: 'unchanged_native_policy', programExecuted: false };
866
- return { isError: false, value: { logs: ['Host intake prepared. No program was evaluated.'], result }, content: [{ type: 'text', text: JSON.stringify(result) }] };
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
+ }
867
1011
  }, { prepend: true }));
868
1012
  this.#disposers.push(tools.guard(execution => {
869
1013
  if (this.#stopped || execution.signal.aborted)
@@ -921,8 +1065,9 @@ export class OnDemandIntake {
921
1065
  state.controller.abort(new Error('Native agent disposed'));
922
1066
  state.status = 'closed';
923
1067
  this.#turns.delete(agent.session.id);
1068
+ this.#clarifications.delete(agent.session.id);
924
1069
  }
925
- 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')); }
926
1071
  async drain() { while (this.#pending.size)
927
1072
  await Promise.allSettled([...this.#pending]); }
928
1073
  async dispose() { this.stop(); await this.drain(); for (const dispose of this.#disposers.reverse())