kiokuko-dsh 0.2.4 → 0.2.5
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/agenticreplay-event-mapper.d.ts +7 -0
- package/dist/dsh/agenticreplay-event-mapper.d.ts.map +1 -1
- package/dist/dsh/agenticreplay-event-mapper.js +52 -13
- package/dist/dsh/agenticreplay-event-mapper.js.map +1 -1
- package/dist/dsh/agenticreplay-hooks.d.ts.map +1 -1
- package/dist/dsh/agenticreplay-hooks.js +10 -1
- package/dist/dsh/agenticreplay-hooks.js.map +1 -1
- package/dist/dsh/agenticreplay-recorder.d.ts +5 -0
- package/dist/dsh/agenticreplay-recorder.d.ts.map +1 -1
- package/dist/dsh/agenticreplay-recorder.js +193 -20
- package/dist/dsh/agenticreplay-recorder.js.map +1 -1
- package/dist/dsh/answer-context.d.ts +8 -6
- package/dist/dsh/answer-context.d.ts.map +1 -1
- package/dist/dsh/answer-context.js +85 -21
- package/dist/dsh/answer-context.js.map +1 -1
- package/dist/dsh/coding-workflow/contracts.d.ts +5 -0
- package/dist/dsh/coding-workflow/contracts.d.ts.map +1 -1
- package/dist/dsh/coding-workflow/contracts.js +20 -3
- package/dist/dsh/coding-workflow/contracts.js.map +1 -1
- package/dist/dsh/coding-workflow/host.d.ts +4 -1
- package/dist/dsh/coding-workflow/host.d.ts.map +1 -1
- package/dist/dsh/coding-workflow/host.js +72 -44
- package/dist/dsh/coding-workflow/host.js.map +1 -1
- package/dist/dsh/coding-workflow/intent.d.ts +8 -0
- package/dist/dsh/coding-workflow/intent.d.ts.map +1 -1
- package/dist/dsh/coding-workflow/intent.js +33 -2
- package/dist/dsh/coding-workflow/intent.js.map +1 -1
- package/dist/dsh/coding-workflow/service.d.ts +214 -0
- package/dist/dsh/coding-workflow/service.d.ts.map +1 -1
- package/dist/dsh/coding-workflow/service.js +86 -6
- package/dist/dsh/coding-workflow/service.js.map +1 -1
- package/dist/dsh/coding-workflow/store.d.ts.map +1 -1
- package/dist/dsh/coding-workflow/store.js +10 -1
- package/dist/dsh/coding-workflow/store.js.map +1 -1
- package/dist/dsh/core/host.d.ts.map +1 -1
- package/dist/dsh/core/host.js +288 -48
- package/dist/dsh/core/host.js.map +1 -1
- package/dist/dsh/core/tasks.d.ts +7 -0
- package/dist/dsh/core/tasks.d.ts.map +1 -1
- package/dist/dsh/core/tasks.js +59 -8
- package/dist/dsh/core/tasks.js.map +1 -1
- package/dist/dsh/decisions/contracts.d.ts +3 -2
- package/dist/dsh/decisions/contracts.d.ts.map +1 -1
- package/dist/dsh/decisions/contracts.js +1 -1
- package/dist/dsh/decisions/contracts.js.map +1 -1
- package/dist/dsh/decisions/request-intent.d.ts +121 -0
- package/dist/dsh/decisions/request-intent.d.ts.map +1 -0
- package/dist/dsh/decisions/request-intent.js +260 -0
- package/dist/dsh/decisions/request-intent.js.map +1 -0
- package/dist/dsh/decisions/service.d.ts +5 -1
- package/dist/dsh/decisions/service.d.ts.map +1 -1
- package/dist/dsh/decisions/service.js +2 -1
- package/dist/dsh/decisions/service.js.map +1 -1
- package/dist/dsh/execution-support.d.ts +2 -0
- package/dist/dsh/execution-support.d.ts.map +1 -1
- package/dist/dsh/execution-support.js +36 -8
- package/dist/dsh/execution-support.js.map +1 -1
- package/dist/dsh/host-adapter/admission.d.ts +4 -1
- package/dist/dsh/host-adapter/admission.d.ts.map +1 -1
- package/dist/dsh/host-adapter/admission.js +294 -12
- package/dist/dsh/host-adapter/admission.js.map +1 -1
- package/dist/dsh/host-adapter/routing.d.ts +2 -1
- package/dist/dsh/host-adapter/routing.d.ts.map +1 -1
- package/dist/dsh/host-adapter/routing.js +32 -18
- package/dist/dsh/host-adapter/routing.js.map +1 -1
- package/dist/dsh/host-adapter/turn-state.d.ts +6 -0
- package/dist/dsh/host-adapter/turn-state.d.ts.map +1 -1
- package/dist/dsh/host-adapter/turn-state.js +19 -1
- package/dist/dsh/host-adapter/turn-state.js.map +1 -1
- package/dist/dsh/host-adapter.d.ts.map +1 -1
- package/dist/dsh/host-adapter.js +71 -8
- package/dist/dsh/host-adapter.js.map +1 -1
- package/dist/dsh/intake-clarification.d.ts +2 -2
- package/dist/dsh/intake-clarification.d.ts.map +1 -1
- package/dist/dsh/intake-clarification.js +13 -4
- package/dist/dsh/intake-clarification.js.map +1 -1
- package/dist/dsh/intake-gate.d.ts +5 -0
- package/dist/dsh/intake-gate.d.ts.map +1 -1
- package/dist/dsh/intake-gate.js +19 -5
- package/dist/dsh/intake-gate.js.map +1 -1
- package/dist/dsh/lisp/candidate.d.ts +1 -1
- package/dist/dsh/lisp/surface.d.ts +1 -1
- package/dist/dsh/memory-owner.d.ts +24 -0
- package/dist/dsh/memory-owner.d.ts.map +1 -0
- package/dist/dsh/memory-owner.js +35 -0
- package/dist/dsh/memory-owner.js.map +1 -0
- package/dist/dsh/memory-request-fence.d.ts +37 -0
- package/dist/dsh/memory-request-fence.d.ts.map +1 -0
- package/dist/dsh/memory-request-fence.js +48 -0
- package/dist/dsh/memory-request-fence.js.map +1 -0
- package/dist/dsh/message-sources.d.ts.map +1 -1
- package/dist/dsh/message-sources.js +2 -1
- package/dist/dsh/message-sources.js.map +1 -1
- package/dist/dsh/models/profiles.d.ts +23 -0
- package/dist/dsh/models/profiles.d.ts.map +1 -1
- package/dist/dsh/models/profiles.js +30 -1
- package/dist/dsh/models/profiles.js.map +1 -1
- package/dist/dsh/models/service.d.ts +9 -0
- package/dist/dsh/models/service.d.ts.map +1 -1
- package/dist/dsh/models/service.js +52 -13
- package/dist/dsh/models/service.js.map +1 -1
- package/dist/dsh/on-demand-intake.d.ts +71 -5
- package/dist/dsh/on-demand-intake.d.ts.map +1 -1
- package/dist/dsh/on-demand-intake.js +299 -70
- package/dist/dsh/on-demand-intake.js.map +1 -1
- package/dist/dsh/owned-execution.d.ts +3 -0
- package/dist/dsh/owned-execution.d.ts.map +1 -1
- package/dist/dsh/owned-execution.js +7 -0
- package/dist/dsh/owned-execution.js.map +1 -1
- package/dist/dsh/request-memory.d.ts +2 -0
- package/dist/dsh/request-memory.d.ts.map +1 -1
- package/dist/dsh/request-memory.js +33 -2
- package/dist/dsh/request-memory.js.map +1 -1
- package/dist/dsh/setup.d.ts +2 -2
- package/dist/dsh/setup.d.ts.map +1 -1
- package/dist/dsh/setup.js +37 -14
- package/dist/dsh/setup.js.map +1 -1
- package/dist/dsh/skill-prompts.json +1 -1
- package/dist/dsh/standard-skills.d.ts +2 -1
- package/dist/dsh/standard-skills.d.ts.map +1 -1
- package/dist/dsh/standard-skills.js +2 -1
- package/dist/dsh/standard-skills.js.map +1 -1
- package/dist/dsh/task-intake.d.ts +6 -0
- package/dist/dsh/task-intake.d.ts.map +1 -1
- package/dist/dsh/task-intake.js +7 -2
- package/dist/dsh/task-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/coding-workflow.md +3 -1
- package/docs/dsh-plugin.md +28 -5
- package/docs/on-demand-intake.md +13 -7
- package/package.json +3 -2
- package/skills/japanese-translation-for-oss-models/SKILL.md +5 -3
|
@@ -1,13 +1,23 @@
|
|
|
1
|
+
import { ExecutionSelectionPending } from './execution-pending.js';
|
|
1
2
|
import { executionClarification } from './intake-clarification.js';
|
|
2
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { z } from 'zod';
|
|
3
5
|
import { TASK_TYPES } from '../akinator/types.js';
|
|
4
6
|
import { canonicalContentHash } from '../serialization/validate.js';
|
|
5
7
|
import { KIOKUKO_DSH_SOURCE_KIND } from './plugin-source.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
+
import { bindMainModelRequestIntent, requestIntentReferenceIds, requestIntentTaskType, RequestIntentSubmissionSchema } from './decisions/request-intent.js';
|
|
9
|
+
import { memoryOwnerKind } from './memory-owner.js';
|
|
8
10
|
export const TASK_PREPARE_TOOL = 'prepare_requested_work';
|
|
9
|
-
export const ON_DEMAND_GUIDANCE = 'The original user request may be answered now without selecting a task category. No execution task has been prepared yet. Preserve
|
|
11
|
+
export const ON_DEMAND_GUIDANCE = 'The original user request may be answered now without selecting a task category. No execution task has been prepared yet. 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; 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.';
|
|
10
12
|
const ACTION_TYPES = TASK_TYPES.filter(type => type !== 'chat');
|
|
13
|
+
const preparationFieldsSchema = z.object({
|
|
14
|
+
taskType: z.enum(ACTION_TYPES).optional(),
|
|
15
|
+
requestIntent: RequestIntentSubmissionSchema.optional(),
|
|
16
|
+
}).strict();
|
|
17
|
+
function parsePreparationRequest(value) {
|
|
18
|
+
const parsed = preparationFieldsSchema.safeParse(value);
|
|
19
|
+
return parsed.success && (parsed.data.taskType !== undefined || parsed.data.requestIntent !== undefined) ? parsed.data : undefined;
|
|
20
|
+
}
|
|
11
21
|
function humanMessages(messages) {
|
|
12
22
|
return messages.filter((value) => value?.role === 'user' && (!value.source || value.source.kind === 'user'));
|
|
13
23
|
}
|
|
@@ -21,9 +31,17 @@ function eventTurn(event) {
|
|
|
21
31
|
return Number.isSafeInteger(data?.turn) ? data.turn : undefined;
|
|
22
32
|
}
|
|
23
33
|
/** Tool names select advice only; admission and native permissions remain authoritative. */
|
|
24
|
-
function automaticPreparationType(intent,
|
|
25
|
-
if (
|
|
26
|
-
|
|
34
|
+
function automaticPreparationType(intent, requestIntent, name) {
|
|
35
|
+
if (requestIntent?.relation === 'unknown' || requestIntent?.action === 'unknown') {
|
|
36
|
+
// Gathering evidence grants no change/process authority. An active Goal
|
|
37
|
+
// still requires its reference to be resolved before opening another run.
|
|
38
|
+
return ['read', 'glob', 'grep', 'web_search', 'web_fetch'].includes(name) ? 'research' : undefined;
|
|
39
|
+
}
|
|
40
|
+
if (requestIntent?.action === 'answer')
|
|
41
|
+
return undefined;
|
|
42
|
+
const interpreted = requestIntentTaskType(requestIntent, intent.taskType);
|
|
43
|
+
if (interpreted && interpreted !== 'chat')
|
|
44
|
+
return interpreted;
|
|
27
45
|
if (intent.taskType && intent.taskType !== 'chat')
|
|
28
46
|
return intent.taskType;
|
|
29
47
|
return ['read', 'glob', 'grep', 'web_search', 'web_fetch'].includes(name) ? 'research' : undefined;
|
|
@@ -35,7 +53,7 @@ export function preparationCarrier(value) {
|
|
|
35
53
|
return undefined;
|
|
36
54
|
const args = value;
|
|
37
55
|
if (Object.keys(args).sort().join(',') !== 'code,description' || typeof args.description !== 'string' || !args.description.trim()
|
|
38
|
-
|| typeof args.code !== 'string' || args.code.length >
|
|
56
|
+
|| typeof args.code !== 'string' || args.code.length > 8192)
|
|
39
57
|
return undefined;
|
|
40
58
|
const code = args.code.trim().replace(/;$/u, '');
|
|
41
59
|
const prefix = 'return await tools.prepare_requested_work(';
|
|
@@ -43,10 +61,10 @@ export function preparationCarrier(value) {
|
|
|
43
61
|
return undefined;
|
|
44
62
|
try {
|
|
45
63
|
const input = JSON.parse(code.slice(prefix.length, -1));
|
|
46
|
-
|
|
47
|
-
|
|
64
|
+
const parsed = parsePreparationRequest(input);
|
|
65
|
+
if (!parsed || code !== prefix + JSON.stringify(input) + ')')
|
|
48
66
|
return undefined;
|
|
49
|
-
return
|
|
67
|
+
return parsed;
|
|
50
68
|
}
|
|
51
69
|
catch {
|
|
52
70
|
return undefined;
|
|
@@ -78,6 +96,15 @@ export class OnDemandIntake {
|
|
|
78
96
|
const last = execution.agent?.session?.snapshotEvents?.().filter(event => event.type === 'turn/start' || event.type === 'turn/end').at(-1);
|
|
79
97
|
return last?.type === 'turn/start' ? eventTurn(last) : undefined;
|
|
80
98
|
}
|
|
99
|
+
acknowledgeExisting(state) {
|
|
100
|
+
this.assertCurrent(state);
|
|
101
|
+
state.existing = true;
|
|
102
|
+
state.status = 'prepared';
|
|
103
|
+
const taskType = requestIntentTaskType(state.requestIntent, state.intent.taskType);
|
|
104
|
+
if (taskType !== undefined)
|
|
105
|
+
state.taskType = taskType;
|
|
106
|
+
state.preparedIntentDigest = canonicalContentHash({ taskType: state.taskType ?? null, requestIntent: state.requestIntent ?? null });
|
|
107
|
+
}
|
|
81
108
|
/** True means ordinary text may continue; false leaves an existing execution on its original path. */
|
|
82
109
|
async capture(input) {
|
|
83
110
|
input.signal.throwIfAborted();
|
|
@@ -98,6 +125,8 @@ export class OnDemandIntake {
|
|
|
98
125
|
if (!fresh.length) {
|
|
99
126
|
if (previous.capture && !await previous.capture)
|
|
100
127
|
return false;
|
|
128
|
+
if (previous.status === 'stale')
|
|
129
|
+
throw previous.captureError ?? new Error('Previous on-demand request is stale');
|
|
101
130
|
return previous.status !== 'prepared';
|
|
102
131
|
}
|
|
103
132
|
previous.status = 'stale';
|
|
@@ -109,15 +138,34 @@ export class OnDemandIntake {
|
|
|
109
138
|
merged.set(humanId(message), message);
|
|
110
139
|
input = { ...input, messages: [...merged.values()] };
|
|
111
140
|
}
|
|
141
|
+
if (previous.input.turn !== input.turn && !humanMessages(input.messages).length && this.host.ready(input.agent)) {
|
|
142
|
+
// A host-driven next turn may carry no new human message. Leave its
|
|
143
|
+
// active execution bound to the host instead of creating an empty intake.
|
|
144
|
+
if (previous.status === 'closed') {
|
|
145
|
+
await this.host.validate(input);
|
|
146
|
+
input.signal.throwIfAborted();
|
|
147
|
+
await this.host.advance?.(input);
|
|
148
|
+
this.#turns.delete(sessionId);
|
|
149
|
+
this.host.answerContext?.retire(input.agent);
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
112
153
|
if (previous.input.turn !== input.turn && !['closed', 'prepared'].includes(previous.status))
|
|
113
154
|
throw new Error('Previous on-demand turn has not ended');
|
|
114
155
|
previous.controller.abort(new Error('A newer native turn superseded this request'));
|
|
115
156
|
}
|
|
157
|
+
else if (!humanMessages(input.messages).length && this.host.ready(input.agent)) {
|
|
158
|
+
await this.host.validate(input);
|
|
159
|
+
input.signal.throwIfAborted();
|
|
160
|
+
await this.host.advance?.(input);
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
this.host.answerContext?.retire(input.agent);
|
|
116
164
|
// Install the pending state synchronously, before any host or classifier awaits.
|
|
117
165
|
const controller = new AbortController();
|
|
118
166
|
const state = { input: { ...input, signal: AbortSignal.any([input.signal, controller.signal]), messages: Object.freeze(structuredClone([...input.messages])) }, task: humanTask(input.messages),
|
|
119
167
|
humanContents: new Map(humanMessages(input.messages).map(message => [humanId(message), canonicalContentHash(message)])),
|
|
120
|
-
status: 'capturing', intent: { deferInference: true }, preparationCalls: new Set(), controller };
|
|
168
|
+
status: 'capturing', intent: { deferInference: true }, memoryOwner: 'none', preparationCalls: new Set(), controller };
|
|
121
169
|
this.#turns.set(sessionId, state);
|
|
122
170
|
const promise = (async () => {
|
|
123
171
|
await this.host.validate(input);
|
|
@@ -126,18 +174,6 @@ export class OnDemandIntake {
|
|
|
126
174
|
// validated owner's first reader once, including absence; never rebind it.
|
|
127
175
|
if (!this.#skillReaders.has(input.agent))
|
|
128
176
|
this.#skillReaders.set(input.agent, this.#startupSkillReader ?? this.#tools?.get('skill', input.agent)?.execute);
|
|
129
|
-
if (previous?.input.turn !== input.turn && await this.host.existing(input)) {
|
|
130
|
-
this.assertCurrent(state);
|
|
131
|
-
// Retain the exact resumed request for the public preparation receipt.
|
|
132
|
-
// Existing ownership alone grants nothing: readiness is rechecked at dispatch.
|
|
133
|
-
if (state.task) {
|
|
134
|
-
state.existing = true;
|
|
135
|
-
state.status = 'prepared';
|
|
136
|
-
}
|
|
137
|
-
else
|
|
138
|
-
this.#turns.delete(sessionId);
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
177
|
if (!humanMessages(input.messages).length)
|
|
142
178
|
throw new Error('Original human input is required before on-demand intake');
|
|
143
179
|
try {
|
|
@@ -147,21 +183,75 @@ export class OnDemandIntake {
|
|
|
147
183
|
input.signal.throwIfAborted();
|
|
148
184
|
state.intent = { deferInference: true };
|
|
149
185
|
}
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
153
|
-
state.
|
|
186
|
+
try {
|
|
187
|
+
const interpreted = await this.host.interpret?.(state.input, state.task);
|
|
188
|
+
if (interpreted) {
|
|
189
|
+
state.requestIntentContext = interpreted.context;
|
|
190
|
+
state.requestIntent = interpreted.decision;
|
|
191
|
+
state.requestIntentContextDigest = canonicalContentHash(interpreted.context);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
input.signal.throwIfAborted();
|
|
154
196
|
}
|
|
197
|
+
state.memoryOwner = memoryOwnerKind(state.requestIntent, state.intent.taskType);
|
|
155
198
|
this.assertCurrent(state);
|
|
199
|
+
if (previous?.input.turn !== input.turn)
|
|
200
|
+
await this.host.advance?.(input, state.requestIntent);
|
|
201
|
+
this.assertCurrent(state);
|
|
202
|
+
const resumeConfirmation = previous?.input.turn !== input.turn && state.task
|
|
203
|
+
? await this.host.resumeConfirmation?.(input) ?? false : false;
|
|
204
|
+
if (process.env.KIOKUKO_INTENT_DEBUG === '1' && previous?.input.turn !== input.turn) {
|
|
205
|
+
console.error('on-demand confirmation route', { turn: input.turn, previousTurn: previous?.input.turn,
|
|
206
|
+
hasTask: state.task.length > 0, resumeConfirmation });
|
|
207
|
+
}
|
|
208
|
+
if (resumeConfirmation) {
|
|
209
|
+
this.assertCurrent(state);
|
|
210
|
+
state.confirmationResume = true;
|
|
211
|
+
state.status = 'answer';
|
|
212
|
+
if (previous?.taskType !== undefined)
|
|
213
|
+
state.taskType = previous.taskType;
|
|
214
|
+
this.host.answerContext?.retire(input.agent);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
if (process.env.KIOKUKO_INTENT_DEBUG === '1' && previous?.input.turn !== input.turn && state.task && this.host.resumeConfirmation) {
|
|
218
|
+
console.error('confirmation resume not selected', input.turn, state.task.length);
|
|
219
|
+
}
|
|
220
|
+
if (state.task && await this.host.replay?.(state.input)) {
|
|
221
|
+
this.acknowledgeExisting(state);
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
const canContinue = state.requestIntent?.relation === 'same_scope_continuation'
|
|
225
|
+
&& state.requestIntent.action !== 'answer' && state.requestIntent.action !== 'unknown';
|
|
226
|
+
if (previous?.input.turn !== input.turn && state.task && canContinue
|
|
227
|
+
&& await this.host.existing(input, state.requestIntent)) {
|
|
228
|
+
// Persisted ownership is only an acknowledgement when this exact native
|
|
229
|
+
// agent/session/turn is already bound. A cold resume stays conversational
|
|
230
|
+
// until explicit preparation reconstructs that binding.
|
|
231
|
+
if (this.host.ready(state.input.agent, state.input.turn)) {
|
|
232
|
+
this.acknowledgeExisting(state);
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
156
236
|
// Eager mode still binds known work before generation. Classifier uncertainty
|
|
157
237
|
// instead reaches ordinary reasoning, with no execution grant or purpose UI.
|
|
158
|
-
|
|
238
|
+
const requestIntentUnresolved = state.requestIntent !== undefined
|
|
239
|
+
&& (state.requestIntent.relation === 'unknown' || state.requestIntent.action === 'unknown');
|
|
240
|
+
const requestAsAnswer = state.requestIntent?.action === 'answer';
|
|
241
|
+
const knownIntent = this.host.interpret === undefined
|
|
242
|
+
? !state.intent.deferInference && state.intent.taskType !== undefined && state.intent.taskType !== 'chat'
|
|
243
|
+
: state.requestIntent !== undefined && !requestIntentUnresolved && !requestAsAnswer
|
|
244
|
+
&& state.requestIntent.relation !== 'unknown' && state.requestIntent.action !== 'unknown';
|
|
245
|
+
if (this.options.onlyWhenDeferred && knownIntent) {
|
|
159
246
|
this.#turns.delete(sessionId);
|
|
160
247
|
state.status = 'closed';
|
|
161
248
|
controller.abort(new Error('Known intent continues through eager admission'));
|
|
162
249
|
return false;
|
|
163
250
|
}
|
|
164
|
-
|
|
251
|
+
// The current request is on the conversational path until actual admission.
|
|
252
|
+
// Provisional intent cannot select a different run's memory validator.
|
|
253
|
+
state.memoryOwner = 'conversation';
|
|
254
|
+
await this.host.answerContext?.prepare(state.input, state.task, state.intent.taskType ?? null, state.memoryOwner);
|
|
165
255
|
this.assertCurrent(state);
|
|
166
256
|
state.status = 'answer';
|
|
167
257
|
return true;
|
|
@@ -172,8 +262,10 @@ export class OnDemandIntake {
|
|
|
172
262
|
return await promise;
|
|
173
263
|
}
|
|
174
264
|
catch (error) {
|
|
175
|
-
if (this.#turns.get(sessionId) === state)
|
|
265
|
+
if (this.#turns.get(sessionId) === state) {
|
|
176
266
|
state.status = 'stale';
|
|
267
|
+
state.captureError = error;
|
|
268
|
+
}
|
|
177
269
|
throw error;
|
|
178
270
|
}
|
|
179
271
|
finally {
|
|
@@ -197,6 +289,11 @@ export class OnDemandIntake {
|
|
|
197
289
|
return undefined;
|
|
198
290
|
return state;
|
|
199
291
|
}
|
|
292
|
+
ownerIdentity(state) {
|
|
293
|
+
const session = state.input.agent.session;
|
|
294
|
+
return { agentId: state.input.agent.id, sessionId: session.id, turn: state.input.turn,
|
|
295
|
+
requestDigest: canonicalContentHash(state.task) };
|
|
296
|
+
}
|
|
200
297
|
definition(execution) {
|
|
201
298
|
try {
|
|
202
299
|
return this.#tools?.get(execution.name, execution.agent);
|
|
@@ -205,17 +302,26 @@ export class OnDemandIntake {
|
|
|
205
302
|
return undefined;
|
|
206
303
|
}
|
|
207
304
|
}
|
|
208
|
-
async prepare(state, taskType, signal) {
|
|
305
|
+
async prepare(state, taskType, signal, requestIntent, explicitIntent = false) {
|
|
209
306
|
this.assertCurrent(state);
|
|
210
307
|
signal.throwIfAborted();
|
|
211
|
-
|
|
308
|
+
if (state.selectionPending)
|
|
309
|
+
throw new ExecutionSelectionPending();
|
|
310
|
+
const resolved = requestIntent ?? state.requestIntent;
|
|
311
|
+
const clarification = executionClarification(state.task, resolved, state.requestIntentContext);
|
|
212
312
|
if (clarification)
|
|
213
313
|
throw new Error(clarification);
|
|
214
314
|
if (!ACTION_TYPES.includes(taskType))
|
|
215
315
|
throw new Error('Answer conversational questions directly; prepare_requested_work is for requested work');
|
|
316
|
+
if (state.status === 'prepared' && state.confirmationResume && explicitIntent
|
|
317
|
+
&& (state.requestIntent?.relation === 'unknown' || state.requestIntent?.action === 'unknown'))
|
|
318
|
+
state.status = 'answer';
|
|
216
319
|
if (state.status === 'prepared') {
|
|
217
320
|
if (state.taskType !== taskType)
|
|
218
321
|
throw new Error('Prepared intent cannot be replaced within a turn');
|
|
322
|
+
if (explicitIntent && state.preparedIntentDigest !== canonicalContentHash({ taskType, requestIntent: requestIntent ?? null })) {
|
|
323
|
+
throw new Error('Prepared interpretation cannot be replaced within a turn');
|
|
324
|
+
}
|
|
219
325
|
return;
|
|
220
326
|
}
|
|
221
327
|
if (state.preparation) {
|
|
@@ -227,12 +333,17 @@ export class OnDemandIntake {
|
|
|
227
333
|
state.taskType = taskType;
|
|
228
334
|
const operation = (async () => {
|
|
229
335
|
// Retain exact original messages. The only caller-supplied input is an advisory task type.
|
|
230
|
-
const admitted = await this.host.prepare({ ...state.input, signal: AbortSignal.any([state.input.signal, signal]) }, taskType);
|
|
336
|
+
const admitted = await this.host.prepare({ ...state.input, signal: AbortSignal.any([state.input.signal, signal]) }, taskType, requestIntent ?? state.requestIntent, state.requestIntentContext);
|
|
231
337
|
this.assertCurrent(state);
|
|
232
338
|
signal.throwIfAborted();
|
|
233
339
|
if (!admitted || !this.host.ready(state.input.agent, state.input.turn))
|
|
234
340
|
throw new Error('Execution preparation did not admit this exact request');
|
|
341
|
+
const preparedIntent = requestIntent ?? state.requestIntent;
|
|
342
|
+
if (preparedIntent !== undefined)
|
|
343
|
+
state.requestIntent = preparedIntent;
|
|
344
|
+
state.preparedIntentDigest = canonicalContentHash({ taskType, requestIntent: state.requestIntent ?? null });
|
|
235
345
|
state.status = 'prepared';
|
|
346
|
+
delete state.confirmationResume;
|
|
236
347
|
})();
|
|
237
348
|
this.watch(operation);
|
|
238
349
|
state.preparation = operation;
|
|
@@ -240,30 +351,76 @@ export class OnDemandIntake {
|
|
|
240
351
|
await operation;
|
|
241
352
|
}
|
|
242
353
|
catch (error) {
|
|
243
|
-
if (!['closed', 'stale'].includes(state.status))
|
|
244
|
-
|
|
354
|
+
if (!['closed', 'stale'].includes(state.status)) {
|
|
355
|
+
// A declined selection preserves this request for a conversational acknowledgement.
|
|
356
|
+
// It is not admission: preparation remains consumed and tools still need a ready host.
|
|
357
|
+
state.selectionPending = error instanceof ExecutionSelectionPending && !state.input.signal.aborted && !signal.aborted;
|
|
358
|
+
state.status = state.selectionPending ? 'answer' : 'stale';
|
|
359
|
+
}
|
|
245
360
|
throw error;
|
|
246
361
|
}
|
|
247
362
|
finally {
|
|
248
363
|
delete state.preparation;
|
|
249
364
|
}
|
|
250
365
|
}
|
|
366
|
+
bindIntent(state, submission) {
|
|
367
|
+
if (!submission)
|
|
368
|
+
return state.requestIntent;
|
|
369
|
+
if (!state.requestIntentContext || state.requestIntentContextDigest !== canonicalContentHash(state.requestIntentContext)) {
|
|
370
|
+
throw new Error('Request interpretation context is stale');
|
|
371
|
+
}
|
|
372
|
+
const bound = bindMainModelRequestIntent(submission, state.requestIntentContext);
|
|
373
|
+
if (!bound)
|
|
374
|
+
throw new Error('Request interpretation references another request, stale revision, or unknown proposal');
|
|
375
|
+
const prior = state.requestIntent;
|
|
376
|
+
if (prior?.source === 'decision-provider' && prior.relation !== 'unknown' && prior.action !== 'unknown') {
|
|
377
|
+
if (prior.relation !== bound.relation || prior.action !== bound.action) {
|
|
378
|
+
throw new Error('Main-model interpretation conflicts with the configured decision provider');
|
|
379
|
+
}
|
|
380
|
+
return { ...prior, evidence: bound.evidence };
|
|
381
|
+
}
|
|
382
|
+
return bound;
|
|
383
|
+
}
|
|
251
384
|
async answerMessages(input, messages) {
|
|
252
|
-
|
|
385
|
+
const state = this.#turns.get(input.agent.session.id);
|
|
386
|
+
// Native pre-step may augment its payload while downstream hooks run.
|
|
387
|
+
// Project the immutable human batch captured before that waterfall.
|
|
388
|
+
if (state?.input.turn === input.turn && state.input.agent !== input.agent)
|
|
389
|
+
throw new Error('Answer projection native agent changed');
|
|
390
|
+
const projected = state?.input.turn === input.turn && !state.confirmationResume && this.host.answerContext
|
|
391
|
+
? await this.host.answerContext.project(state.input, messages, this.ownerIdentity(state)) : messages;
|
|
392
|
+
return this.guidance(projected, state?.input.turn === input.turn ? state : undefined);
|
|
393
|
+
}
|
|
394
|
+
async beforeRequest(agent) {
|
|
395
|
+
const state = agent.session && this.#turns.get(agent.session.id);
|
|
396
|
+
if (state && state.input.agent === agent && state.status === 'answer' && this.pending(agent)) {
|
|
397
|
+
await this.host.answerContext?.beforeRequest(state.input, this.ownerIdentity(state));
|
|
398
|
+
}
|
|
253
399
|
}
|
|
254
|
-
async beforeRequest(agent) { if (this.pending(agent))
|
|
255
|
-
await this.host.answerContext?.beforeRequest(agent); }
|
|
256
400
|
async fence(agent, request) {
|
|
257
|
-
|
|
258
|
-
|
|
401
|
+
const state = agent.session && this.#turns.get(agent.session.id);
|
|
402
|
+
if (state && state.input.agent === agent && state.status === 'answer' && this.pending(agent)) {
|
|
403
|
+
await this.host.answerContext?.fence(state.input, request, this.ownerIdentity(state));
|
|
404
|
+
}
|
|
259
405
|
}
|
|
260
|
-
guidance(messages) {
|
|
261
|
-
|
|
262
|
-
|
|
406
|
+
guidance(messages, state) {
|
|
407
|
+
const context = state?.requestIntentContext;
|
|
408
|
+
const interpretation = state?.requestIntent;
|
|
409
|
+
const intentText = context ? `\n\nCurrent request references and same-session work (host-bound, advisory only):\n${JSON.stringify({
|
|
410
|
+
references: requestIntentReferenceIds(context),
|
|
411
|
+
activeGoal: context.activeGoal ?? null,
|
|
412
|
+
proposals: context.proposals.map(({ id, revision, digest, text }) => ({ id, revision, digest, text })),
|
|
413
|
+
decision: interpretation ? { relation: interpretation.relation, action: interpretation.action, source: interpretation.source,
|
|
414
|
+
policyVersion: interpretation.policyVersion, evidence: interpretation.evidence, unresolvedReason: interpretation.unresolvedReason ?? null } : null,
|
|
415
|
+
})}\nUse only these exact references in requestIntent.evidenceRefs. The original human message remains unchanged.` : '';
|
|
416
|
+
const text = ON_DEMAND_GUIDANCE + intentText;
|
|
417
|
+
return [...messages, { id: randomUUID(), role: 'user', content: [{ type: 'text', text }],
|
|
418
|
+
source: { kind: KIOKUKO_DSH_SOURCE_KIND, form: 'snapshot', sections: [{ name: 'on-demand-intake', text }] } }];
|
|
263
419
|
}
|
|
264
420
|
snapshot(sessionId) {
|
|
265
421
|
const state = this.#turns.get(sessionId);
|
|
266
|
-
return state && { task: state.task, taskType: state.taskType ?? state.intent.taskType ?? null, status: state.status, turn: state.input.turn
|
|
422
|
+
return state && { task: state.task, taskType: state.taskType ?? state.intent.taskType ?? null, status: state.status, turn: state.input.turn,
|
|
423
|
+
...(state.requestIntent ? { requestIntent: structuredClone(state.requestIntent) } : {}), memoryOwner: state.memoryOwner };
|
|
267
424
|
}
|
|
268
425
|
/** A current host-validated control call, never arbitrary code or a name-based exemption. */
|
|
269
426
|
preparationOnly(execution) {
|
|
@@ -273,13 +430,14 @@ export class OnDemandIntake {
|
|
|
273
430
|
|| state.preparationCalls.has(execution.callId))
|
|
274
431
|
return false;
|
|
275
432
|
if (proof.carrier)
|
|
276
|
-
return execution.name === 'run_code' && state.status === 'answer'
|
|
433
|
+
return execution.name === 'run_code' && state.status === 'answer'
|
|
434
|
+
&& canonicalContentHash(preparationCarrier(execution.arguments) ?? null) === proof.carrierDigest;
|
|
277
435
|
return proof.requestPreparation === true && execution.name === TASK_PREPARE_TOOL
|
|
278
436
|
&& (state.status === 'answer' || state.existing === true && state.status === 'prepared' && this.host.ready(state.input.agent, state.input.turn));
|
|
279
437
|
}
|
|
280
438
|
pending(agent) {
|
|
281
439
|
const state = agent.session && this.#turns.get(agent.session.id);
|
|
282
|
-
return !!state && state.input.agent === agent && !['prepared', 'closed', 'stale'].includes(state.status);
|
|
440
|
+
return !!state && !state.confirmationResume && state.input.agent === agent && !['prepared', 'closed', 'stale'].includes(state.status);
|
|
283
441
|
}
|
|
284
442
|
/** An admitted host continuation still needs capture to retire the previous turn's receipt. */
|
|
285
443
|
continuing(agent) { return !this.#stopped && this.host.ready(agent); }
|
|
@@ -288,6 +446,7 @@ export class OnDemandIntake {
|
|
|
288
446
|
if (state?.input.agent.session === session && state.input.turn === turn) {
|
|
289
447
|
state.status = 'closed';
|
|
290
448
|
state.controller.abort(new Error('Native turn ended'));
|
|
449
|
+
this.host.answerContext?.retire(state.input.agent);
|
|
291
450
|
}
|
|
292
451
|
}
|
|
293
452
|
mount(context, tools, systemPrompt) {
|
|
@@ -298,27 +457,66 @@ export class OnDemandIntake {
|
|
|
298
457
|
// When absent, capture binds the validated agent's installed preset reader.
|
|
299
458
|
this.#startupSkillReader = tools.get('skill')?.execute;
|
|
300
459
|
if (systemPrompt && !this.options.onlyWhenDeferred)
|
|
301
|
-
this.#disposers.push(systemPrompt.section({ name: 'kiokuko:on-demand-intake', order: -99999, text: 'On-demand host contract: text answers do not require execution intake. The public prepare_requested_work tool accepts an advisory
|
|
460
|
+
this.#disposers.push(systemPrompt.section({ name: 'kiokuko:on-demand-intake', order: -99999, text: 'On-demand host contract: text answers do not require execution intake. The public prepare_requested_work tool accepts an advisory taskType or a source-bound requestIntent; task_prepare/task_answer remain host-only. ' + ON_DEMAND_GUIDANCE }));
|
|
302
461
|
const definition = { name: TASK_PREPARE_TOOL, modelFacing: true,
|
|
303
|
-
description: 'Prepare the original user request for tool-backed work. Supply
|
|
304
|
-
parameters: {
|
|
462
|
+
description: 'Prepare the exact original user request for tool-backed work. Supply either legacy advisory taskType or requestIntent with exact evidenceRefs from the host context; never rewrite the task or invent a target. This does not grant native tool permissions. Answer ordinary questions directly; clarify unknown actions or targets first.',
|
|
463
|
+
parameters: { ...JSON.parse(JSON.stringify(z.toJSONSchema(preparationFieldsSchema))), anyOf: [{ required: ['taskType'] }, { required: ['requestIntent'] }] },
|
|
305
464
|
output: { schema: {}, render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }] },
|
|
306
465
|
execute: async (args, execution) => {
|
|
307
466
|
const state = this.bound(execution);
|
|
308
|
-
const value = args;
|
|
309
|
-
if (!state || execution.name !== TASK_PREPARE_TOOL || this.definition(execution)?.execute !== definition.execute
|
|
310
|
-
|| !value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length !== 1 || !ACTION_TYPES.includes(value.taskType))
|
|
467
|
+
const value = parsePreparationRequest(args);
|
|
468
|
+
if (!state || execution.name !== TASK_PREPARE_TOOL || this.definition(execution)?.execute !== definition.execute || !value)
|
|
311
469
|
throw new Error('Invalid task preparation request');
|
|
312
|
-
if (state.preparationCalls.has(execution.callId) || !(state.status === 'answer' || state.
|
|
470
|
+
if (state.preparationCalls.has(execution.callId) || !(state.status === 'answer' || state.status === 'prepared' && this.host.ready(state.input.agent, state.input.turn)))
|
|
313
471
|
throw new Error('Task preparation was already consumed for this turn');
|
|
314
472
|
state.preparationCalls.add(execution.callId);
|
|
315
473
|
if (state.existing) {
|
|
316
474
|
this.assertCurrent(state);
|
|
317
475
|
execution.signal.throwIfAborted();
|
|
476
|
+
const boundIntent = this.bindIntent(state, value.requestIntent);
|
|
477
|
+
if (boundIntent?.relation === 'same_scope_continuation' && boundIntent.action !== 'answer' && boundIntent.action !== 'unknown') {
|
|
478
|
+
const intentType = requestIntentTaskType(boundIntent, state.taskType ?? state.intent.taskType ?? value.taskType);
|
|
479
|
+
if (value.taskType !== undefined && intentType !== undefined && value.taskType !== intentType) {
|
|
480
|
+
throw new Error('The advisory taskType conflicts with the bound request action');
|
|
481
|
+
}
|
|
482
|
+
state.requestIntent = boundIntent;
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
if (!boundIntent || boundIntent.relation === 'unknown' || boundIntent.action === 'unknown' || boundIntent.action === 'answer') {
|
|
486
|
+
throw new Error('Clarify whether this request continues, changes, or starts work before preparing it');
|
|
487
|
+
}
|
|
488
|
+
const intentType = requestIntentTaskType(boundIntent, state.taskType ?? state.intent.taskType ?? value.taskType);
|
|
489
|
+
if (!intentType || intentType === 'chat' || value.taskType !== undefined && value.taskType !== intentType) {
|
|
490
|
+
throw new Error('The advisory taskType conflicts with the bound request action');
|
|
491
|
+
}
|
|
492
|
+
state.existing = false;
|
|
493
|
+
state.status = 'answer';
|
|
494
|
+
state.memoryOwner = memoryOwnerKind(boundIntent, intentType);
|
|
495
|
+
await this.prepare(state, intentType, execution.signal, boundIntent, value.requestIntent !== undefined);
|
|
496
|
+
}
|
|
318
497
|
}
|
|
319
|
-
else
|
|
320
|
-
|
|
321
|
-
|
|
498
|
+
else {
|
|
499
|
+
const suppliedIntent = this.bindIntent(state, value.requestIntent);
|
|
500
|
+
const currentIntent = suppliedIntent ?? state.requestIntent;
|
|
501
|
+
const unresolved = currentIntent?.relation === 'unknown' || currentIntent?.action === 'unknown';
|
|
502
|
+
if (unresolved && (state.requestIntentContext?.activeGoal || value.requestIntent !== undefined || value.taskType === undefined)) {
|
|
503
|
+
throw new Error('Clarify the request before preparing work');
|
|
504
|
+
}
|
|
505
|
+
if (state.requestIntentContext?.activeGoal && currentIntent === undefined) {
|
|
506
|
+
throw new Error('Clarify whether this request continues, changes, or starts the active Goal');
|
|
507
|
+
}
|
|
508
|
+
const intentType = unresolved ? undefined : requestIntentTaskType(currentIntent, state.taskType ?? state.intent.taskType ?? value.taskType);
|
|
509
|
+
if (value.taskType !== undefined && intentType !== undefined && value.taskType !== intentType) {
|
|
510
|
+
throw new Error('The advisory taskType conflicts with the bound request action');
|
|
511
|
+
}
|
|
512
|
+
const taskType = intentType ?? value.taskType;
|
|
513
|
+
if (!taskType || taskType === 'chat' || !ACTION_TYPES.includes(taskType))
|
|
514
|
+
throw new Error('Answer ordinary questions directly; clarify unknown actions before preparing work');
|
|
515
|
+
state.memoryOwner = memoryOwnerKind(currentIntent, taskType);
|
|
516
|
+
await this.prepare(state, taskType, execution.signal, currentIntent, value.requestIntent !== undefined);
|
|
517
|
+
}
|
|
518
|
+
return { prepared: true, taskType: state.taskType ?? value.taskType ?? requestIntentTaskType(state.requestIntent), originalTask: state.task,
|
|
519
|
+
...(state.requestIntent ? { requestIntent: state.requestIntent } : {}), memoryOwner: state.memoryOwner, permissions: 'unchanged_native_policy' };
|
|
322
520
|
} };
|
|
323
521
|
this.#disposers.push(tools.register(definition));
|
|
324
522
|
this.#disposers.push(context.on('tools/pre-execute', async (execution, next) => {
|
|
@@ -343,25 +541,29 @@ export class OnDemandIntake {
|
|
|
343
541
|
}
|
|
344
542
|
try {
|
|
345
543
|
this.assertCurrent(state);
|
|
544
|
+
if (state.confirmationResume && execution.name !== TASK_PREPARE_TOOL && this.host.ready(state.input.agent, state.input.turn))
|
|
545
|
+
this.acknowledgeExisting(state);
|
|
346
546
|
const skillReader = this.#skillReaders.get(state.input.agent);
|
|
347
547
|
const skillRead = execution.name === 'skill' && skillReader !== undefined && actual.execute === skillReader && state.status === 'answer';
|
|
348
|
-
const carrier = execution.name === 'run_code'
|
|
349
|
-
|
|
548
|
+
const carrier = execution.name === 'run_code' ? preparationCarrier(execution.arguments) : undefined;
|
|
549
|
+
const carrierDigest = carrier ? canonicalContentHash(carrier) : undefined;
|
|
550
|
+
if (execution.name === 'run_code' && state.status !== 'prepared' && !carrier
|
|
551
|
+
&& !automaticPreparationType(state.intent, state.requestIntent, execution.name))
|
|
350
552
|
return denied('Clarify the requested work before code execution.');
|
|
351
553
|
if (carrier) {
|
|
352
|
-
if (state.status
|
|
554
|
+
if (!(state.status === 'answer' || state.status === 'prepared' && this.host.ready(state.input.agent, state.input.turn)) || state.preparationCalls.has(execution.callId))
|
|
353
555
|
return denied('Preparation carrier is not current');
|
|
354
556
|
}
|
|
355
557
|
else if (execution.name === TASK_PREPARE_TOOL) {
|
|
356
|
-
if (actual.execute !== definition.execute || !(state.status === 'answer' || state.
|
|
558
|
+
if (actual.execute !== definition.execute || !(state.status === 'answer' || state.status === 'prepared' && this.host.ready(state.input.agent, state.input.turn)) || state.preparationCalls.has(execution.callId))
|
|
357
559
|
return denied('Task preparation is not current');
|
|
358
560
|
}
|
|
359
561
|
else if (!skillRead) {
|
|
360
562
|
if (state.status !== 'prepared') {
|
|
361
|
-
const type = automaticPreparationType(state.intent,
|
|
563
|
+
const type = automaticPreparationType(state.intent, state.requestIntent, execution.name);
|
|
362
564
|
if (!type)
|
|
363
565
|
return denied('Before tool-backed work, call prepare_requested_work with its advisory taskType. Clarify unknown actions or targets first; the original request is preserved.');
|
|
364
|
-
await this.prepare(state, type, execution.signal);
|
|
566
|
+
await this.prepare(state, type, execution.signal, state.requestIntent);
|
|
365
567
|
}
|
|
366
568
|
if (!this.host.ready(state.input.agent, state.input.turn))
|
|
367
569
|
return denied('Prepared execution is no longer current');
|
|
@@ -369,7 +571,9 @@ export class OnDemandIntake {
|
|
|
369
571
|
this.assertCurrent(state);
|
|
370
572
|
if (this.definition(execution)?.execute !== actual.execute)
|
|
371
573
|
return denied('Native tool definition changed during preparation');
|
|
372
|
-
this.#executions.set(execution, { turn: state, execute: actual.execute,
|
|
574
|
+
this.#executions.set(execution, { turn: state, execute: actual.execute,
|
|
575
|
+
...(carrier && carrierDigest ? { carrier, carrierDigest } : {}), ...(skillRead ? { skillRead: true } : {}),
|
|
576
|
+
...(execution.name === TASK_PREPARE_TOOL ? { requestPreparation: true } : {}) });
|
|
373
577
|
// Preserve native allow/deny/ask unchanged. Approval is resolved by the native registry AFTER this waterfall.
|
|
374
578
|
const decision = await next();
|
|
375
579
|
// DSH 0.1.5 does not understand cancel and otherwise dispatches it without running guards.
|
|
@@ -392,11 +596,32 @@ export class OnDemandIntake {
|
|
|
392
596
|
execution.signal = AbortSignal.any([execution.signal, state.input.signal, this.#lifecycle.signal]);
|
|
393
597
|
if (!proof.carrier)
|
|
394
598
|
return next();
|
|
395
|
-
|
|
599
|
+
const carrier = preparationCarrier(execution.arguments);
|
|
600
|
+
if (!(state.status === 'answer' || state.status === 'prepared' && this.host.ready(state.input.agent, state.input.turn)) || state.preparationCalls.has(execution.callId)
|
|
601
|
+
|| !carrier || canonicalContentHash(carrier) !== proof.carrierDigest)
|
|
396
602
|
throw new Error('Preparation carrier was already consumed or changed');
|
|
397
603
|
state.preparationCalls.add(execution.callId);
|
|
398
|
-
|
|
399
|
-
const
|
|
604
|
+
const requestIntent = this.bindIntent(state, carrier.requestIntent);
|
|
605
|
+
const clarification = executionClarification(state.task, requestIntent, state.requestIntentContext);
|
|
606
|
+
if (clarification)
|
|
607
|
+
throw new Error(clarification);
|
|
608
|
+
const unresolved = requestIntent?.relation === 'unknown' || requestIntent?.action === 'unknown';
|
|
609
|
+
const legacyTaskTypeAllowed = carrier.taskType !== undefined && !state.requestIntentContext?.activeGoal
|
|
610
|
+
&& (state.requestIntentContext?.proposals.length ?? 0) === 0;
|
|
611
|
+
if (unresolved && !legacyTaskTypeAllowed)
|
|
612
|
+
throw new Error('Clarify the requested action and scope before preparing work');
|
|
613
|
+
const intentType = unresolved ? undefined : requestIntentTaskType(requestIntent, state.taskType ?? state.intent.taskType ?? carrier.taskType);
|
|
614
|
+
const taskType = intentType ?? carrier.taskType;
|
|
615
|
+
if (carrier.taskType !== undefined && taskType !== carrier.taskType)
|
|
616
|
+
throw new Error('The advisory taskType conflicts with the bound request action');
|
|
617
|
+
if (!taskType || taskType === 'chat') {
|
|
618
|
+
throw new Error('Clarify the requested action and scope before preparing work');
|
|
619
|
+
}
|
|
620
|
+
state.memoryOwner = memoryOwnerKind(requestIntent, taskType);
|
|
621
|
+
await this.prepare(state, taskType, execution.signal, requestIntent, carrier.requestIntent !== undefined);
|
|
622
|
+
const result = { prepared: true, taskType: state.taskType, originalTask: state.task,
|
|
623
|
+
...(state.requestIntent ? { requestIntent: state.requestIntent } : {}), memoryOwner: state.memoryOwner,
|
|
624
|
+
permissions: 'unchanged_native_policy', programExecuted: false };
|
|
400
625
|
return { isError: false, value: { logs: ['Host intake prepared. No program was evaluated.'], result }, content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
401
626
|
}, { prepend: true }));
|
|
402
627
|
this.#disposers.push(tools.guard(execution => {
|
|
@@ -417,19 +642,23 @@ export class OnDemandIntake {
|
|
|
417
642
|
if (!proof || proof.turn !== state || proof.execute !== actual.execute || ['closed', 'stale', 'capturing'].includes(state.status))
|
|
418
643
|
return 'Tool execution has no current on-demand preparation';
|
|
419
644
|
if (proof.carrier)
|
|
420
|
-
return state.status === 'answer'
|
|
645
|
+
return (state.status === 'answer' || state.status === 'prepared' && this.host.ready(state.input.agent, state.input.turn))
|
|
646
|
+
&& canonicalContentHash(preparationCarrier(execution.arguments) ?? null) === proof.carrierDigest ? undefined : 'Preparation carrier is not current';
|
|
421
647
|
if (proof.skillRead)
|
|
422
648
|
return state.status === 'answer' && actual.execute === this.#skillReaders.get(state.input.agent) ? undefined : 'Conversational Skill read is not current';
|
|
423
649
|
if (execution.name === TASK_PREPARE_TOOL)
|
|
424
|
-
return actual.execute === definition.execute && (state.status === 'answer' || state.
|
|
650
|
+
return actual.execute === definition.execute && (state.status === 'answer' || state.status === 'prepared' && this.host.ready(state.input.agent, state.input.turn)) ? undefined : 'Task preparation is not current';
|
|
425
651
|
return state.status === 'prepared' && this.host.ready(state.input.agent, state.input.turn) ? undefined : 'Execution task is not prepared';
|
|
426
652
|
}));
|
|
427
653
|
}
|
|
428
654
|
answerOwner(session) {
|
|
429
655
|
if (this.#stopped)
|
|
430
656
|
return undefined;
|
|
431
|
-
const state = this.#turns.get(session.id)
|
|
432
|
-
|
|
657
|
+
const state = this.#turns.get(session.id);
|
|
658
|
+
if (!state || state.status !== 'answer' || state.input.agent.session !== session || state.memoryOwner !== 'conversation')
|
|
659
|
+
return undefined;
|
|
660
|
+
const owner = this.host.answerContext?.owner(session, this.ownerIdentity(state));
|
|
661
|
+
return state.input.agent === owner?.agent ? owner : undefined;
|
|
433
662
|
}
|
|
434
663
|
retire(agent) {
|
|
435
664
|
this.host.answerContext?.retire(agent);
|