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.
- package/dist/dsh/coding-workflow/host.d.ts.map +1 -1
- package/dist/dsh/coding-workflow/host.js +9 -1
- package/dist/dsh/coding-workflow/host.js.map +1 -1
- package/dist/dsh/config.d.ts +3 -3
- package/dist/dsh/config.d.ts.map +1 -1
- package/dist/dsh/config.js +8 -1
- package/dist/dsh/config.js.map +1 -1
- package/dist/dsh/core/host.d.ts.map +1 -1
- package/dist/dsh/core/host.js +15 -5
- 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 +26 -2
- 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 +15 -4
- package/dist/dsh/host-adapter.js.map +1 -1
- package/dist/dsh/index.d.ts +2 -2
- package/dist/dsh/intake-gate.js +3 -3
- package/dist/dsh/intake-gate.js.map +1 -1
- package/dist/dsh/intake-questions.d.ts +5 -1
- package/dist/dsh/intake-questions.d.ts.map +1 -1
- package/dist/dsh/intake-questions.js +3 -2
- package/dist/dsh/intake-questions.js.map +1 -1
- package/dist/dsh/lisp/coding-choice.d.ts +5 -0
- package/dist/dsh/lisp/coding-choice.d.ts.map +1 -1
- package/dist/dsh/lisp/coding-choice.js.map +1 -1
- package/dist/dsh/lisp/coding-preparation.d.ts.map +1 -1
- package/dist/dsh/lisp/coding-preparation.js +4 -1
- package/dist/dsh/lisp/coding-preparation.js.map +1 -1
- package/dist/dsh/lisp/surface.js +1 -1
- package/dist/dsh/lisp/surface.js.map +1 -1
- package/dist/dsh/modules/enno.d.ts +3 -3
- package/dist/dsh/on-demand-intake.d.ts +7 -1
- package/dist/dsh/on-demand-intake.d.ts.map +1 -1
- package/dist/dsh/on-demand-intake.js +219 -74
- 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/akinator-memory.md +5 -3
- package/docs/coding-workflow.md +2 -0
- package/docs/on-demand-intake.md +10 -0
- package/docs/skill-compiler.md +4 -1
- 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;
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
})}\
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
|
|
642
|
-
|
|
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
|
-
|
|
645
|
-
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
&&
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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())
|