kiokuko-dsh 0.2.28 → 0.2.30
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/clarification-context.d.ts.map +1 -1
- package/dist/dsh/clarification-context.js +24 -7
- package/dist/dsh/clarification-context.js.map +1 -1
- package/dist/dsh/coding-workflow/host.d.ts +1 -1
- package/dist/dsh/coding-workflow/host.d.ts.map +1 -1
- package/dist/dsh/coding-workflow/host.js +226 -83
- package/dist/dsh/coding-workflow/host.js.map +1 -1
- package/dist/dsh/core/host.d.ts.map +1 -1
- package/dist/dsh/core/host.js +1026 -315
- 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 +9 -2
- package/dist/dsh/core/tasks.js.map +1 -1
- package/dist/dsh/decisions/intent-assessment.d.ts +1 -0
- package/dist/dsh/decisions/intent-assessment.d.ts.map +1 -1
- package/dist/dsh/decisions/intent-assessment.js +100 -18
- package/dist/dsh/decisions/intent-assessment.js.map +1 -1
- package/dist/dsh/decisions/request-intent.d.ts.map +1 -1
- package/dist/dsh/decisions/request-intent.js +405 -138
- package/dist/dsh/decisions/request-intent.js.map +1 -1
- package/dist/dsh/host-adapter/admission.d.ts +2 -1
- package/dist/dsh/host-adapter/admission.d.ts.map +1 -1
- package/dist/dsh/host-adapter/admission.js +877 -312
- package/dist/dsh/host-adapter/admission.js.map +1 -1
- package/dist/dsh/host-adapter.d.ts.map +1 -1
- package/dist/dsh/host-adapter.js +1 -0
- package/dist/dsh/host-adapter.js.map +1 -1
- package/dist/dsh/on-demand-intake.d.ts +1 -0
- package/dist/dsh/on-demand-intake.d.ts.map +1 -1
- package/dist/dsh/on-demand-intake.js +1120 -316
- package/dist/dsh/on-demand-intake.js.map +1 -1
- package/dist/dsh/request-proposals.d.ts +8 -0
- package/dist/dsh/request-proposals.d.ts.map +1 -0
- package/dist/dsh/request-proposals.js +67 -0
- package/dist/dsh/request-proposals.js.map +1 -0
- package/dist/dsh/skill-prompts.json +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/package.json +2 -2
- package/skills/coding-ideal-routine-skill/SKILL.md +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { isCodingWorkflowStatus } from './coding-workflow/host.js';
|
|
2
2
|
import { symbols } from '@deepseek-ai/cordis';
|
|
3
|
-
import { requestInterpretationError, requestInterpretationRecovery } from './request-interpretation-guidance.js';
|
|
4
|
-
import { collectClarificationContext, clarificationReply, reusableClarificationAnswer } from './clarification-context.js';
|
|
3
|
+
import { requestInterpretationError, requestInterpretationRecovery, } from './request-interpretation-guidance.js';
|
|
4
|
+
import { collectClarificationContext, clarificationReply, reusableClarificationAnswer, } from './clarification-context.js';
|
|
5
5
|
import { abortable } from './boundary-worker.js';
|
|
6
|
-
import { isLispImplementation, isLispNativeCommand, withRootLispAdmission, rememberLispConversation } from './lisp-tool-identity.js';
|
|
6
|
+
import { isLispImplementation, isLispNativeCommand, withRootLispAdmission, rememberLispConversation, } from './lisp-tool-identity.js';
|
|
7
7
|
import { ExecutionSelectionPending } from './execution-pending.js';
|
|
8
8
|
import { executionClarification } from './intake-clarification.js';
|
|
9
9
|
import { randomUUID } from 'node:crypto';
|
|
@@ -11,36 +11,56 @@ import { z } from 'zod';
|
|
|
11
11
|
import { TASK_TYPES } from '../akinator/types.js';
|
|
12
12
|
import { canonicalContentHash } from '../serialization/validate.js';
|
|
13
13
|
import { KIOKUKO_DSH_SOURCE_KIND } from './plugin-source.js';
|
|
14
|
-
import { taskAllowedOperations, taskCanExecute } from './task-state.js';
|
|
15
|
-
import { createRequestIntentContext, unresolvedRequestIntent, validateMainModelRequestIntent, requestIntentReferenceIds, requestIntentRequiredReferenceIds, requestIntentIsUnresolved, requestIntentMessageRefs, requestIntentTaskType, RequestIntentSubmissionSchema } from './decisions/request-intent.js';
|
|
16
|
-
import { memoryOwnerKind } from './memory-owner.js';
|
|
14
|
+
import { taskAllowedOperations, taskCanExecute, } from './task-state.js';
|
|
15
|
+
import { createRequestIntentContext, unresolvedRequestIntent, validateMainModelRequestIntent, requestIntentReferenceIds, requestIntentRequiredReferenceIds, requestIntentIsUnresolved, requestIntentMessageRefs, requestIntentTaskType, RequestIntentSubmissionSchema, } from './decisions/request-intent.js';
|
|
16
|
+
import { memoryOwnerKind, } from './memory-owner.js';
|
|
17
17
|
class RequestClarification extends Error {
|
|
18
18
|
assessment;
|
|
19
|
-
|
|
19
|
+
interpretation;
|
|
20
|
+
candidate;
|
|
21
|
+
constructor(message, assessment = false, interpretation = false, candidate) {
|
|
20
22
|
super(message);
|
|
21
23
|
this.assessment = assessment;
|
|
24
|
+
this.interpretation = interpretation;
|
|
25
|
+
this.candidate = candidate;
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
28
|
export const TASK_PREPARE_TOOL = 'prepare_requested_work';
|
|
25
29
|
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.';
|
|
26
|
-
const ACTION_TYPES = TASK_TYPES.filter(type => type !== 'chat');
|
|
27
|
-
const NO_ACTIVE_GOAL_GUIDANCE = 'The current context has no active Goal. same_scope_continuation may refer to the exact current-session conversation without a formal Goal; it grants no execution permission. scope_change requires an active Goal or
|
|
28
|
-
const preparationFieldsSchema = z
|
|
30
|
+
const ACTION_TYPES = TASK_TYPES.filter((type) => type !== 'chat');
|
|
31
|
+
const NO_ACTIVE_GOAL_GUIDANCE = 'The current context has no active Goal. same_scope_continuation may refer to the exact current-session conversation without a formal Goal; it grants no execution permission. scope_change requires an active Goal, task, or an explicitly selected current-session proposal. A proposal is evidence, never approval by itself. If the original request clearly specifies a self-contained task, use relation=new and the action it requests (verify for verification), with the exact current evidenceRefs. Otherwise clarify the intended target with the user. Preserve the original scope, constraints and native permissions.';
|
|
32
|
+
const preparationFieldsSchema = z
|
|
33
|
+
.object({
|
|
29
34
|
taskType: z.enum(ACTION_TYPES).optional(),
|
|
30
35
|
requestIntent: RequestIntentSubmissionSchema.optional(),
|
|
31
|
-
})
|
|
36
|
+
})
|
|
37
|
+
.strict();
|
|
32
38
|
function parsePreparationRequest(value) {
|
|
33
39
|
const parsed = preparationFieldsSchema.safeParse(value);
|
|
34
|
-
return parsed.success &&
|
|
40
|
+
return parsed.success &&
|
|
41
|
+
(parsed.data.taskType !== undefined ||
|
|
42
|
+
parsed.data.requestIntent !== undefined)
|
|
43
|
+
? parsed.data
|
|
44
|
+
: undefined;
|
|
35
45
|
}
|
|
36
46
|
function humanMessages(messages) {
|
|
37
47
|
return messages.filter((value) => value?.role === 'user' && (!value.source || value.source.kind === 'user'));
|
|
38
48
|
}
|
|
39
49
|
function humanTask(messages) {
|
|
40
|
-
return humanMessages(messages)
|
|
41
|
-
|
|
50
|
+
return humanMessages(messages)
|
|
51
|
+
.flatMap((message) => typeof message.content === 'string'
|
|
52
|
+
? [message.content]
|
|
53
|
+
: (message.content ?? [])
|
|
54
|
+
.filter((block) => block.type === 'text')
|
|
55
|
+
.map((block) => block.text))
|
|
56
|
+
.join('\n')
|
|
57
|
+
.trim();
|
|
58
|
+
}
|
|
59
|
+
function humanId(message) {
|
|
60
|
+
return typeof message.id === 'string'
|
|
61
|
+
? message.id
|
|
62
|
+
: canonicalContentHash(message);
|
|
42
63
|
}
|
|
43
|
-
function humanId(message) { return typeof message.id === 'string' ? message.id : canonicalContentHash(message); }
|
|
44
64
|
function eventTurn(event) {
|
|
45
65
|
const data = event?.data;
|
|
46
66
|
return Number.isSafeInteger(data?.turn) ? data.turn : undefined;
|
|
@@ -50,7 +70,9 @@ function automaticPreparationType(intent, requestIntent, name) {
|
|
|
50
70
|
if (requestIntent !== undefined && requestIntentIsUnresolved(requestIntent)) {
|
|
51
71
|
// Gathering evidence grants no change/process authority. An active Goal
|
|
52
72
|
// still requires its reference to be resolved before opening another run.
|
|
53
|
-
return ['read', 'glob', 'grep', 'web_search', 'web_fetch'].includes(name)
|
|
73
|
+
return ['read', 'glob', 'grep', 'web_search', 'web_fetch'].includes(name)
|
|
74
|
+
? 'research'
|
|
75
|
+
: undefined;
|
|
54
76
|
}
|
|
55
77
|
if (requestIntent?.action === 'answer')
|
|
56
78
|
return undefined;
|
|
@@ -59,18 +81,39 @@ function automaticPreparationType(intent, requestIntent, name) {
|
|
|
59
81
|
return interpreted;
|
|
60
82
|
if (intent.taskType && intent.taskType !== 'chat')
|
|
61
83
|
return intent.taskType;
|
|
62
|
-
return ['read', 'glob', 'grep', 'web_search', 'web_fetch'].includes(name)
|
|
84
|
+
return ['read', 'glob', 'grep', 'web_search', 'web_fetch'].includes(name)
|
|
85
|
+
? 'research'
|
|
86
|
+
: undefined;
|
|
87
|
+
}
|
|
88
|
+
const NATIVE_EVIDENCE_READS = new Set([
|
|
89
|
+
'read',
|
|
90
|
+
'glob',
|
|
91
|
+
'grep',
|
|
92
|
+
'web_search',
|
|
93
|
+
'web_fetch',
|
|
94
|
+
]);
|
|
95
|
+
const UNOWNED_NATIVE_TOOLS = new Set([
|
|
96
|
+
'bash',
|
|
97
|
+
'read',
|
|
98
|
+
'glob',
|
|
99
|
+
'grep',
|
|
100
|
+
'web_search',
|
|
101
|
+
'web_fetch',
|
|
102
|
+
'skill',
|
|
103
|
+
]);
|
|
104
|
+
function denied(reason) {
|
|
105
|
+
return { kind: 'deny', reason };
|
|
63
106
|
}
|
|
64
|
-
const NATIVE_EVIDENCE_READS = new Set(['read', 'glob', 'grep', 'web_search', 'web_fetch']);
|
|
65
|
-
const UNOWNED_NATIVE_TOOLS = new Set(['bash', 'read', 'glob', 'grep', 'web_search', 'web_fetch', 'skill']);
|
|
66
|
-
function denied(reason) { return { kind: 'deny', reason }; }
|
|
67
107
|
/** A protocol carrier, not JavaScript evaluation. Only this exact JSON-literal call is recognized. */
|
|
68
108
|
export function preparationCarrier(value) {
|
|
69
109
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
70
110
|
return undefined;
|
|
71
111
|
const args = value;
|
|
72
|
-
if (Object.keys(args).sort().join(',') !== 'code,description' ||
|
|
73
|
-
|
|
112
|
+
if (Object.keys(args).sort().join(',') !== 'code,description' ||
|
|
113
|
+
typeof args.description !== 'string' ||
|
|
114
|
+
!args.description.trim() ||
|
|
115
|
+
typeof args.code !== 'string' ||
|
|
116
|
+
args.code.length > 8192)
|
|
74
117
|
return undefined;
|
|
75
118
|
const code = args.code.trim().replace(/;$/u, '');
|
|
76
119
|
const prefix = 'return await tools.prepare_requested_work(';
|
|
@@ -93,9 +136,17 @@ export class OnDemandIntake {
|
|
|
93
136
|
options;
|
|
94
137
|
async diagnose(state, observation) {
|
|
95
138
|
try {
|
|
96
|
-
await this.host.diagnostic?.(state.input, {
|
|
139
|
+
await this.host.diagnostic?.(state.input, {
|
|
140
|
+
...observation,
|
|
141
|
+
...(state.requestIntentContext
|
|
142
|
+
? { context: state.requestIntentContext }
|
|
143
|
+
: {}),
|
|
144
|
+
...(state.requestIntent ? { decision: state.requestIntent } : {}),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
/* Diagnostic loss cannot authorize or veto work. */
|
|
97
149
|
}
|
|
98
|
-
catch { /* Diagnostic loss cannot authorize or veto work. */ }
|
|
99
150
|
}
|
|
100
151
|
#turns = new Map();
|
|
101
152
|
#clarificationContexts = new WeakMap();
|
|
@@ -111,6 +162,7 @@ export class OnDemandIntake {
|
|
|
111
162
|
#startupSkillReader;
|
|
112
163
|
#startupQuestionTool;
|
|
113
164
|
#questionTools = new WeakMap();
|
|
165
|
+
#planTools = new WeakMap();
|
|
114
166
|
#skillReaders = new WeakMap();
|
|
115
167
|
#stopped = false;
|
|
116
168
|
constructor(host, options = {}) {
|
|
@@ -120,35 +172,63 @@ export class OnDemandIntake {
|
|
|
120
172
|
/** Exact host-owned implementations, never a name-only execution bypass. */
|
|
121
173
|
registerControlTool(definition) {
|
|
122
174
|
this.#controlTools.set(definition.name, definition.execute);
|
|
123
|
-
return () => {
|
|
124
|
-
this.#controlTools.
|
|
175
|
+
return () => {
|
|
176
|
+
if (this.#controlTools.get(definition.name) === definition.execute)
|
|
177
|
+
this.#controlTools.delete(definition.name);
|
|
178
|
+
};
|
|
125
179
|
}
|
|
126
180
|
isControl(execution) {
|
|
127
181
|
const proof = this.#controlExecutions.get(execution);
|
|
128
|
-
return !this.#stopped &&
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
182
|
+
return (!this.#stopped &&
|
|
183
|
+
!execution.signal.aborted &&
|
|
184
|
+
!!proof &&
|
|
185
|
+
(!proof.turn ||
|
|
186
|
+
(this.bound(execution) === proof.turn &&
|
|
187
|
+
!proof.turn.input.signal.aborted &&
|
|
188
|
+
!['closed', 'stale'].includes(proof.turn.status))) &&
|
|
189
|
+
this.definition(execution)?.execute === proof.execute &&
|
|
190
|
+
canonicalContentHash(execution.arguments) === proof.digest &&
|
|
191
|
+
(proof.invoke
|
|
192
|
+
? [...this.#controlTools].some(([name, invoke]) => invoke === proof.invoke &&
|
|
193
|
+
this.#tools?.get(name, execution.agent)?.execute === invoke)
|
|
194
|
+
: this.#controlTools.get(execution.name) === proof.execute ||
|
|
195
|
+
this.conversationControl(execution, this.definition(execution))));
|
|
133
196
|
}
|
|
134
197
|
controlTask(execution) {
|
|
135
|
-
return this.isControl(execution)
|
|
198
|
+
return this.isControl(execution)
|
|
199
|
+
? this.#controlExecutions.get(execution)?.turn?.task
|
|
200
|
+
: undefined;
|
|
136
201
|
}
|
|
137
202
|
/** Observational/conversational calls still traverse native policy; never grant effect authority. */
|
|
138
203
|
conversationControl(execution, actual) {
|
|
139
204
|
const state = this.bound(execution);
|
|
140
|
-
return !!state &&
|
|
141
|
-
|
|
142
|
-
|
|
205
|
+
return (!!state &&
|
|
206
|
+
state.status === 'answer' &&
|
|
207
|
+
execution.parent === undefined &&
|
|
208
|
+
((execution.name === 'ask_user_question' &&
|
|
209
|
+
actual.execute === this.#questionTools.get(state.input.agent)) ||
|
|
210
|
+
(execution.name === 'exit_plan_mode' &&
|
|
211
|
+
actual.execute === this.#planTools.get(state.input.agent)) ||
|
|
212
|
+
isCodingWorkflowStatus(actual, execution.arguments)));
|
|
143
213
|
}
|
|
144
214
|
controlCall(execution, actual) {
|
|
145
|
-
if (this.#controlTools.get(execution.name) === actual.execute ||
|
|
146
|
-
|
|
147
|
-
|
|
215
|
+
if (this.#controlTools.get(execution.name) === actual.execute ||
|
|
216
|
+
this.conversationControl(execution, actual))
|
|
217
|
+
return {
|
|
218
|
+
execute: actual.execute,
|
|
219
|
+
digest: canonicalContentHash(execution.arguments),
|
|
220
|
+
};
|
|
221
|
+
if (execution.name !== 'run_code' ||
|
|
222
|
+
!execution.arguments ||
|
|
223
|
+
typeof execution.arguments !== 'object' ||
|
|
224
|
+
Array.isArray(execution.arguments))
|
|
148
225
|
return undefined;
|
|
149
226
|
const value = execution.arguments;
|
|
150
|
-
if (Object.keys(value).sort().join(',') !== 'code,description' ||
|
|
151
|
-
|
|
227
|
+
if (Object.keys(value).sort().join(',') !== 'code,description' ||
|
|
228
|
+
typeof value.code !== 'string' ||
|
|
229
|
+
value.code.length > 8192 ||
|
|
230
|
+
typeof value.description !== 'string' ||
|
|
231
|
+
!value.description.trim())
|
|
152
232
|
return undefined;
|
|
153
233
|
const code = value.code.trim().replace(/;$/u, '');
|
|
154
234
|
for (const [name, invoke] of this.#controlTools) {
|
|
@@ -160,9 +240,16 @@ export class OnDemandIntake {
|
|
|
160
240
|
try {
|
|
161
241
|
const args = JSON.parse(code.slice(prefix.length, -1));
|
|
162
242
|
if (code === prefix + JSON.stringify(args) + ')')
|
|
163
|
-
return {
|
|
243
|
+
return {
|
|
244
|
+
execute: actual.execute,
|
|
245
|
+
digest: canonicalContentHash(execution.arguments),
|
|
246
|
+
invoke,
|
|
247
|
+
args,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
/* Nonliteral programs keep ordinary execution admission. */
|
|
164
252
|
}
|
|
165
|
-
catch { /* Nonliteral programs keep ordinary execution admission. */ }
|
|
166
253
|
}
|
|
167
254
|
return undefined;
|
|
168
255
|
}
|
|
@@ -172,7 +259,10 @@ export class OnDemandIntake {
|
|
|
172
259
|
return promise;
|
|
173
260
|
}
|
|
174
261
|
existingTurn(execution) {
|
|
175
|
-
const last = execution.agent?.session
|
|
262
|
+
const last = execution.agent?.session
|
|
263
|
+
?.snapshotEvents?.()
|
|
264
|
+
.filter((event) => event.type === 'turn/start' || event.type === 'turn/end')
|
|
265
|
+
.at(-1);
|
|
176
266
|
return last?.type === 'turn/start' ? eventTurn(last) : undefined;
|
|
177
267
|
}
|
|
178
268
|
acknowledgeExisting(state) {
|
|
@@ -182,42 +272,57 @@ export class OnDemandIntake {
|
|
|
182
272
|
const taskType = requestIntentTaskType(state.requestIntent, state.intent.taskType);
|
|
183
273
|
if (taskType !== undefined)
|
|
184
274
|
state.taskType = taskType;
|
|
185
|
-
state.preparedIntentDigest = canonicalContentHash({
|
|
275
|
+
state.preparedIntentDigest = canonicalContentHash({
|
|
276
|
+
taskType: state.taskType ?? null,
|
|
277
|
+
requestIntent: state.requestIntent ?? null,
|
|
278
|
+
});
|
|
186
279
|
}
|
|
187
280
|
/** True means ordinary text may continue; false leaves an existing execution on its original path. */
|
|
188
281
|
async capture(input) {
|
|
189
282
|
input.signal.throwIfAborted();
|
|
190
283
|
if (!this.#stopped && this.host.nativeChild?.(input.agent))
|
|
191
284
|
return false;
|
|
192
|
-
if (this.#stopped ||
|
|
285
|
+
if (this.#stopped ||
|
|
286
|
+
!input.agent.session ||
|
|
287
|
+
!Number.isSafeInteger(input.turn) ||
|
|
288
|
+
input.turn < 1)
|
|
193
289
|
throw new Error('Invalid on-demand intake identity');
|
|
194
|
-
input = {
|
|
290
|
+
input = {
|
|
291
|
+
...input,
|
|
292
|
+
signal: AbortSignal.any([input.signal, this.#lifecycle.signal]),
|
|
293
|
+
};
|
|
195
294
|
const sessionId = input.agent.session.id;
|
|
196
295
|
const previous = this.#turns.get(sessionId);
|
|
197
296
|
if (previous) {
|
|
198
|
-
if (previous.input.agent !== input.agent ||
|
|
297
|
+
if (previous.input.agent !== input.agent ||
|
|
298
|
+
previous.input.agent.session !== input.agent.session ||
|
|
299
|
+
input.turn < previous.input.turn)
|
|
199
300
|
throw new Error('Stale on-demand intake identity');
|
|
200
301
|
if (previous.input.turn === input.turn) {
|
|
201
302
|
if (previous.status === 'closed')
|
|
202
303
|
throw new Error('On-demand turn is closed');
|
|
203
|
-
const fresh = humanMessages(input.messages).filter(message => previous.humanContents.get(humanId(message)) !==
|
|
304
|
+
const fresh = humanMessages(input.messages).filter((message) => previous.humanContents.get(humanId(message)) !==
|
|
305
|
+
canonicalContentHash(message));
|
|
204
306
|
if (!fresh.length) {
|
|
205
|
-
if (previous.capture && !await previous.capture)
|
|
307
|
+
if (previous.capture && !(await previous.capture))
|
|
206
308
|
return false;
|
|
207
309
|
if (previous.status === 'stale')
|
|
208
|
-
throw previous.captureError ??
|
|
310
|
+
throw (previous.captureError ??
|
|
311
|
+
new Error('Previous on-demand request is stale'));
|
|
209
312
|
return previous.status !== 'prepared';
|
|
210
313
|
}
|
|
211
314
|
previous.status = 'stale';
|
|
212
315
|
previous.controller.abort(new Error('Human instructions changed'));
|
|
213
316
|
// A new native batch can contain only the steering message. Preserve
|
|
214
317
|
// the original request while replacing changed messages by identity.
|
|
215
|
-
const merged = new Map(previous.input.messages.map(message => [humanId(message), message]));
|
|
318
|
+
const merged = new Map(previous.input.messages.map((message) => [humanId(message), message]));
|
|
216
319
|
for (const message of input.messages)
|
|
217
320
|
merged.set(humanId(message), message);
|
|
218
321
|
input = { ...input, messages: [...merged.values()] };
|
|
219
322
|
}
|
|
220
|
-
if (previous.input.turn !== input.turn &&
|
|
323
|
+
if (previous.input.turn !== input.turn &&
|
|
324
|
+
!humanMessages(input.messages).length &&
|
|
325
|
+
this.host.ready(input.agent)) {
|
|
221
326
|
// A host-driven next turn may carry no new human message. Leave its
|
|
222
327
|
// active execution bound to the host instead of creating an empty intake.
|
|
223
328
|
if (previous.status === 'closed') {
|
|
@@ -229,11 +334,13 @@ export class OnDemandIntake {
|
|
|
229
334
|
return false;
|
|
230
335
|
}
|
|
231
336
|
}
|
|
232
|
-
if (previous.input.turn !== input.turn &&
|
|
337
|
+
if (previous.input.turn !== input.turn &&
|
|
338
|
+
!['closed', 'prepared'].includes(previous.status))
|
|
233
339
|
throw new Error('Previous on-demand turn has not ended');
|
|
234
340
|
previous.controller.abort(new Error('A newer native turn superseded this request'));
|
|
235
341
|
}
|
|
236
|
-
else if (!humanMessages(input.messages).length &&
|
|
342
|
+
else if (!humanMessages(input.messages).length &&
|
|
343
|
+
this.host.ready(input.agent)) {
|
|
237
344
|
await this.host.validate(input);
|
|
238
345
|
input.signal.throwIfAborted();
|
|
239
346
|
await this.host.advance?.(input);
|
|
@@ -242,28 +349,56 @@ export class OnDemandIntake {
|
|
|
242
349
|
this.host.answerContext?.retire(input.agent);
|
|
243
350
|
// Install the pending state synchronously, before any host or classifier awaits.
|
|
244
351
|
const controller = new AbortController();
|
|
245
|
-
const state = {
|
|
246
|
-
|
|
247
|
-
|
|
352
|
+
const state = {
|
|
353
|
+
input: {
|
|
354
|
+
...input,
|
|
355
|
+
signal: AbortSignal.any([input.signal, controller.signal]),
|
|
356
|
+
messages: Object.freeze(structuredClone([...input.messages])),
|
|
357
|
+
},
|
|
358
|
+
task: humanTask(input.messages),
|
|
359
|
+
humanContents: new Map(humanMessages(input.messages).map((message) => [
|
|
360
|
+
humanId(message),
|
|
361
|
+
canonicalContentHash(message),
|
|
362
|
+
])),
|
|
363
|
+
status: 'capturing',
|
|
364
|
+
intent: { deferInference: true },
|
|
365
|
+
memoryOwner: 'none',
|
|
366
|
+
preparationCalls: new Set(),
|
|
367
|
+
controller,
|
|
368
|
+
};
|
|
248
369
|
this.#turns.set(sessionId, state);
|
|
249
|
-
rememberLispConversation(input.agent, () => !this.options.onlyWhenDeferred &&
|
|
250
|
-
|
|
370
|
+
rememberLispConversation(input.agent, () => !this.options.onlyWhenDeferred &&
|
|
371
|
+
!this.#stopped &&
|
|
372
|
+
this.#turns.get(sessionId) === state &&
|
|
373
|
+
state.status === 'answer' &&
|
|
374
|
+
!state.input.signal.aborted &&
|
|
375
|
+
this.host.nativeChild?.(input.agent) !== true);
|
|
251
376
|
const promise = (async () => {
|
|
252
377
|
await this.host.validate(input);
|
|
253
378
|
this.assertCurrent(state);
|
|
254
379
|
// Presets and later host plugins register readers after mount. Pin the
|
|
255
380
|
// validated owner's first reader once, including absence; never rebind it.
|
|
256
381
|
if (!this.#skillReaders.has(input.agent))
|
|
257
|
-
this.#skillReaders.set(input.agent, this.#startupSkillReader ??
|
|
382
|
+
this.#skillReaders.set(input.agent, this.#startupSkillReader ??
|
|
383
|
+
this.#tools?.get('skill', input.agent)?.execute);
|
|
258
384
|
if (!this.#questionTools.has(input.agent))
|
|
259
|
-
this.#questionTools.set(input.agent, this.#startupQuestionTool ??
|
|
385
|
+
this.#questionTools.set(input.agent, this.#startupQuestionTool ??
|
|
386
|
+
this.#tools?.get('ask_user_question', input.agent)?.execute);
|
|
387
|
+
if (!this.#planTools.has(input.agent))
|
|
388
|
+
this.#planTools.set(input.agent, this.#tools?.get('exit_plan_mode', input.agent)?.execute);
|
|
260
389
|
if (!humanMessages(input.messages).length)
|
|
261
390
|
throw new Error('Original human input is required before on-demand intake');
|
|
262
391
|
// Native authority is independent of optional memory, Skills and model
|
|
263
392
|
// classification. Never manufacture an owned task merely to run Bash.
|
|
264
|
-
if (!this.options.onlyWhenDeferred &&
|
|
393
|
+
if (!this.options.onlyWhenDeferred &&
|
|
394
|
+
(await this.host.nativeOwnership?.(state.input)) === 'unowned') {
|
|
395
|
+
this.assertCurrent(state);
|
|
396
|
+
const context = (await this.host.requestContext?.(state.input, state.task)) ??
|
|
397
|
+
createRequestIntentContext({
|
|
398
|
+
task: state.task,
|
|
399
|
+
messageRefs: requestIntentMessageRefs(state.input.messages, state.task),
|
|
400
|
+
});
|
|
265
401
|
this.assertCurrent(state);
|
|
266
|
-
const context = createRequestIntentContext({ task: state.task, messageRefs: requestIntentMessageRefs(state.input.messages, state.task) });
|
|
267
402
|
await this.host.prepareNativeCoding?.(state.input, state.task, context);
|
|
268
403
|
this.assertCurrent(state);
|
|
269
404
|
state.nativeUnowned = true;
|
|
@@ -298,8 +433,10 @@ export class OnDemandIntake {
|
|
|
298
433
|
const pendingClarification = this.#clarifications.get(sessionId);
|
|
299
434
|
await this.diagnose(state, { kind: 'interpretation' });
|
|
300
435
|
await this.diagnose(state, { kind: 'resume' });
|
|
301
|
-
if (pendingClarification?.agent === input.agent &&
|
|
302
|
-
|
|
436
|
+
if (pendingClarification?.agent === input.agent &&
|
|
437
|
+
state.requestIntent &&
|
|
438
|
+
!requestIntentIsUnresolved(state.requestIntent) &&
|
|
439
|
+
!executionClarification(state.task, state.requestIntent, state.requestIntentContext))
|
|
303
440
|
this.#clarifications.delete(sessionId);
|
|
304
441
|
state.memoryOwner = memoryOwnerKind(state.requestIntent, state.intent.taskType);
|
|
305
442
|
this.assertCurrent(state);
|
|
@@ -307,10 +444,16 @@ export class OnDemandIntake {
|
|
|
307
444
|
await this.host.advance?.(input, state.requestIntent);
|
|
308
445
|
this.assertCurrent(state);
|
|
309
446
|
const resumeConfirmation = previous?.input.turn !== input.turn && state.task
|
|
310
|
-
? await this.host.resumeConfirmation?.(input) ?? false
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
447
|
+
? ((await this.host.resumeConfirmation?.(input)) ?? false)
|
|
448
|
+
: false;
|
|
449
|
+
if (process.env.KIOKUKO_INTENT_DEBUG === '1' &&
|
|
450
|
+
previous?.input.turn !== input.turn) {
|
|
451
|
+
console.error('on-demand confirmation route', {
|
|
452
|
+
turn: input.turn,
|
|
453
|
+
previousTurn: previous?.input.turn,
|
|
454
|
+
hasTask: state.task.length > 0,
|
|
455
|
+
resumeConfirmation,
|
|
456
|
+
});
|
|
314
457
|
}
|
|
315
458
|
if (resumeConfirmation) {
|
|
316
459
|
this.assertCurrent(state);
|
|
@@ -321,19 +464,27 @@ export class OnDemandIntake {
|
|
|
321
464
|
this.host.answerContext?.retire(input.agent);
|
|
322
465
|
return false;
|
|
323
466
|
}
|
|
324
|
-
if (process.env.KIOKUKO_INTENT_DEBUG === '1' &&
|
|
467
|
+
if (process.env.KIOKUKO_INTENT_DEBUG === '1' &&
|
|
468
|
+
previous?.input.turn !== input.turn &&
|
|
469
|
+
state.task &&
|
|
470
|
+
this.host.resumeConfirmation) {
|
|
325
471
|
console.error('confirmation resume not selected', input.turn, state.task.length);
|
|
326
472
|
}
|
|
327
|
-
if (state.task && await this.host.replay?.(state.input)) {
|
|
473
|
+
if (state.task && (await this.host.replay?.(state.input))) {
|
|
328
474
|
this.acknowledgeExisting(state);
|
|
329
475
|
return false;
|
|
330
476
|
}
|
|
331
477
|
const assessmentConsistent = state.requestIntent && state.requestIntent.action !== 'answer'
|
|
332
|
-
? await this.assess(state, state.requestIntent)
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
478
|
+
? await this.assess(state, state.requestIntent)
|
|
479
|
+
: true;
|
|
480
|
+
const canContinue = assessmentConsistent &&
|
|
481
|
+
state.requestIntent?.relation === 'same_scope_continuation' &&
|
|
482
|
+
!requestIntentIsUnresolved(state.requestIntent) &&
|
|
483
|
+
state.requestIntent.action !== 'answer';
|
|
484
|
+
if (previous?.input.turn !== input.turn &&
|
|
485
|
+
state.task &&
|
|
486
|
+
canContinue &&
|
|
487
|
+
(await this.host.existing(input, state.requestIntent))) {
|
|
337
488
|
// Persisted ownership is only an acknowledgement when this exact native
|
|
338
489
|
// agent/session/turn is already bound. A cold resume stays conversational
|
|
339
490
|
// until explicit preparation reconstructs that binding.
|
|
@@ -344,16 +495,25 @@ export class OnDemandIntake {
|
|
|
344
495
|
}
|
|
345
496
|
// Eager mode still binds known work before generation. Classifier uncertainty
|
|
346
497
|
// instead reaches ordinary reasoning, with no execution grant or purpose UI.
|
|
347
|
-
const requestIntentUnresolved = state.requestIntent !== undefined
|
|
348
|
-
|
|
498
|
+
const requestIntentUnresolved = state.requestIntent !== undefined &&
|
|
499
|
+
requestIntentIsUnresolved(state.requestIntent);
|
|
349
500
|
const requestAsAnswer = state.requestIntent?.action === 'answer';
|
|
350
501
|
const knownIntent = this.host.interpret === undefined
|
|
351
|
-
? !state.intent.deferInference &&
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
502
|
+
? !state.intent.deferInference &&
|
|
503
|
+
state.intent.taskType !== undefined &&
|
|
504
|
+
state.intent.taskType !== 'chat'
|
|
505
|
+
: state.requestIntent !== undefined &&
|
|
506
|
+
!requestIntentUnresolved &&
|
|
507
|
+
!requestAsAnswer &&
|
|
508
|
+
state.requestIntent.relation !== 'unknown' &&
|
|
509
|
+
state.requestIntent.action !== 'unknown';
|
|
510
|
+
const missingExecutionDetail = executionClarification(state.task, state.requestIntent, state.requestIntentContext) ||
|
|
511
|
+
(state.requestIntentContext?.activeTask &&
|
|
512
|
+
!taskCanExecute(state.requestIntentContext.activeTask));
|
|
513
|
+
if (this.options.onlyWhenDeferred &&
|
|
514
|
+
knownIntent &&
|
|
515
|
+
!missingExecutionDetail &&
|
|
516
|
+
assessmentConsistent) {
|
|
357
517
|
this.#turns.delete(sessionId);
|
|
358
518
|
state.status = 'closed';
|
|
359
519
|
controller.abort(new Error('Known intent continues through eager admission'));
|
|
@@ -391,16 +551,22 @@ export class OnDemandIntake {
|
|
|
391
551
|
}
|
|
392
552
|
assertCurrent(state) {
|
|
393
553
|
state.input.signal.throwIfAborted();
|
|
394
|
-
if (this.#stopped ||
|
|
554
|
+
if (this.#stopped ||
|
|
555
|
+
this.#turns.get(state.input.agent.session.id) !== state ||
|
|
556
|
+
['closed', 'stale'].includes(state.status))
|
|
395
557
|
throw new Error('On-demand request changed or closed');
|
|
396
558
|
}
|
|
397
559
|
bound(execution) {
|
|
398
560
|
const agent = execution.agent, state = agent?.session && this.#turns.get(agent.session.id);
|
|
399
|
-
if (!state ||
|
|
561
|
+
if (!state ||
|
|
562
|
+
state.input.agent !== agent ||
|
|
563
|
+
state.input.agent.session !== agent.session)
|
|
400
564
|
return undefined;
|
|
401
565
|
if (state.existing && this.existingTurn(execution) !== state.input.turn)
|
|
402
566
|
return undefined;
|
|
403
|
-
const starts = agent.session
|
|
567
|
+
const starts = agent.session
|
|
568
|
+
.snapshotEvents?.()
|
|
569
|
+
.filter((event) => event.type === 'turn/start');
|
|
404
570
|
const latest = eventTurn(starts?.at(-1));
|
|
405
571
|
if (latest !== undefined && latest !== state.input.turn)
|
|
406
572
|
return undefined;
|
|
@@ -408,8 +574,12 @@ export class OnDemandIntake {
|
|
|
408
574
|
}
|
|
409
575
|
ownerIdentity(state) {
|
|
410
576
|
const session = state.input.agent.session;
|
|
411
|
-
return {
|
|
412
|
-
|
|
577
|
+
return {
|
|
578
|
+
agentId: state.input.agent.id,
|
|
579
|
+
sessionId: session.id,
|
|
580
|
+
turn: state.input.turn,
|
|
581
|
+
requestDigest: canonicalContentHash(state.task),
|
|
582
|
+
};
|
|
413
583
|
}
|
|
414
584
|
definition(execution) {
|
|
415
585
|
try {
|
|
@@ -417,8 +587,11 @@ export class OnDemandIntake {
|
|
|
417
587
|
if (registered)
|
|
418
588
|
return registered;
|
|
419
589
|
const transport = this.#executorTransports.get(execution);
|
|
420
|
-
return transport &&
|
|
421
|
-
|
|
590
|
+
return transport &&
|
|
591
|
+
transport.current() === transport.runtime &&
|
|
592
|
+
transport.runtime.run === transport.definition.execute
|
|
593
|
+
? transport.definition
|
|
594
|
+
: undefined;
|
|
422
595
|
}
|
|
423
596
|
catch {
|
|
424
597
|
return undefined;
|
|
@@ -428,31 +601,51 @@ export class OnDemandIntake {
|
|
|
428
601
|
* The runtime function is an identity check; dispatch still belongs to DSH.
|
|
429
602
|
* A native or missing executor cannot obtain a preparation-only carrier here. */
|
|
430
603
|
async bindExecutorTransport(execution, context) {
|
|
431
|
-
if (execution.name !== 'run_code' ||
|
|
432
|
-
|
|
604
|
+
if (execution.name !== 'run_code' ||
|
|
605
|
+
execution.parent !== undefined ||
|
|
606
|
+
typeof execution.token !== 'symbol' ||
|
|
607
|
+
typeof this.#tools?.setExecutor !== 'function' ||
|
|
608
|
+
!context.get)
|
|
433
609
|
return;
|
|
434
610
|
// Cordis returns fresh contextual proxies; compare the underlying service,
|
|
435
611
|
// never invoke it outside the selected executor's dispatch.
|
|
436
612
|
const owner = execution.agent?.ctx;
|
|
437
613
|
const services = typeof owner?.get === 'function' ? owner : context;
|
|
438
|
-
const current = () => {
|
|
614
|
+
const current = () => {
|
|
615
|
+
const value = services.get('ptcRuntime', false);
|
|
616
|
+
return value?.[symbols.original] ?? value;
|
|
617
|
+
};
|
|
439
618
|
const runtime = current(), prompt = services.get('systemPrompt', false);
|
|
440
|
-
if (typeof runtime?.run !== 'function' ||
|
|
619
|
+
if (typeof runtime?.run !== 'function' ||
|
|
620
|
+
typeof prompt?.assemble !== 'function')
|
|
441
621
|
return;
|
|
442
|
-
const projected = await prompt.assemble({
|
|
622
|
+
const projected = await prompt.assemble({
|
|
623
|
+
scope: execution.agent,
|
|
624
|
+
agent: execution.agent,
|
|
625
|
+
});
|
|
443
626
|
execution.signal.throwIfAborted();
|
|
444
|
-
if (current() !== runtime ||
|
|
445
|
-
|
|
627
|
+
if (current() !== runtime ||
|
|
628
|
+
projected.tools.length !== 1 ||
|
|
629
|
+
projected.tools[0]?.name !== 'run_code' ||
|
|
630
|
+
!projected.sections.some((section) => section.name === 'tools:sdk'))
|
|
446
631
|
return;
|
|
447
|
-
this.#executorTransports.set(execution, {
|
|
632
|
+
this.#executorTransports.set(execution, {
|
|
633
|
+
definition: { name: 'run_code', execute: runtime.run },
|
|
634
|
+
runtime,
|
|
635
|
+
current,
|
|
636
|
+
});
|
|
448
637
|
}
|
|
449
638
|
async assess(state, decision) {
|
|
450
|
-
if (!this.host.assess ||
|
|
639
|
+
if (!this.host.assess ||
|
|
640
|
+
decision?.action === 'answer' ||
|
|
641
|
+
decision?.action === 'research' ||
|
|
642
|
+
requestIntentIsUnresolved(decision))
|
|
451
643
|
return true;
|
|
452
644
|
if (!decision || !state.requestIntentContext)
|
|
453
645
|
throw new RequestClarification('The source or candidate is unresolved', true);
|
|
454
646
|
const context = state.requestIntentContext, digest = canonicalContentHash([context, decision]);
|
|
455
|
-
if (state.assessment?.digest !== digest
|
|
647
|
+
if (state.assessment?.digest !== digest ||
|
|
648
|
+
state.assessment.result.outcome === 'unavailable') {
|
|
456
649
|
const result = await this.host.assess(state.input, context, decision);
|
|
457
650
|
this.assertCurrent(state);
|
|
458
651
|
if (!result) {
|
|
@@ -473,19 +666,33 @@ export class OnDemandIntake {
|
|
|
473
666
|
throw new ExecutionSelectionPending();
|
|
474
667
|
const resolved = requestIntent ?? state.requestIntent;
|
|
475
668
|
try {
|
|
476
|
-
await this.host.diagnostic?.(state.input, {
|
|
669
|
+
await this.host.diagnostic?.(state.input, {
|
|
670
|
+
kind: 'interpretation',
|
|
671
|
+
...(state.requestIntentContext
|
|
672
|
+
? { context: state.requestIntentContext }
|
|
673
|
+
: {}),
|
|
674
|
+
...(resolved ? { decision: resolved } : {}),
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
/* Advisory only. */
|
|
477
679
|
}
|
|
478
|
-
catch { /* Advisory only. */ }
|
|
479
680
|
if (resolved?.action === 'change' && resolved.changeScope === 'unknown') {
|
|
480
681
|
throw new RequestClarification('The requested change scope is unresolved. Specify implementation or document using the current request evidence before preparing work.');
|
|
481
682
|
}
|
|
482
|
-
if (resolved &&
|
|
683
|
+
if (resolved &&
|
|
684
|
+
requestIntentIsUnresolved(resolved) &&
|
|
685
|
+
(state.requestIntentContext?.activeGoal ||
|
|
686
|
+
state.requestIntentContext?.activeTask)) {
|
|
483
687
|
throw new RequestClarification('Clarify the request before preparing work');
|
|
484
688
|
}
|
|
485
|
-
if (!await this.assess(state, resolved))
|
|
486
|
-
throw new RequestClarification('Independent source comparison or finite-domain verification requires clarification', true);
|
|
689
|
+
if (!(await this.assess(state, resolved)))
|
|
690
|
+
throw new RequestClarification('Independent source comparison or finite-domain verification requires clarification', true, false, resolved);
|
|
487
691
|
const activeTask = state.requestIntentContext?.activeTask;
|
|
488
|
-
if (activeTask &&
|
|
692
|
+
if (activeTask &&
|
|
693
|
+
resolved?.relation === 'same_scope_continuation' &&
|
|
694
|
+
!taskCanExecute(activeTask) &&
|
|
695
|
+
!resolved.taskOperation) {
|
|
489
696
|
throw new RequestClarification('The current task requires an explicit user-selected state operation before execution.');
|
|
490
697
|
}
|
|
491
698
|
const clarification = executionClarification(state.task, resolved, state.requestIntentContext);
|
|
@@ -493,13 +700,21 @@ export class OnDemandIntake {
|
|
|
493
700
|
throw new RequestClarification(clarification);
|
|
494
701
|
if (!ACTION_TYPES.includes(taskType))
|
|
495
702
|
throw new Error('Answer conversational questions directly; prepare_requested_work is for requested work');
|
|
496
|
-
if (state.status === 'prepared' &&
|
|
497
|
-
|
|
703
|
+
if (state.status === 'prepared' &&
|
|
704
|
+
state.confirmationResume &&
|
|
705
|
+
explicitIntent &&
|
|
706
|
+
(state.requestIntent?.relation === 'unknown' ||
|
|
707
|
+
state.requestIntent?.action === 'unknown'))
|
|
498
708
|
state.status = 'answer';
|
|
499
709
|
if (state.status === 'prepared') {
|
|
500
710
|
if (state.taskType !== taskType)
|
|
501
711
|
throw requestInterpretationError('Prepared intent cannot be replaced within a turn', state.requestIntent, state.requestIntentContext, true);
|
|
502
|
-
if (explicitIntent &&
|
|
712
|
+
if (explicitIntent &&
|
|
713
|
+
state.preparedIntentDigest !==
|
|
714
|
+
canonicalContentHash({
|
|
715
|
+
taskType,
|
|
716
|
+
requestIntent: requestIntent ?? null,
|
|
717
|
+
})) {
|
|
503
718
|
throw requestInterpretationError('Prepared interpretation cannot be replaced within a turn', state.requestIntent, state.requestIntentContext, true);
|
|
504
719
|
}
|
|
505
720
|
return;
|
|
@@ -514,7 +729,10 @@ export class OnDemandIntake {
|
|
|
514
729
|
state.taskType = taskType;
|
|
515
730
|
const operation = (async () => {
|
|
516
731
|
// Retain exact original messages. The only caller-supplied input is an advisory task type.
|
|
517
|
-
const admitted = await this.host.prepare({
|
|
732
|
+
const admitted = await this.host.prepare({
|
|
733
|
+
...state.input,
|
|
734
|
+
signal: AbortSignal.any([state.input.signal, signal]),
|
|
735
|
+
}, taskType, requestIntent ?? state.requestIntent, state.requestIntentContext);
|
|
518
736
|
this.assertCurrent(state);
|
|
519
737
|
signal.throwIfAborted();
|
|
520
738
|
if (!admitted || !this.host.ready(state.input.agent, state.input.turn))
|
|
@@ -522,8 +740,12 @@ export class OnDemandIntake {
|
|
|
522
740
|
const preparedIntent = requestIntent ?? state.requestIntent;
|
|
523
741
|
if (preparedIntent !== undefined)
|
|
524
742
|
state.requestIntent = preparedIntent;
|
|
525
|
-
state.preparedIntentDigest = canonicalContentHash({
|
|
743
|
+
state.preparedIntentDigest = canonicalContentHash({
|
|
744
|
+
taskType,
|
|
745
|
+
requestIntent: state.requestIntent ?? null,
|
|
746
|
+
});
|
|
526
747
|
state.status = 'prepared';
|
|
748
|
+
this.#clarifications.delete(state.input.agent.session.id);
|
|
527
749
|
delete state.confirmationResume;
|
|
528
750
|
})();
|
|
529
751
|
this.watch(operation);
|
|
@@ -535,11 +757,17 @@ export class OnDemandIntake {
|
|
|
535
757
|
if (!['closed', 'stale'].includes(state.status)) {
|
|
536
758
|
// A declined selection preserves this request for a conversational acknowledgement.
|
|
537
759
|
// It is not admission: preparation remains consumed and tools still need a ready host.
|
|
538
|
-
state.selectionPending =
|
|
760
|
+
state.selectionPending =
|
|
761
|
+
error instanceof ExecutionSelectionPending &&
|
|
762
|
+
!state.input.signal.aborted &&
|
|
763
|
+
!signal.aborted;
|
|
539
764
|
// Preparation failure is not a change of human authority. Keep the
|
|
540
765
|
// captured request retryable, but never revive Stop/cancellation. A
|
|
541
766
|
// new call must pass host admission and native permission checks again.
|
|
542
|
-
state.status =
|
|
767
|
+
state.status =
|
|
768
|
+
state.input.signal.aborted || signal.aborted || this.#stopped
|
|
769
|
+
? 'stale'
|
|
770
|
+
: 'answer';
|
|
543
771
|
delete state.preparedIntentDigest;
|
|
544
772
|
delete state.taskType;
|
|
545
773
|
state.memoryOwner = 'none';
|
|
@@ -555,9 +783,14 @@ export class OnDemandIntake {
|
|
|
555
783
|
const pending = agent.session && this.#clarifications.get(agent.session.id);
|
|
556
784
|
if (this.#stopped || pending?.agent !== agent)
|
|
557
785
|
return false;
|
|
558
|
-
const boundary = agent.session
|
|
786
|
+
const boundary = agent.session
|
|
787
|
+
?.snapshotEvents?.()
|
|
788
|
+
.filter((event) => event.type === 'turn/end')
|
|
789
|
+
.at(-1);
|
|
559
790
|
const data = boundary?.data;
|
|
560
|
-
if (data?.turn !== undefined &&
|
|
791
|
+
if (data?.turn !== undefined &&
|
|
792
|
+
data.turn >= pending.turn &&
|
|
793
|
+
['aborted', 'error', 'max-tokens', 'blocked'].includes(data.reason?.kind ?? '')) {
|
|
561
794
|
this.#clarifications.delete(agent.session.id);
|
|
562
795
|
return false;
|
|
563
796
|
}
|
|
@@ -565,67 +798,231 @@ export class OnDemandIntake {
|
|
|
565
798
|
}
|
|
566
799
|
async clarify(state, executionSignal, reason) {
|
|
567
800
|
this.assertCurrent(state);
|
|
568
|
-
const signal = AbortSignal.any([
|
|
801
|
+
const signal = AbortSignal.any([
|
|
802
|
+
executionSignal,
|
|
803
|
+
state.input.signal,
|
|
804
|
+
state.controller.signal,
|
|
805
|
+
]);
|
|
569
806
|
const context = state.requestIntentContext;
|
|
570
807
|
const sessionId = state.input.agent.session.id;
|
|
571
|
-
if (reason.
|
|
808
|
+
if (reason.interpretation)
|
|
809
|
+
return {
|
|
810
|
+
prepared: false,
|
|
811
|
+
status: 'request_interpretation_required',
|
|
812
|
+
originalTask: state.task,
|
|
813
|
+
requestInterpretation: requestInterpretationRecovery(reason.candidate ?? state.requestIntent, context),
|
|
814
|
+
currentReferences: context
|
|
815
|
+
? requestIntentRequiredReferenceIds(context)
|
|
816
|
+
: [],
|
|
817
|
+
permissions: 'unchanged_native_policy',
|
|
818
|
+
nextAction: 'submit_current_request_intent',
|
|
819
|
+
detail: 'Interpret the captured user request and current host references in the main model. A taskType label alone is not an action or approval. Ask the human only if the action or referenced target is actually unclear.',
|
|
820
|
+
};
|
|
821
|
+
if (reason.assessment &&
|
|
822
|
+
state.assessment?.result.outcome === 'unavailable') {
|
|
572
823
|
const a = state.assessment.result;
|
|
573
|
-
return {
|
|
574
|
-
|
|
575
|
-
|
|
824
|
+
return {
|
|
825
|
+
prepared: false,
|
|
826
|
+
status: 'assessment_unavailable',
|
|
827
|
+
semantic: a.semantic,
|
|
828
|
+
detector: a.detector.state === 'unavailable'
|
|
829
|
+
? a.detector.reason
|
|
830
|
+
: a.detector.result.status,
|
|
831
|
+
originalTask: state.task,
|
|
832
|
+
permissions: 'unchanged_native_policy',
|
|
833
|
+
preparedExecutionAllowed: false,
|
|
834
|
+
nextAction: 'report_assessment_unavailable',
|
|
835
|
+
recovery: 'Preserve the request and report the unavailable assessment. Do not repeat clarification, claim preparation succeeded, or switch tools to bypass this preparation boundary.',
|
|
836
|
+
detail: '原文照合または有限領域検査を利用できません。実行は準備されていません。',
|
|
837
|
+
};
|
|
576
838
|
}
|
|
577
|
-
|
|
578
|
-
|
|
839
|
+
// An inconclusive detector is not evidence that the human omitted a target.
|
|
840
|
+
// Return the submitted interpretation for repair; keep preparation denied.
|
|
841
|
+
if (reason.assessment &&
|
|
842
|
+
state.assessment?.result.semantic === 'insufficient' &&
|
|
843
|
+
reason.candidate) {
|
|
844
|
+
return {
|
|
845
|
+
prepared: false,
|
|
846
|
+
status: 'assessment_rejected',
|
|
847
|
+
semantic: state.assessment.result.semantic,
|
|
848
|
+
semanticIssues: state.assessment.result.semanticIssues ?? [],
|
|
849
|
+
originalTask: state.task,
|
|
850
|
+
requestIntent: reason.candidate,
|
|
851
|
+
currentReferences: context
|
|
852
|
+
? requestIntentRequiredReferenceIds(context)
|
|
853
|
+
: [],
|
|
854
|
+
availableReferences: context ? requestIntentReferenceIds(context) : [],
|
|
855
|
+
permissions: 'unchanged_native_policy',
|
|
856
|
+
nextAction: 'revise_request_intent_or_report_conflict',
|
|
857
|
+
detail: 'The independent comparison did not establish the submitted candidate. Re-read the captured request, answers, active task and same-session plan. Cite an existing proposal only when the request adopts it. Correct the candidate or report the specific unresolved comparison; do not repeat an identical submission or ask the human to supply internal classification fields. Ask a concrete question only if the source itself leaves an action or target ambiguous. No work is prepared.',
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
const scope = (state.requestIntent?.action === 'change' &&
|
|
861
|
+
state.requestIntent.changeScope === 'unknown') ||
|
|
862
|
+
reason.message.startsWith('The requested change scope');
|
|
579
863
|
const target = !!executionClarification(state.task, state.requestIntent, context);
|
|
580
864
|
const transition = context?.activeTask && !taskCanExecute(context.activeTask);
|
|
581
|
-
const question = reason.assessment
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
865
|
+
const question = reason.assessment
|
|
866
|
+
? '現在の依頼と実行候補の対象・操作・制約を照合できませんでした。今回の対象と希望する操作、以前の指示を変更する範囲を教えてください。'
|
|
867
|
+
: transition
|
|
868
|
+
? `現在のタスクは ${context.activeTask.state} です。続けるために、どの操作を希望しますか?(${taskAllowedOperations(context.activeTask).join(' / ')})`
|
|
869
|
+
: target
|
|
870
|
+
? 'どの具体的なファイル・項目・相手・操作を対象にしますか?'
|
|
871
|
+
: scope
|
|
872
|
+
? '変更するのは文章だけですか、それともコード・テスト・設定も含みますか?'
|
|
873
|
+
: state.requestIntent?.action === 'unknown'
|
|
874
|
+
? 'その対象に、具体的に何をしてほしいですか?'
|
|
586
875
|
: '今回の依頼は現在の作業の続きですか、それとも別の作業ですか?';
|
|
587
|
-
const replies = this.#clarificationContexts.get(state.input.agent) ??
|
|
588
|
-
|
|
589
|
-
const
|
|
876
|
+
const replies = this.#clarificationContexts.get(state.input.agent) ??
|
|
877
|
+
new Map();
|
|
878
|
+
const clarification = collectClarificationContext(humanMessages(state.input.messages).map((message) => ({
|
|
879
|
+
id: humanId(message),
|
|
880
|
+
text: humanTask([message]),
|
|
881
|
+
})), replies);
|
|
882
|
+
const answerScope = canonicalContentHash({
|
|
883
|
+
activeTask: context?.activeTask ?? null,
|
|
884
|
+
activeGoal: context?.activeGoal ?? null,
|
|
885
|
+
proposals: context?.proposals ?? [],
|
|
886
|
+
});
|
|
590
887
|
const answered = reusableClarificationAnswer(clarification, question, answerScope);
|
|
591
888
|
if (answered)
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
889
|
+
this.#clarifications.delete(sessionId);
|
|
890
|
+
if (answered && reason.assessment)
|
|
891
|
+
return {
|
|
892
|
+
prepared: false,
|
|
893
|
+
status: 'assessment_rejected',
|
|
894
|
+
semantic: state.assessment?.result.semantic,
|
|
895
|
+
semanticIssues: state.assessment?.result.semanticIssues ?? [],
|
|
896
|
+
detector: state.assessment?.result.detector.state === 'available'
|
|
897
|
+
? state.assessment.result.detector.result.status
|
|
898
|
+
: state.assessment?.result.detector.reason,
|
|
899
|
+
originalTask: state.task,
|
|
900
|
+
currentReferences: context
|
|
901
|
+
? requestIntentRequiredReferenceIds(context)
|
|
902
|
+
: [],
|
|
903
|
+
permissions: 'unchanged_native_policy',
|
|
904
|
+
nextAction: 'revise_request_intent_or_report_conflict',
|
|
905
|
+
detail: 'The current candidate still conflicts with, or is not established by, the source including the latest answer. Revise the candidate using that answer or explain the specific unresolved conflict. Do not repeat an identical submission or the answered question.',
|
|
906
|
+
};
|
|
907
|
+
if (answered)
|
|
908
|
+
return {
|
|
909
|
+
prepared: false,
|
|
910
|
+
status: 'clarification_answer_available',
|
|
911
|
+
question,
|
|
912
|
+
questionId: answered.questionId,
|
|
913
|
+
answer: answered.value,
|
|
914
|
+
originalTask: clarification.sources
|
|
915
|
+
.map((source) => source.text)
|
|
916
|
+
.join('\n'),
|
|
917
|
+
permissions: 'unchanged_native_policy',
|
|
918
|
+
currentReferences: context
|
|
919
|
+
? requestIntentRequiredReferenceIds(context)
|
|
920
|
+
: [],
|
|
921
|
+
nextAction: 'submit_current_request_intent',
|
|
922
|
+
};
|
|
923
|
+
const key = canonicalContentHash({
|
|
924
|
+
context: context ?? null,
|
|
925
|
+
task: state.task,
|
|
926
|
+
humanEvidence: [...state.humanContents],
|
|
927
|
+
question,
|
|
928
|
+
});
|
|
596
929
|
const previous = this.#clarifications.get(sessionId);
|
|
597
930
|
if (previous?.agent === state.input.agent && previous.key === key)
|
|
598
931
|
return previous.operation ?? previous.result;
|
|
599
932
|
const id = `request-clarification-${key.slice(0, 20)}`;
|
|
600
|
-
const result = {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
933
|
+
const result = {
|
|
934
|
+
prepared: false,
|
|
935
|
+
status: 'awaiting_clarification',
|
|
936
|
+
question,
|
|
937
|
+
questionId: id,
|
|
938
|
+
originalTask: state.task,
|
|
939
|
+
...(requestIntentIsUnresolved(state.requestIntent)
|
|
940
|
+
? {
|
|
941
|
+
requestInterpretation: requestInterpretationRecovery(state.requestIntent, context),
|
|
942
|
+
}
|
|
943
|
+
: {}),
|
|
944
|
+
...(reason.assessment
|
|
945
|
+
? {
|
|
946
|
+
semantic: state.assessment?.result.semantic,
|
|
947
|
+
semanticIssues: state.assessment?.result.semanticIssues ?? [],
|
|
948
|
+
}
|
|
949
|
+
: {}),
|
|
950
|
+
permissions: 'unchanged_native_policy',
|
|
951
|
+
nextAction: this.host.questions ? 'wait_for_user' : 'ask_user_once',
|
|
952
|
+
};
|
|
953
|
+
const pending = {
|
|
954
|
+
agent: state.input.agent,
|
|
955
|
+
turn: state.input.turn,
|
|
956
|
+
key,
|
|
957
|
+
status: 'waiting',
|
|
958
|
+
result: result,
|
|
959
|
+
operation: undefined,
|
|
960
|
+
};
|
|
604
961
|
this.#clarifications.set(sessionId, pending);
|
|
605
|
-
await this.diagnose(state, {
|
|
962
|
+
await this.diagnose(state, {
|
|
963
|
+
kind: 'question',
|
|
964
|
+
questionRef: id,
|
|
965
|
+
reason: transition
|
|
966
|
+
? 'transition_required'
|
|
967
|
+
: target
|
|
968
|
+
? 'target_unknown'
|
|
969
|
+
: scope
|
|
970
|
+
? 'scope_unknown'
|
|
971
|
+
: 'clarification_required',
|
|
972
|
+
});
|
|
606
973
|
if (!this.host.questions || !state.input.agent.followup)
|
|
607
974
|
return result;
|
|
608
975
|
const operation = (async () => {
|
|
609
976
|
try {
|
|
610
|
-
const response = await abortable(this.host.questions.ask({
|
|
611
|
-
|
|
977
|
+
const response = await abortable(this.host.questions.ask({
|
|
978
|
+
agent: state.input.agent,
|
|
979
|
+
signal,
|
|
980
|
+
questions: [
|
|
981
|
+
{
|
|
982
|
+
id,
|
|
983
|
+
question,
|
|
984
|
+
...(scope && !target && !transition
|
|
985
|
+
? {
|
|
986
|
+
options: [
|
|
987
|
+
{ label: '文章のみ' },
|
|
988
|
+
{ label: 'コード・テスト・設定を含む' },
|
|
989
|
+
],
|
|
990
|
+
}
|
|
991
|
+
: {}),
|
|
992
|
+
},
|
|
993
|
+
],
|
|
994
|
+
}), signal);
|
|
612
995
|
signal.throwIfAborted();
|
|
613
996
|
this.assertCurrent(state);
|
|
614
|
-
if (this.#clarifications.get(sessionId) !== pending ||
|
|
997
|
+
if (this.#clarifications.get(sessionId) !== pending ||
|
|
998
|
+
pending.status !== 'waiting')
|
|
615
999
|
return result;
|
|
616
1000
|
await this.host.validate(state.input);
|
|
617
1001
|
signal.throwIfAborted();
|
|
618
1002
|
this.assertCurrent(state);
|
|
619
|
-
const current =
|
|
1003
|
+
const current = this.host.requestContext
|
|
1004
|
+
? await this.host.requestContext(state.input, state.task)
|
|
1005
|
+
: (await this.host.interpret?.(state.input, state.task))?.context;
|
|
620
1006
|
signal.throwIfAborted();
|
|
621
1007
|
this.assertCurrent(state);
|
|
622
|
-
if (context &&
|
|
1008
|
+
if (context &&
|
|
1009
|
+
(!current ||
|
|
1010
|
+
canonicalContentHash(current) !== canonicalContentHash(context))) {
|
|
623
1011
|
if (current) {
|
|
624
|
-
state.requestIntentContext = current
|
|
625
|
-
state.requestIntent = current
|
|
626
|
-
state.requestIntentContextDigest = canonicalContentHash(current
|
|
1012
|
+
state.requestIntentContext = current;
|
|
1013
|
+
state.requestIntent = unresolvedRequestIntent(current, 'clarification_context_changed');
|
|
1014
|
+
state.requestIntentContextDigest = canonicalContentHash(current);
|
|
627
1015
|
}
|
|
628
|
-
pending.result = {
|
|
1016
|
+
pending.result = {
|
|
1017
|
+
...result,
|
|
1018
|
+
...(current
|
|
1019
|
+
? {
|
|
1020
|
+
currentReferences: requestIntentRequiredReferenceIds(current),
|
|
1021
|
+
}
|
|
1022
|
+
: {}),
|
|
1023
|
+
status: 'clarification_context_changed',
|
|
1024
|
+
detail: 'The task or current evidence changed while awaiting the answer. Refresh the current request before asking again.',
|
|
1025
|
+
};
|
|
629
1026
|
return pending.result;
|
|
630
1027
|
}
|
|
631
1028
|
const answer = response.answers[0];
|
|
@@ -636,17 +1033,38 @@ export class OnDemandIntake {
|
|
|
636
1033
|
// label the actual UI answer separately, and re-enter ordinary native
|
|
637
1034
|
// capture. The reply itself is never mapped to execution authority.
|
|
638
1035
|
const answerMessageId = randomUUID();
|
|
639
|
-
const reply = clarificationReply(clarification, {
|
|
1036
|
+
const reply = clarificationReply(clarification, {
|
|
1037
|
+
id: answerMessageId,
|
|
1038
|
+
questionId: id,
|
|
1039
|
+
question,
|
|
1040
|
+
value,
|
|
1041
|
+
scope: answerScope,
|
|
1042
|
+
});
|
|
640
1043
|
const content = reply.content;
|
|
641
1044
|
signal.throwIfAborted();
|
|
642
1045
|
this.assertCurrent(state);
|
|
643
1046
|
replies.set(answerMessageId, reply.context);
|
|
644
1047
|
this.#clarificationContexts.set(state.input.agent, replies);
|
|
645
|
-
state.input.agent.followup({
|
|
646
|
-
|
|
1048
|
+
state.input.agent.followup({
|
|
1049
|
+
id: answerMessageId,
|
|
1050
|
+
role: 'user',
|
|
1051
|
+
source: { kind: 'user' },
|
|
1052
|
+
content: [{ type: 'text', text: content }],
|
|
1053
|
+
});
|
|
1054
|
+
await this.diagnose(state, {
|
|
1055
|
+
kind: 'answer',
|
|
1056
|
+
questionRef: id,
|
|
1057
|
+
answerRef: answerMessageId,
|
|
1058
|
+
});
|
|
647
1059
|
pending.status = 'queued';
|
|
648
|
-
pending.result = {
|
|
649
|
-
|
|
1060
|
+
pending.result = {
|
|
1061
|
+
...result,
|
|
1062
|
+
status: 'clarification_answer_queued',
|
|
1063
|
+
answer: value,
|
|
1064
|
+
answerRef: answerMessageId,
|
|
1065
|
+
nextAction: 'await_queued_answer_capture',
|
|
1066
|
+
detail: 'The answer is already queued as native user input. Do not ask it again or reuse old references. Continue only after the host captures the queued answer and supplies its current references.',
|
|
1067
|
+
};
|
|
650
1068
|
return pending.result;
|
|
651
1069
|
}
|
|
652
1070
|
catch (error) {
|
|
@@ -655,8 +1073,18 @@ export class OnDemandIntake {
|
|
|
655
1073
|
this.#clarifications.delete(sessionId);
|
|
656
1074
|
throw error;
|
|
657
1075
|
}
|
|
658
|
-
// A
|
|
659
|
-
|
|
1076
|
+
// Dismissal remains unapproved. A failed surface or host read can be
|
|
1077
|
+
// retried explicitly instead of retaining a dead pending question.
|
|
1078
|
+
if (error?.code === 'ASK_CANCELLED')
|
|
1079
|
+
return result;
|
|
1080
|
+
if (this.#clarifications.get(sessionId) === pending)
|
|
1081
|
+
this.#clarifications.delete(sessionId);
|
|
1082
|
+
return {
|
|
1083
|
+
...result,
|
|
1084
|
+
status: 'clarification_unavailable',
|
|
1085
|
+
nextAction: 'retry_preparation',
|
|
1086
|
+
detail: 'The question or its current context could not be read. No work was prepared and no answer was queued. Retry explicitly after restoring the question surface or host storage.',
|
|
1087
|
+
};
|
|
660
1088
|
}
|
|
661
1089
|
finally {
|
|
662
1090
|
pending.operation = undefined;
|
|
@@ -665,24 +1093,50 @@ export class OnDemandIntake {
|
|
|
665
1093
|
pending.operation = operation;
|
|
666
1094
|
return this.watch(operation);
|
|
667
1095
|
}
|
|
668
|
-
bindIntent(state, submission) {
|
|
1096
|
+
async bindIntent(state, submission) {
|
|
669
1097
|
if (!submission) {
|
|
670
|
-
if (state.requestIntent
|
|
1098
|
+
if (requestIntentIsUnresolved(state.requestIntent) &&
|
|
1099
|
+
this.host.interpret) {
|
|
1100
|
+
const current = await this.host.interpret(state.input, state.task);
|
|
1101
|
+
this.assertCurrent(state);
|
|
1102
|
+
if (current) {
|
|
1103
|
+
state.requestIntentContext = current.context;
|
|
1104
|
+
state.requestIntentContextDigest = canonicalContentHash(current.context);
|
|
1105
|
+
state.requestIntent = current.decision;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
if (requestIntentIsUnresolved(state.requestIntent) &&
|
|
1109
|
+
state.requestIntentContext &&
|
|
1110
|
+
state.requestIntent?.source === 'unresolved' &&
|
|
1111
|
+
state.requestIntent.unresolvedReason !==
|
|
1112
|
+
'provider_abstained_or_uncertain' &&
|
|
1113
|
+
!this.#clarifications.has(state.input.agent.session.id)) {
|
|
1114
|
+
throw new RequestClarification('The main model must interpret the current request before preparing work', false, true);
|
|
1115
|
+
}
|
|
1116
|
+
if (state.requestIntent?.action === 'change' &&
|
|
1117
|
+
state.requestIntent.changeScope === 'unknown') {
|
|
671
1118
|
throw new RequestClarification('The requested change scope is unresolved. Specify implementation or document using the current request evidence before preparing work.');
|
|
672
1119
|
}
|
|
673
1120
|
return state.requestIntent;
|
|
674
1121
|
}
|
|
675
|
-
if (!state.requestIntentContext ||
|
|
1122
|
+
if (!state.requestIntentContext ||
|
|
1123
|
+
state.requestIntentContextDigest !==
|
|
1124
|
+
canonicalContentHash(state.requestIntentContext)) {
|
|
676
1125
|
throw new Error('Request interpretation context is stale');
|
|
677
1126
|
}
|
|
678
1127
|
const result = validateMainModelRequestIntent(submission, state.requestIntentContext);
|
|
679
1128
|
if (!result.ok) {
|
|
680
1129
|
switch (result.reason) {
|
|
681
|
-
case 'no_active_goal':
|
|
682
|
-
|
|
683
|
-
case '
|
|
684
|
-
|
|
685
|
-
case '
|
|
1130
|
+
case 'no_active_goal':
|
|
1131
|
+
throw new Error(`Request relation ${submission.relation} cannot be used without an active Goal. ${NO_ACTIVE_GOAL_GUIDANCE}`);
|
|
1132
|
+
case 'missing_reference':
|
|
1133
|
+
throw new Error('Request interpretation is missing required current message or active Goal references. Use the exact current evidenceRefs from the supplied context.');
|
|
1134
|
+
case 'unknown_reference':
|
|
1135
|
+
throw new Error('Request interpretation references another request, stale revision, or unknown proposal. Use only the exact current evidenceRefs from the supplied context.');
|
|
1136
|
+
case 'unresolved_intent':
|
|
1137
|
+
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.');
|
|
1138
|
+
case 'invalid_submission':
|
|
1139
|
+
throw new Error('Invalid request interpretation. Supply a relation, action and unique exact current evidenceRefs.');
|
|
686
1140
|
case 'invalid_task_operation': {
|
|
687
1141
|
const task = state.requestIntentContext.activeTask;
|
|
688
1142
|
if (!task)
|
|
@@ -694,10 +1148,12 @@ export class OnDemandIntake {
|
|
|
694
1148
|
}
|
|
695
1149
|
const bound = result.decision;
|
|
696
1150
|
if (bound.action === 'change' && bound.changeScope === 'unknown') {
|
|
697
|
-
throw new RequestClarification('
|
|
1151
|
+
throw new RequestClarification('Determine requestIntent.changeScope from the captured request and plan in the main model. Ask a concrete human question only if the source itself leaves that choice unclear.', false, true, bound);
|
|
698
1152
|
}
|
|
699
1153
|
const prior = state.requestIntent;
|
|
700
|
-
if (prior?.source === 'decision-provider' &&
|
|
1154
|
+
if (prior?.source === 'decision-provider' &&
|
|
1155
|
+
prior.relation !== 'unknown' &&
|
|
1156
|
+
prior.action !== 'unknown') {
|
|
701
1157
|
if (prior.relation !== bound.relation || prior.action !== bound.action) {
|
|
702
1158
|
throw new Error('Main-model interpretation conflicts with the configured decision provider');
|
|
703
1159
|
}
|
|
@@ -711,55 +1167,118 @@ export class OnDemandIntake {
|
|
|
711
1167
|
// Project the immutable human batch captured before that waterfall.
|
|
712
1168
|
if (state?.input.turn === input.turn && state.input.agent !== input.agent)
|
|
713
1169
|
throw new Error('Answer projection native agent changed');
|
|
714
|
-
const projected = state?.input.turn === input.turn &&
|
|
715
|
-
|
|
1170
|
+
const projected = state?.input.turn === input.turn &&
|
|
1171
|
+
!state.nativeUnowned &&
|
|
1172
|
+
state.answerContextReady &&
|
|
1173
|
+
!state.confirmationResume &&
|
|
1174
|
+
this.host.answerContext
|
|
1175
|
+
? await this.host.answerContext.project(state.input, messages, this.ownerIdentity(state))
|
|
1176
|
+
: messages;
|
|
716
1177
|
return this.guidance(projected, state?.input.turn === input.turn ? state : undefined);
|
|
717
1178
|
}
|
|
718
1179
|
async beforeRequest(agent) {
|
|
719
1180
|
const state = agent.session && this.#turns.get(agent.session.id);
|
|
720
|
-
if (state &&
|
|
1181
|
+
if (state &&
|
|
1182
|
+
!state.nativeUnowned &&
|
|
1183
|
+
state.answerContextReady &&
|
|
1184
|
+
state.input.agent === agent &&
|
|
1185
|
+
state.status === 'answer' &&
|
|
1186
|
+
this.pending(agent)) {
|
|
721
1187
|
await this.host.answerContext?.beforeRequest(state.input, this.ownerIdentity(state));
|
|
722
1188
|
}
|
|
723
1189
|
}
|
|
724
1190
|
async fence(agent, request) {
|
|
725
1191
|
const state = agent.session && this.#turns.get(agent.session.id);
|
|
726
|
-
if (state &&
|
|
1192
|
+
if (state &&
|
|
1193
|
+
!state.nativeUnowned &&
|
|
1194
|
+
state.answerContextReady &&
|
|
1195
|
+
state.input.agent === agent &&
|
|
1196
|
+
state.status === 'answer' &&
|
|
1197
|
+
this.pending(agent)) {
|
|
727
1198
|
await this.host.answerContext?.fence(state.input, request, this.ownerIdentity(state));
|
|
728
1199
|
}
|
|
729
1200
|
}
|
|
730
1201
|
guidance(messages, state) {
|
|
731
1202
|
const context = state?.requestIntentContext;
|
|
732
1203
|
const interpretation = state?.requestIntent;
|
|
733
|
-
const intentText = context
|
|
734
|
-
references
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1204
|
+
const intentText = context
|
|
1205
|
+
? `\n\nCurrent request references and same-session work (host-bound, advisory only):\n${JSON.stringify({
|
|
1206
|
+
references: requestIntentReferenceIds(context),
|
|
1207
|
+
requiredReferences: requestIntentRequiredReferenceIds(context),
|
|
1208
|
+
activeGoal: context.activeGoal ?? null,
|
|
1209
|
+
activeTask: context.activeTask ?? null,
|
|
1210
|
+
proposals: context.proposals.map(({ id, revision, digest, text }) => ({
|
|
1211
|
+
id,
|
|
1212
|
+
revision,
|
|
1213
|
+
digest,
|
|
1214
|
+
text,
|
|
1215
|
+
evidenceRef: `proposal:${canonicalContentHash([id, revision, digest])}`,
|
|
1216
|
+
})),
|
|
1217
|
+
decision: interpretation
|
|
1218
|
+
? {
|
|
1219
|
+
relation: interpretation.relation,
|
|
1220
|
+
action: interpretation.action,
|
|
1221
|
+
source: interpretation.source,
|
|
1222
|
+
policyVersion: interpretation.policyVersion,
|
|
1223
|
+
evidence: interpretation.evidence,
|
|
1224
|
+
unresolvedReason: interpretation.unresolvedReason ?? null,
|
|
1225
|
+
}
|
|
1226
|
+
: null,
|
|
1227
|
+
})}\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.`
|
|
1228
|
+
: '';
|
|
1229
|
+
const text = ON_DEMAND_GUIDANCE +
|
|
1230
|
+
intentText +
|
|
1231
|
+
(context && !context.activeGoal && !context.activeTask
|
|
1232
|
+
? `\n${NO_ACTIVE_GOAL_GUIDANCE}`
|
|
1233
|
+
: '');
|
|
1234
|
+
return [
|
|
1235
|
+
...messages,
|
|
1236
|
+
{
|
|
1237
|
+
id: randomUUID(),
|
|
1238
|
+
role: 'user',
|
|
1239
|
+
content: [{ type: 'text', text }],
|
|
1240
|
+
source: {
|
|
1241
|
+
kind: KIOKUKO_DSH_SOURCE_KIND,
|
|
1242
|
+
form: 'snapshot',
|
|
1243
|
+
sections: [{ name: 'on-demand-intake', text }],
|
|
1244
|
+
},
|
|
1245
|
+
},
|
|
1246
|
+
];
|
|
746
1247
|
}
|
|
747
1248
|
snapshot(sessionId) {
|
|
748
1249
|
const state = this.#turns.get(sessionId);
|
|
749
|
-
return state && {
|
|
750
|
-
|
|
1250
|
+
return (state && {
|
|
1251
|
+
task: state.task,
|
|
1252
|
+
taskType: state.taskType ?? state.intent.taskType ?? null,
|
|
1253
|
+
status: state.status,
|
|
1254
|
+
turn: state.input.turn,
|
|
1255
|
+
...(state.requestIntent
|
|
1256
|
+
? { requestIntent: structuredClone(state.requestIntent) }
|
|
1257
|
+
: {}),
|
|
1258
|
+
memoryOwner: state.memoryOwner,
|
|
1259
|
+
});
|
|
751
1260
|
}
|
|
752
1261
|
/** Refresh only authority facts after a committed control operation; retain the exact human request. */
|
|
753
1262
|
taskStateChanged(task) {
|
|
754
1263
|
const state = this.#turns.get(task.sessionId);
|
|
755
|
-
if (!state ||
|
|
1264
|
+
if (!state ||
|
|
1265
|
+
['closed', 'stale'].includes(state.status) ||
|
|
1266
|
+
state.input.signal.aborted ||
|
|
1267
|
+
!state.requestIntentContext)
|
|
756
1268
|
return;
|
|
757
1269
|
state.nativeUnowned = false;
|
|
758
1270
|
const previous = state.requestIntentContext.activeTask;
|
|
759
|
-
if (previous?.taskId === task.taskId &&
|
|
1271
|
+
if (previous?.taskId === task.taskId &&
|
|
1272
|
+
previous.revision === task.revision &&
|
|
1273
|
+
previous.generation === task.generation)
|
|
760
1274
|
return;
|
|
761
|
-
const context = createRequestIntentContext({
|
|
762
|
-
...
|
|
1275
|
+
const context = createRequestIntentContext({
|
|
1276
|
+
...state.requestIntentContext,
|
|
1277
|
+
activeTask: task,
|
|
1278
|
+
...(state.requestIntentContext.activeGoal?.runId === task.runId
|
|
1279
|
+
? {}
|
|
1280
|
+
: { activeGoal: undefined }),
|
|
1281
|
+
});
|
|
763
1282
|
if (!context)
|
|
764
1283
|
return;
|
|
765
1284
|
state.requestIntentContext = context;
|
|
@@ -774,19 +1293,40 @@ export class OnDemandIntake {
|
|
|
774
1293
|
/** Exact registered native-tool proof, never a model flag or a generic task-control bypass. */
|
|
775
1294
|
unownedNative(execution) {
|
|
776
1295
|
const state = this.bound(execution), proof = this.#executions.get(execution);
|
|
777
|
-
return !this.#stopped &&
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1296
|
+
return (!this.#stopped &&
|
|
1297
|
+
!execution.signal.aborted &&
|
|
1298
|
+
UNOWNED_NATIVE_TOOLS.has(execution.name) &&
|
|
1299
|
+
execution.parent === undefined &&
|
|
1300
|
+
!!state &&
|
|
1301
|
+
state.nativeUnowned === true &&
|
|
1302
|
+
state.status === 'answer' &&
|
|
1303
|
+
!state.input.signal.aborted &&
|
|
1304
|
+
!!proof &&
|
|
1305
|
+
proof.turn === state &&
|
|
1306
|
+
proof.execute === this.definition(execution)?.execute &&
|
|
1307
|
+
proof.nativeUnownedDigest === canonicalContentHash(execution.arguments));
|
|
781
1308
|
}
|
|
782
1309
|
/** Direct native Bash follows native permissions, independently of optional task intake. */
|
|
783
1310
|
nativeCommand(execution) {
|
|
784
1311
|
const state = this.bound(execution), actual = this.definition(execution), proof = this.#executions.get(execution);
|
|
785
|
-
if (!this.options.onlyWhenDeferred &&
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1312
|
+
if (!this.options.onlyWhenDeferred &&
|
|
1313
|
+
!this.#stopped &&
|
|
1314
|
+
actual &&
|
|
1315
|
+
isLispNativeCommand(execution, actual.execute))
|
|
1316
|
+
return (!proof ||
|
|
1317
|
+
(proof.turn === state &&
|
|
1318
|
+
proof.execute === actual.execute &&
|
|
1319
|
+
proof.nativeCommandDigest ===
|
|
1320
|
+
canonicalContentHash(execution.arguments)));
|
|
1321
|
+
if (this.options.onlyWhenDeferred ||
|
|
1322
|
+
this.#stopped ||
|
|
1323
|
+
execution.signal.aborted ||
|
|
1324
|
+
execution.name !== 'bash' ||
|
|
1325
|
+
execution.parent !== undefined ||
|
|
1326
|
+
!state ||
|
|
1327
|
+
state.input.signal.aborted ||
|
|
1328
|
+
!actual ||
|
|
1329
|
+
(execution.agent && this.host.nativeChild?.(execution.agent)))
|
|
790
1330
|
return false;
|
|
791
1331
|
try {
|
|
792
1332
|
this.assertCurrent(state);
|
|
@@ -794,15 +1334,24 @@ export class OnDemandIntake {
|
|
|
794
1334
|
catch {
|
|
795
1335
|
return false;
|
|
796
1336
|
}
|
|
797
|
-
return !proof ||
|
|
798
|
-
|
|
1337
|
+
return (!proof ||
|
|
1338
|
+
(proof.turn === state &&
|
|
1339
|
+
proof.execute === actual.execute &&
|
|
1340
|
+
proof.nativeCommandDigest === canonicalContentHash(execution.arguments)));
|
|
799
1341
|
}
|
|
800
1342
|
/** Delegate real root Lisp tools to their own effect/lease/approval authority. */
|
|
801
1343
|
nativeLisp(execution) {
|
|
802
1344
|
const state = this.bound(execution), actual = this.definition(execution), proof = this.#executions.get(execution);
|
|
803
|
-
if (this.options.onlyWhenDeferred ||
|
|
804
|
-
|
|
805
|
-
|
|
1345
|
+
if (this.options.onlyWhenDeferred ||
|
|
1346
|
+
this.#stopped ||
|
|
1347
|
+
execution.signal.aborted ||
|
|
1348
|
+
execution.parent !== undefined ||
|
|
1349
|
+
!state ||
|
|
1350
|
+
state.status !== 'answer' ||
|
|
1351
|
+
state.input.signal.aborted ||
|
|
1352
|
+
!actual ||
|
|
1353
|
+
!isLispImplementation(actual.execute) ||
|
|
1354
|
+
(execution.agent && this.host.nativeChild?.(execution.agent)))
|
|
806
1355
|
return false;
|
|
807
1356
|
try {
|
|
808
1357
|
this.assertCurrent(state);
|
|
@@ -810,35 +1359,63 @@ export class OnDemandIntake {
|
|
|
810
1359
|
catch {
|
|
811
1360
|
return false;
|
|
812
1361
|
}
|
|
813
|
-
return !proof ||
|
|
814
|
-
|
|
1362
|
+
return (!proof ||
|
|
1363
|
+
(proof.turn === state &&
|
|
1364
|
+
proof.execute === actual.execute &&
|
|
1365
|
+
proof.nativeLispDigest === canonicalContentHash(execution.arguments)));
|
|
815
1366
|
}
|
|
816
1367
|
preparationOnly(execution) {
|
|
817
1368
|
if (this.isControl(execution))
|
|
818
1369
|
return true;
|
|
819
1370
|
const state = this.bound(execution), proof = this.#executions.get(execution);
|
|
820
|
-
if (this.#stopped ||
|
|
821
|
-
|
|
822
|
-
|
|
1371
|
+
if (this.#stopped ||
|
|
1372
|
+
execution.signal.aborted ||
|
|
1373
|
+
execution.parent !== undefined ||
|
|
1374
|
+
!state ||
|
|
1375
|
+
state.input.signal.aborted ||
|
|
1376
|
+
!proof ||
|
|
1377
|
+
proof.turn !== state ||
|
|
1378
|
+
this.definition(execution)?.execute !== proof.execute ||
|
|
1379
|
+
state.preparationCalls.has(execution.callId))
|
|
823
1380
|
return false;
|
|
824
1381
|
if (proof.carrier)
|
|
825
|
-
return execution.name === 'run_code' &&
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1382
|
+
return (execution.name === 'run_code' &&
|
|
1383
|
+
state.status === 'answer' &&
|
|
1384
|
+
canonicalContentHash(preparationCarrier(execution.arguments) ?? null) === proof.carrierDigest);
|
|
1385
|
+
return (proof.requestPreparation === true &&
|
|
1386
|
+
execution.name === TASK_PREPARE_TOOL &&
|
|
1387
|
+
(state.status === 'answer' ||
|
|
1388
|
+
(state.existing === true &&
|
|
1389
|
+
state.status === 'prepared' &&
|
|
1390
|
+
this.host.ready(state.input.agent, state.input.turn))));
|
|
829
1391
|
}
|
|
830
1392
|
pending(agent) {
|
|
831
1393
|
const state = agent.session && this.#turns.get(agent.session.id);
|
|
832
|
-
return !!state &&
|
|
1394
|
+
return (!!state &&
|
|
1395
|
+
!state.confirmationResume &&
|
|
1396
|
+
state.input.agent === agent &&
|
|
1397
|
+
!['prepared', 'closed', 'stale'].includes(state.status));
|
|
833
1398
|
}
|
|
834
1399
|
/** An admitted host continuation still needs capture to retire the previous turn's receipt. */
|
|
835
|
-
continuing(agent) {
|
|
1400
|
+
continuing(agent) {
|
|
1401
|
+
return !this.#stopped && this.host.ready(agent);
|
|
1402
|
+
}
|
|
836
1403
|
/** Exact captured task at a native lifecycle boundary, never a later selected task. */
|
|
837
1404
|
boundaryTask(session, turn) {
|
|
838
1405
|
const state = this.#turns.get(session.id), task = state?.requestIntentContext?.activeTask;
|
|
839
|
-
if (state?.input.agent.session !== session ||
|
|
1406
|
+
if (state?.input.agent.session !== session ||
|
|
1407
|
+
state.input.turn !== turn ||
|
|
1408
|
+
!task ||
|
|
1409
|
+
task.state !== 'Working')
|
|
840
1410
|
return undefined;
|
|
841
|
-
return {
|
|
1411
|
+
return {
|
|
1412
|
+
agent: state.input.agent,
|
|
1413
|
+
expected: {
|
|
1414
|
+
taskId: task.taskId,
|
|
1415
|
+
revision: task.revision,
|
|
1416
|
+
generation: task.generation,
|
|
1417
|
+
},
|
|
1418
|
+
};
|
|
842
1419
|
}
|
|
843
1420
|
finish(session, turn) {
|
|
844
1421
|
const state = this.#turns.get(session.id);
|
|
@@ -858,37 +1435,66 @@ export class OnDemandIntake {
|
|
|
858
1435
|
// Trust the validated host registry, not a later same-name replacement.
|
|
859
1436
|
this.#startupQuestionTool = tools.get('ask_user_question')?.execute;
|
|
860
1437
|
if (systemPrompt && !this.options.onlyWhenDeferred)
|
|
861
|
-
this.#disposers.push(systemPrompt.section({
|
|
862
|
-
|
|
1438
|
+
this.#disposers.push(systemPrompt.section({
|
|
1439
|
+
name: 'kiokuko:on-demand-intake',
|
|
1440
|
+
order: -99999,
|
|
1441
|
+
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. ' +
|
|
1442
|
+
ON_DEMAND_GUIDANCE,
|
|
1443
|
+
}));
|
|
1444
|
+
const definition = {
|
|
1445
|
+
name: TASK_PREPARE_TOOL,
|
|
1446
|
+
modelFacing: true,
|
|
863
1447
|
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.',
|
|
864
|
-
parameters: {
|
|
865
|
-
|
|
1448
|
+
parameters: {
|
|
1449
|
+
...JSON.parse(JSON.stringify(z.toJSONSchema(preparationFieldsSchema))),
|
|
1450
|
+
anyOf: [{ required: ['taskType'] }, { required: ['requestIntent'] }],
|
|
1451
|
+
},
|
|
1452
|
+
output: {
|
|
1453
|
+
schema: {},
|
|
1454
|
+
render: (_args, value) => [
|
|
1455
|
+
{ type: 'text', text: JSON.stringify(value) },
|
|
1456
|
+
],
|
|
1457
|
+
},
|
|
866
1458
|
execute: async (args, execution) => {
|
|
867
1459
|
const state = this.bound(execution);
|
|
868
1460
|
const value = parsePreparationRequest(args);
|
|
869
|
-
if (!state ||
|
|
1461
|
+
if (!state ||
|
|
1462
|
+
execution.name !== TASK_PREPARE_TOOL ||
|
|
1463
|
+
this.definition(execution)?.execute !== definition.execute ||
|
|
1464
|
+
!value)
|
|
870
1465
|
throw new Error('Invalid task preparation request');
|
|
871
|
-
if (state.preparationCalls.has(execution.callId) ||
|
|
1466
|
+
if (state.preparationCalls.has(execution.callId) ||
|
|
1467
|
+
!(state.status === 'answer' ||
|
|
1468
|
+
(state.status === 'prepared' &&
|
|
1469
|
+
this.host.ready(state.input.agent, state.input.turn))))
|
|
872
1470
|
throw new Error('Task preparation was already consumed for this turn');
|
|
873
1471
|
state.preparationCalls.add(execution.callId);
|
|
874
1472
|
try {
|
|
875
1473
|
if (state.existing) {
|
|
876
1474
|
this.assertCurrent(state);
|
|
877
1475
|
execution.signal.throwIfAborted();
|
|
878
|
-
const boundIntent = this.bindIntent(state, value.requestIntent);
|
|
879
|
-
if (boundIntent?.relation === 'same_scope_continuation' &&
|
|
1476
|
+
const boundIntent = await this.bindIntent(state, value.requestIntent);
|
|
1477
|
+
if (boundIntent?.relation === 'same_scope_continuation' &&
|
|
1478
|
+
!requestIntentIsUnresolved(boundIntent) &&
|
|
1479
|
+
boundIntent.action !== 'answer') {
|
|
880
1480
|
const intentType = requestIntentTaskType(boundIntent, state.taskType ?? state.intent.taskType ?? value.taskType);
|
|
881
|
-
if (value.taskType !== undefined &&
|
|
1481
|
+
if (value.taskType !== undefined &&
|
|
1482
|
+
intentType !== undefined &&
|
|
1483
|
+
value.taskType !== intentType) {
|
|
882
1484
|
throw new Error('The advisory taskType conflicts with the bound request action');
|
|
883
1485
|
}
|
|
884
1486
|
state.requestIntent = boundIntent;
|
|
885
1487
|
}
|
|
886
1488
|
else {
|
|
887
|
-
if (!boundIntent ||
|
|
1489
|
+
if (!boundIntent ||
|
|
1490
|
+
requestIntentIsUnresolved(boundIntent) ||
|
|
1491
|
+
boundIntent.action === 'answer') {
|
|
888
1492
|
throw new RequestClarification('Clarify whether this request continues, changes, or starts work before preparing it');
|
|
889
1493
|
}
|
|
890
1494
|
const intentType = requestIntentTaskType(boundIntent, state.taskType ?? state.intent.taskType ?? value.taskType);
|
|
891
|
-
if (!intentType ||
|
|
1495
|
+
if (!intentType ||
|
|
1496
|
+
intentType === 'chat' ||
|
|
1497
|
+
(value.taskType !== undefined && value.taskType !== intentType)) {
|
|
892
1498
|
throw new Error('The advisory taskType conflicts with the bound request action');
|
|
893
1499
|
}
|
|
894
1500
|
state.existing = false;
|
|
@@ -898,43 +1504,68 @@ export class OnDemandIntake {
|
|
|
898
1504
|
}
|
|
899
1505
|
}
|
|
900
1506
|
else {
|
|
901
|
-
const suppliedIntent = this.bindIntent(state, value.requestIntent);
|
|
1507
|
+
const suppliedIntent = await this.bindIntent(state, value.requestIntent);
|
|
902
1508
|
const currentIntent = suppliedIntent ?? state.requestIntent;
|
|
903
|
-
const unresolved = currentIntent !== undefined &&
|
|
904
|
-
|
|
1509
|
+
const unresolved = currentIntent !== undefined &&
|
|
1510
|
+
requestIntentIsUnresolved(currentIntent);
|
|
1511
|
+
if (unresolved) {
|
|
905
1512
|
throw new RequestClarification('Clarify the request before preparing work');
|
|
906
1513
|
}
|
|
907
|
-
if (state.requestIntentContext?.activeGoal &&
|
|
1514
|
+
if (state.requestIntentContext?.activeGoal &&
|
|
1515
|
+
currentIntent === undefined) {
|
|
908
1516
|
throw new RequestClarification('Clarify whether this request continues, changes, or starts the active Goal');
|
|
909
1517
|
}
|
|
910
|
-
const intentType = unresolved
|
|
911
|
-
|
|
1518
|
+
const intentType = unresolved
|
|
1519
|
+
? undefined
|
|
1520
|
+
: requestIntentTaskType(currentIntent, state.taskType ?? state.intent.taskType ?? value.taskType);
|
|
1521
|
+
if (value.taskType !== undefined &&
|
|
1522
|
+
intentType !== undefined &&
|
|
1523
|
+
value.taskType !== intentType) {
|
|
912
1524
|
throw new Error('The advisory taskType conflicts with the bound request action');
|
|
913
1525
|
}
|
|
914
1526
|
const taskType = intentType ?? value.taskType;
|
|
915
|
-
if (!taskType ||
|
|
1527
|
+
if (!taskType ||
|
|
1528
|
+
taskType === 'chat' ||
|
|
1529
|
+
!ACTION_TYPES.includes(taskType))
|
|
916
1530
|
throw new RequestClarification('Answer ordinary questions directly; clarify unknown actions before preparing work');
|
|
917
1531
|
state.memoryOwner = memoryOwnerKind(currentIntent, taskType);
|
|
918
1532
|
await this.prepare(state, taskType, execution.signal, currentIntent, value.requestIntent !== undefined);
|
|
919
1533
|
}
|
|
920
|
-
return {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1534
|
+
return {
|
|
1535
|
+
prepared: true,
|
|
1536
|
+
taskType: state.taskType ??
|
|
1537
|
+
value.taskType ??
|
|
1538
|
+
requestIntentTaskType(state.requestIntent),
|
|
1539
|
+
originalTask: state.task,
|
|
1540
|
+
...(state.requestIntent
|
|
1541
|
+
? { requestIntent: state.requestIntent }
|
|
1542
|
+
: {}),
|
|
1543
|
+
...(state.requestIntent &&
|
|
1544
|
+
requestIntentIsUnresolved(state.requestIntent)
|
|
1545
|
+
? {
|
|
1546
|
+
requestInterpretation: requestInterpretationRecovery(state.requestIntent, state.requestIntentContext, true),
|
|
1547
|
+
}
|
|
1548
|
+
: {}),
|
|
1549
|
+
memoryOwner: state.memoryOwner,
|
|
1550
|
+
permissions: 'unchanged_native_policy',
|
|
1551
|
+
};
|
|
924
1552
|
}
|
|
925
1553
|
catch (error) {
|
|
926
1554
|
if (!(error instanceof RequestClarification))
|
|
927
1555
|
throw error;
|
|
928
1556
|
return this.clarify(state, execution.signal, error);
|
|
929
1557
|
}
|
|
930
|
-
}
|
|
1558
|
+
},
|
|
1559
|
+
};
|
|
931
1560
|
this.#disposers.push(tools.register(definition));
|
|
932
1561
|
this.#disposers.push(context.on('tools/pre-execute', async (execution, next) => {
|
|
933
1562
|
if (this.#stopped || execution.signal.aborted)
|
|
934
1563
|
return denied('On-demand intake is stopped or cancelled');
|
|
935
1564
|
if (execution.agent && this.host.nativeChild?.(execution.agent)) {
|
|
936
1565
|
const decision = await next();
|
|
937
|
-
return decision?.kind === 'cancel'
|
|
1566
|
+
return decision?.kind === 'cancel'
|
|
1567
|
+
? denied('Native tool policy cancelled this call')
|
|
1568
|
+
: decision;
|
|
938
1569
|
}
|
|
939
1570
|
await this.bindExecutorTransport(execution, context);
|
|
940
1571
|
const actual = this.definition(execution);
|
|
@@ -943,7 +1574,9 @@ export class OnDemandIntake {
|
|
|
943
1574
|
const control = this.controlCall(execution, actual);
|
|
944
1575
|
if (control) {
|
|
945
1576
|
const turn = this.bound(execution);
|
|
946
|
-
if (execution.agent?.session &&
|
|
1577
|
+
if (execution.agent?.session &&
|
|
1578
|
+
this.#turns.has(execution.agent.session.id) &&
|
|
1579
|
+
!turn)
|
|
947
1580
|
return denied('Stale task control request');
|
|
948
1581
|
if (turn) {
|
|
949
1582
|
try {
|
|
@@ -953,70 +1586,127 @@ export class OnDemandIntake {
|
|
|
953
1586
|
return denied('Stale task control request');
|
|
954
1587
|
}
|
|
955
1588
|
}
|
|
956
|
-
this.#controlExecutions.set(execution, {
|
|
1589
|
+
this.#controlExecutions.set(execution, {
|
|
1590
|
+
...control,
|
|
1591
|
+
...(turn ? { turn } : {}),
|
|
1592
|
+
});
|
|
957
1593
|
const decision = await next();
|
|
958
|
-
return decision?.kind === 'cancel'
|
|
1594
|
+
return decision?.kind === 'cancel'
|
|
1595
|
+
? denied('Native tool policy cancelled this call')
|
|
1596
|
+
: decision;
|
|
959
1597
|
}
|
|
960
1598
|
const state = this.bound(execution);
|
|
961
1599
|
if (!state) {
|
|
962
|
-
if (execution.agent?.session &&
|
|
1600
|
+
if (execution.agent?.session &&
|
|
1601
|
+
this.#turns.has(execution.agent.session.id))
|
|
963
1602
|
return denied('Stale on-demand native turn');
|
|
964
1603
|
const turn = this.existingTurn(execution);
|
|
965
|
-
if (!execution.agent ||
|
|
1604
|
+
if (!execution.agent ||
|
|
1605
|
+
turn === undefined ||
|
|
1606
|
+
!this.host.ready(execution.agent, turn))
|
|
966
1607
|
return denied('No current native request for tool execution');
|
|
967
1608
|
const decision = await next();
|
|
968
|
-
return decision?.kind === 'cancel'
|
|
1609
|
+
return decision?.kind === 'cancel'
|
|
1610
|
+
? denied('Native tool policy cancelled this call')
|
|
1611
|
+
: decision;
|
|
969
1612
|
}
|
|
970
1613
|
try {
|
|
971
1614
|
this.assertCurrent(state);
|
|
972
|
-
if (state.confirmationResume &&
|
|
1615
|
+
if (state.confirmationResume &&
|
|
1616
|
+
execution.name !== TASK_PREPARE_TOOL &&
|
|
1617
|
+
this.host.ready(state.input.agent, state.input.turn))
|
|
973
1618
|
this.acknowledgeExisting(state);
|
|
974
1619
|
if (this.nativeCommand(execution)) {
|
|
975
|
-
this.#executions.set(execution, {
|
|
1620
|
+
this.#executions.set(execution, {
|
|
1621
|
+
turn: state,
|
|
1622
|
+
execute: actual.execute,
|
|
1623
|
+
nativeCommandDigest: canonicalContentHash(execution.arguments),
|
|
1624
|
+
});
|
|
976
1625
|
const decision = await next();
|
|
977
|
-
return decision?.kind === 'cancel'
|
|
1626
|
+
return decision?.kind === 'cancel'
|
|
1627
|
+
? denied('Native tool policy cancelled this call')
|
|
1628
|
+
: decision;
|
|
978
1629
|
}
|
|
979
1630
|
if (this.nativeLisp(execution)) {
|
|
980
|
-
this.#executions.set(execution, {
|
|
1631
|
+
this.#executions.set(execution, {
|
|
1632
|
+
turn: state,
|
|
1633
|
+
execute: actual.execute,
|
|
1634
|
+
nativeLispDigest: canonicalContentHash(execution.arguments),
|
|
1635
|
+
});
|
|
981
1636
|
const decision = await next();
|
|
982
|
-
return decision?.kind === 'cancel'
|
|
1637
|
+
return decision?.kind === 'cancel'
|
|
1638
|
+
? denied('Native tool policy cancelled this call')
|
|
1639
|
+
: decision;
|
|
983
1640
|
}
|
|
984
1641
|
// Evidence gathering does not decide the request's action or scope.
|
|
985
1642
|
// In particular, an inherited owner must not turn a read into an
|
|
986
1643
|
// immutable research preparation before the main model can interpret it.
|
|
987
|
-
if (state.status === 'answer' &&
|
|
1644
|
+
if (state.status === 'answer' &&
|
|
1645
|
+
!state.nativeUnowned &&
|
|
1646
|
+
execution.parent === undefined &&
|
|
1647
|
+
NATIVE_EVIDENCE_READS.has(execution.name)) {
|
|
988
1648
|
const clarification = executionClarification(state.task, state.requestIntent, state.requestIntentContext);
|
|
989
1649
|
if (clarification)
|
|
990
1650
|
return denied(clarification);
|
|
991
|
-
this.#executions.set(execution, {
|
|
1651
|
+
this.#executions.set(execution, {
|
|
1652
|
+
turn: state,
|
|
1653
|
+
execute: actual.execute,
|
|
1654
|
+
evidenceReadDigest: canonicalContentHash(execution.arguments),
|
|
1655
|
+
});
|
|
992
1656
|
const decision = await next();
|
|
993
|
-
return decision?.kind === 'cancel'
|
|
1657
|
+
return decision?.kind === 'cancel'
|
|
1658
|
+
? denied('Native tool policy cancelled this call')
|
|
1659
|
+
: decision;
|
|
994
1660
|
}
|
|
995
|
-
if (state.nativeUnowned &&
|
|
996
|
-
|
|
1661
|
+
if (state.nativeUnowned &&
|
|
1662
|
+
UNOWNED_NATIVE_TOOLS.has(execution.name) &&
|
|
1663
|
+
execution.parent === undefined) {
|
|
1664
|
+
if ((await this.host.nativeOwnership?.(state.input)) !== 'unowned') {
|
|
997
1665
|
state.nativeUnowned = false;
|
|
998
1666
|
return denied('Native execution acquired a Kiokuko owner; its current authority is required');
|
|
999
1667
|
}
|
|
1000
1668
|
this.assertCurrent(state);
|
|
1001
1669
|
if (this.definition(execution)?.execute !== actual.execute)
|
|
1002
1670
|
return denied('Native tool definition changed');
|
|
1003
|
-
this.#executions.set(execution, {
|
|
1671
|
+
this.#executions.set(execution, {
|
|
1672
|
+
turn: state,
|
|
1673
|
+
execute: actual.execute,
|
|
1674
|
+
nativeUnownedDigest: canonicalContentHash(execution.arguments),
|
|
1675
|
+
});
|
|
1004
1676
|
const decision = await next();
|
|
1005
|
-
return decision?.kind === 'cancel'
|
|
1677
|
+
return decision?.kind === 'cancel'
|
|
1678
|
+
? denied('Native tool policy cancelled this call')
|
|
1679
|
+
: decision;
|
|
1006
1680
|
}
|
|
1007
1681
|
const skillReader = this.#skillReaders.get(state.input.agent);
|
|
1008
|
-
const skillRead = execution.name === 'skill' &&
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1682
|
+
const skillRead = execution.name === 'skill' &&
|
|
1683
|
+
skillReader !== undefined &&
|
|
1684
|
+
actual.execute === skillReader &&
|
|
1685
|
+
state.status === 'answer';
|
|
1686
|
+
const carrier = execution.name === 'run_code'
|
|
1687
|
+
? preparationCarrier(execution.arguments)
|
|
1688
|
+
: undefined;
|
|
1689
|
+
const carrierDigest = carrier
|
|
1690
|
+
? canonicalContentHash(carrier)
|
|
1691
|
+
: undefined;
|
|
1692
|
+
if (execution.name === 'run_code' &&
|
|
1693
|
+
state.status !== 'prepared' &&
|
|
1694
|
+
!carrier &&
|
|
1695
|
+
!automaticPreparationType(state.intent, state.requestIntent, execution.name))
|
|
1013
1696
|
return denied('Clarify the requested work before code execution.');
|
|
1014
1697
|
if (carrier) {
|
|
1015
|
-
if (!(state.status === 'answer' ||
|
|
1698
|
+
if (!(state.status === 'answer' ||
|
|
1699
|
+
(state.status === 'prepared' &&
|
|
1700
|
+
this.host.ready(state.input.agent, state.input.turn))) ||
|
|
1701
|
+
state.preparationCalls.has(execution.callId))
|
|
1016
1702
|
return denied('Preparation carrier is not current');
|
|
1017
1703
|
}
|
|
1018
1704
|
else if (execution.name === TASK_PREPARE_TOOL) {
|
|
1019
|
-
if (actual.execute !== definition.execute ||
|
|
1705
|
+
if (actual.execute !== definition.execute ||
|
|
1706
|
+
!(state.status === 'answer' ||
|
|
1707
|
+
(state.status === 'prepared' &&
|
|
1708
|
+
this.host.ready(state.input.agent, state.input.turn))) ||
|
|
1709
|
+
state.preparationCalls.has(execution.callId))
|
|
1020
1710
|
return denied('Task preparation is not current');
|
|
1021
1711
|
}
|
|
1022
1712
|
else if (!skillRead) {
|
|
@@ -1032,13 +1722,21 @@ export class OnDemandIntake {
|
|
|
1032
1722
|
this.assertCurrent(state);
|
|
1033
1723
|
if (this.definition(execution)?.execute !== actual.execute)
|
|
1034
1724
|
return denied('Native tool definition changed during preparation');
|
|
1035
|
-
this.#executions.set(execution, {
|
|
1036
|
-
|
|
1037
|
-
|
|
1725
|
+
this.#executions.set(execution, {
|
|
1726
|
+
turn: state,
|
|
1727
|
+
execute: actual.execute,
|
|
1728
|
+
...(carrier && carrierDigest ? { carrier, carrierDigest } : {}),
|
|
1729
|
+
...(skillRead ? { skillRead: true } : {}),
|
|
1730
|
+
...(execution.name === TASK_PREPARE_TOOL
|
|
1731
|
+
? { requestPreparation: true }
|
|
1732
|
+
: {}),
|
|
1733
|
+
});
|
|
1038
1734
|
// Preserve native allow/deny/ask unchanged. Approval is resolved by the native registry AFTER this waterfall.
|
|
1039
1735
|
const decision = await next();
|
|
1040
1736
|
// DSH 0.1.5 does not understand cancel and otherwise dispatches it without running guards.
|
|
1041
|
-
return decision?.kind === 'cancel'
|
|
1737
|
+
return decision?.kind === 'cancel'
|
|
1738
|
+
? denied('Native tool policy cancelled this call')
|
|
1739
|
+
: decision;
|
|
1042
1740
|
}
|
|
1043
1741
|
catch (error) {
|
|
1044
1742
|
if (error instanceof RequestClarification) {
|
|
@@ -1047,10 +1745,14 @@ export class OnDemandIntake {
|
|
|
1047
1745
|
return denied(`Execution was not prepared; no work was executed. ${JSON.stringify(result)}`);
|
|
1048
1746
|
}
|
|
1049
1747
|
catch (cancelled) {
|
|
1050
|
-
return denied(cancelled instanceof Error
|
|
1748
|
+
return denied(cancelled instanceof Error
|
|
1749
|
+
? cancelled.message
|
|
1750
|
+
: 'Clarification cancelled');
|
|
1051
1751
|
}
|
|
1052
1752
|
}
|
|
1053
|
-
return denied(error instanceof Error
|
|
1753
|
+
return denied(error instanceof Error
|
|
1754
|
+
? error.message
|
|
1755
|
+
: 'Execution preparation failed');
|
|
1054
1756
|
}
|
|
1055
1757
|
}, { prepend: true }));
|
|
1056
1758
|
this.#disposers.push(context.on('tools/execute', async (execution, next) => {
|
|
@@ -1060,21 +1762,37 @@ export class OnDemandIntake {
|
|
|
1060
1762
|
if (control) {
|
|
1061
1763
|
if (!this.isControl(execution))
|
|
1062
1764
|
throw new Error('Task control identity or arguments changed');
|
|
1063
|
-
execution.signal = AbortSignal.any([
|
|
1765
|
+
execution.signal = AbortSignal.any([
|
|
1766
|
+
execution.signal,
|
|
1767
|
+
this.#lifecycle.signal,
|
|
1768
|
+
...(control.turn ? [control.turn.input.signal] : []),
|
|
1769
|
+
]);
|
|
1064
1770
|
if (!control.invoke)
|
|
1065
1771
|
return next();
|
|
1066
1772
|
const result = await control.invoke(control.args, execution);
|
|
1067
|
-
return {
|
|
1068
|
-
|
|
1773
|
+
return {
|
|
1774
|
+
isError: false,
|
|
1775
|
+
value: {
|
|
1776
|
+
logs: ['Host task operation. No program was evaluated.'],
|
|
1777
|
+
result,
|
|
1778
|
+
},
|
|
1779
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
1780
|
+
};
|
|
1069
1781
|
}
|
|
1070
1782
|
const proof = this.#executions.get(execution);
|
|
1071
1783
|
if (!proof)
|
|
1072
1784
|
return next(); // Existing independently admitted runs keep their original path.
|
|
1073
1785
|
const state = this.bound(execution);
|
|
1074
|
-
if (!state ||
|
|
1786
|
+
if (!state ||
|
|
1787
|
+
proof.turn !== state ||
|
|
1788
|
+
this.definition(execution)?.execute !== proof.execute)
|
|
1075
1789
|
throw new Error('On-demand dispatch identity changed');
|
|
1076
1790
|
this.assertCurrent(state);
|
|
1077
|
-
execution.signal = AbortSignal.any([
|
|
1791
|
+
execution.signal = AbortSignal.any([
|
|
1792
|
+
execution.signal,
|
|
1793
|
+
state.input.signal,
|
|
1794
|
+
this.#lifecycle.signal,
|
|
1795
|
+
]);
|
|
1078
1796
|
if (proof.nativeCommandDigest !== undefined) {
|
|
1079
1797
|
if (!this.nativeCommand(execution))
|
|
1080
1798
|
throw new Error('Native command identity or arguments changed');
|
|
@@ -1086,13 +1804,17 @@ export class OnDemandIntake {
|
|
|
1086
1804
|
return withRootLispAdmission(execution, () => this.nativeLisp(execution), next);
|
|
1087
1805
|
}
|
|
1088
1806
|
if (proof.evidenceReadDigest !== undefined) {
|
|
1089
|
-
if (state.status !== 'answer' ||
|
|
1090
|
-
|
|
1807
|
+
if (state.status !== 'answer' ||
|
|
1808
|
+
execution.parent !== undefined ||
|
|
1809
|
+
!NATIVE_EVIDENCE_READS.has(execution.name) ||
|
|
1810
|
+
canonicalContentHash(execution.arguments) !==
|
|
1811
|
+
proof.evidenceReadDigest)
|
|
1091
1812
|
throw new Error('Native evidence read identity or arguments changed');
|
|
1092
1813
|
return next();
|
|
1093
1814
|
}
|
|
1094
1815
|
if (proof.nativeUnownedDigest !== undefined) {
|
|
1095
|
-
if (!this.unownedNative(execution) ||
|
|
1816
|
+
if (!this.unownedNative(execution) ||
|
|
1817
|
+
(await this.host.nativeOwnership?.(state.input)) !== 'unowned') {
|
|
1096
1818
|
state.nativeUnowned = false;
|
|
1097
1819
|
throw new Error('Native unowned authority changed or became unavailable');
|
|
1098
1820
|
}
|
|
@@ -1102,21 +1824,26 @@ export class OnDemandIntake {
|
|
|
1102
1824
|
if (!proof.carrier)
|
|
1103
1825
|
return next();
|
|
1104
1826
|
const carrier = preparationCarrier(execution.arguments);
|
|
1105
|
-
if (!(state.status === 'answer' ||
|
|
1106
|
-
|
|
1827
|
+
if (!(state.status === 'answer' ||
|
|
1828
|
+
(state.status === 'prepared' &&
|
|
1829
|
+
this.host.ready(state.input.agent, state.input.turn))) ||
|
|
1830
|
+
state.preparationCalls.has(execution.callId) ||
|
|
1831
|
+
!carrier ||
|
|
1832
|
+
canonicalContentHash(carrier) !== proof.carrierDigest)
|
|
1107
1833
|
throw new Error('Preparation carrier was already consumed or changed');
|
|
1108
1834
|
state.preparationCalls.add(execution.callId);
|
|
1109
1835
|
try {
|
|
1110
|
-
const requestIntent = this.bindIntent(state, carrier.requestIntent);
|
|
1836
|
+
const requestIntent = await this.bindIntent(state, carrier.requestIntent);
|
|
1111
1837
|
const clarification = executionClarification(state.task, requestIntent, state.requestIntentContext);
|
|
1112
1838
|
if (clarification)
|
|
1113
1839
|
throw new RequestClarification(clarification);
|
|
1114
|
-
const unresolved = requestIntent !== undefined &&
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
if (unresolved && !legacyTaskTypeAllowed)
|
|
1840
|
+
const unresolved = requestIntent !== undefined &&
|
|
1841
|
+
requestIntentIsUnresolved(requestIntent);
|
|
1842
|
+
if (unresolved)
|
|
1118
1843
|
throw new RequestClarification('Clarify the requested action and scope before preparing work');
|
|
1119
|
-
const intentType = unresolved
|
|
1844
|
+
const intentType = unresolved
|
|
1845
|
+
? undefined
|
|
1846
|
+
: requestIntentTaskType(requestIntent, state.taskType ?? state.intent.taskType ?? carrier.taskType);
|
|
1120
1847
|
const taskType = intentType ?? carrier.taskType;
|
|
1121
1848
|
if (carrier.taskType !== undefined && taskType !== carrier.taskType)
|
|
1122
1849
|
throw new Error('The advisory taskType conflicts with the bound request action');
|
|
@@ -1125,20 +1852,50 @@ export class OnDemandIntake {
|
|
|
1125
1852
|
}
|
|
1126
1853
|
state.memoryOwner = memoryOwnerKind(requestIntent, taskType);
|
|
1127
1854
|
await this.prepare(state, taskType, execution.signal, requestIntent, carrier.requestIntent !== undefined);
|
|
1128
|
-
const result = {
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1855
|
+
const result = {
|
|
1856
|
+
prepared: true,
|
|
1857
|
+
taskType: state.taskType,
|
|
1858
|
+
originalTask: state.task,
|
|
1859
|
+
...(state.requestIntent
|
|
1860
|
+
? { requestIntent: state.requestIntent }
|
|
1861
|
+
: {}),
|
|
1862
|
+
...(state.requestIntent &&
|
|
1863
|
+
requestIntentIsUnresolved(state.requestIntent)
|
|
1864
|
+
? {
|
|
1865
|
+
requestInterpretation: requestInterpretationRecovery(state.requestIntent, state.requestIntentContext, true),
|
|
1866
|
+
}
|
|
1867
|
+
: {}),
|
|
1868
|
+
memoryOwner: state.memoryOwner,
|
|
1869
|
+
permissions: 'unchanged_native_policy',
|
|
1870
|
+
programExecuted: false,
|
|
1871
|
+
};
|
|
1872
|
+
return {
|
|
1873
|
+
isError: false,
|
|
1874
|
+
value: {
|
|
1875
|
+
logs: ['Host intake prepared. No program was evaluated.'],
|
|
1876
|
+
result,
|
|
1877
|
+
},
|
|
1878
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
1879
|
+
};
|
|
1133
1880
|
}
|
|
1134
1881
|
catch (error) {
|
|
1135
1882
|
if (!(error instanceof RequestClarification))
|
|
1136
1883
|
throw error;
|
|
1137
|
-
const result = {
|
|
1138
|
-
|
|
1884
|
+
const result = {
|
|
1885
|
+
...(await this.clarify(state, execution.signal, error)),
|
|
1886
|
+
programExecuted: false,
|
|
1887
|
+
};
|
|
1888
|
+
return {
|
|
1889
|
+
isError: false,
|
|
1890
|
+
value: {
|
|
1891
|
+
logs: ['Execution was not prepared. No program was evaluated.'],
|
|
1892
|
+
result,
|
|
1893
|
+
},
|
|
1894
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
1895
|
+
};
|
|
1139
1896
|
}
|
|
1140
1897
|
}, { prepend: true }));
|
|
1141
|
-
this.#disposers.push(tools.guard(execution => {
|
|
1898
|
+
this.#disposers.push(tools.guard((execution) => {
|
|
1142
1899
|
if (this.#stopped || execution.signal.aborted)
|
|
1143
1900
|
return 'On-demand intake is stopped or cancelled';
|
|
1144
1901
|
if (execution.agent && this.host.nativeChild?.(execution.agent))
|
|
@@ -1149,39 +1906,75 @@ export class OnDemandIntake {
|
|
|
1149
1906
|
if (!actual)
|
|
1150
1907
|
return 'Unknown native tool definition';
|
|
1151
1908
|
if (!state) {
|
|
1152
|
-
if (execution.agent?.session &&
|
|
1909
|
+
if (execution.agent?.session &&
|
|
1910
|
+
this.#turns.has(execution.agent.session.id))
|
|
1153
1911
|
return 'Stale on-demand native turn';
|
|
1154
1912
|
const turn = this.existingTurn(execution);
|
|
1155
|
-
return execution.agent &&
|
|
1913
|
+
return execution.agent &&
|
|
1914
|
+
turn !== undefined &&
|
|
1915
|
+
this.host.ready(execution.agent, turn)
|
|
1916
|
+
? undefined
|
|
1917
|
+
: 'No current native request for tool execution';
|
|
1156
1918
|
}
|
|
1157
1919
|
const proof = this.#executions.get(execution);
|
|
1158
|
-
if (!proof ||
|
|
1920
|
+
if (!proof ||
|
|
1921
|
+
proof.turn !== state ||
|
|
1922
|
+
proof.execute !== actual.execute ||
|
|
1923
|
+
['closed', 'stale', 'capturing'].includes(state.status))
|
|
1159
1924
|
return 'Tool execution has no current on-demand preparation';
|
|
1160
1925
|
if (proof.nativeCommandDigest !== undefined)
|
|
1161
|
-
return this.nativeCommand(execution)
|
|
1926
|
+
return this.nativeCommand(execution)
|
|
1927
|
+
? undefined
|
|
1928
|
+
: 'Native command identity or arguments changed';
|
|
1162
1929
|
if (proof.nativeLispDigest !== undefined)
|
|
1163
|
-
return this.nativeLisp(execution)
|
|
1930
|
+
return this.nativeLisp(execution)
|
|
1931
|
+
? undefined
|
|
1932
|
+
: 'Native Lisp identity or arguments changed';
|
|
1164
1933
|
if (proof.evidenceReadDigest !== undefined)
|
|
1165
|
-
return state.status === 'answer' &&
|
|
1166
|
-
|
|
1167
|
-
|
|
1934
|
+
return state.status === 'answer' &&
|
|
1935
|
+
execution.parent === undefined &&
|
|
1936
|
+
NATIVE_EVIDENCE_READS.has(execution.name) &&
|
|
1937
|
+
canonicalContentHash(execution.arguments) ===
|
|
1938
|
+
proof.evidenceReadDigest
|
|
1939
|
+
? undefined
|
|
1940
|
+
: 'Native evidence read identity or arguments changed';
|
|
1168
1941
|
if (proof.nativeUnownedDigest !== undefined)
|
|
1169
|
-
return this.unownedNative(execution)
|
|
1942
|
+
return this.unownedNative(execution)
|
|
1943
|
+
? undefined
|
|
1944
|
+
: 'Native unowned execution proof changed';
|
|
1170
1945
|
if (proof.carrier)
|
|
1171
|
-
return (state.status === 'answer' ||
|
|
1172
|
-
|
|
1946
|
+
return (state.status === 'answer' ||
|
|
1947
|
+
(state.status === 'prepared' &&
|
|
1948
|
+
this.host.ready(state.input.agent, state.input.turn))) &&
|
|
1949
|
+
canonicalContentHash(preparationCarrier(execution.arguments) ?? null) === proof.carrierDigest
|
|
1950
|
+
? undefined
|
|
1951
|
+
: 'Preparation carrier is not current';
|
|
1173
1952
|
if (proof.skillRead)
|
|
1174
|
-
return state.status === 'answer' &&
|
|
1953
|
+
return state.status === 'answer' &&
|
|
1954
|
+
actual.execute === this.#skillReaders.get(state.input.agent)
|
|
1955
|
+
? undefined
|
|
1956
|
+
: 'Conversational Skill read is not current';
|
|
1175
1957
|
if (execution.name === TASK_PREPARE_TOOL)
|
|
1176
|
-
return actual.execute === definition.execute &&
|
|
1177
|
-
|
|
1958
|
+
return actual.execute === definition.execute &&
|
|
1959
|
+
(state.status === 'answer' ||
|
|
1960
|
+
(state.status === 'prepared' &&
|
|
1961
|
+
this.host.ready(state.input.agent, state.input.turn)))
|
|
1962
|
+
? undefined
|
|
1963
|
+
: 'Task preparation is not current';
|
|
1964
|
+
return state.status === 'prepared' &&
|
|
1965
|
+
this.host.ready(state.input.agent, state.input.turn)
|
|
1966
|
+
? undefined
|
|
1967
|
+
: 'Execution task is not prepared';
|
|
1178
1968
|
}));
|
|
1179
1969
|
}
|
|
1180
1970
|
answerOwner(session) {
|
|
1181
1971
|
if (this.#stopped)
|
|
1182
1972
|
return undefined;
|
|
1183
1973
|
const state = this.#turns.get(session.id);
|
|
1184
|
-
if (!state ||
|
|
1974
|
+
if (!state ||
|
|
1975
|
+
state.status !== 'answer' ||
|
|
1976
|
+
state.input.agent.session !== session ||
|
|
1977
|
+
state.memoryOwner !== 'conversation')
|
|
1185
1978
|
return undefined;
|
|
1186
1979
|
const owner = this.host.answerContext?.owner(session, this.ownerIdentity(state));
|
|
1187
1980
|
return state.input.agent === owner?.agent ? owner : undefined;
|
|
@@ -1189,17 +1982,28 @@ export class OnDemandIntake {
|
|
|
1189
1982
|
retire(agent) {
|
|
1190
1983
|
this.host.answerContext?.retire(agent);
|
|
1191
1984
|
const state = agent.session && this.#turns.get(agent.session.id);
|
|
1192
|
-
if (state?.input.agent !== agent ||
|
|
1985
|
+
if (state?.input.agent !== agent ||
|
|
1986
|
+
state.input.agent.session !== agent.session)
|
|
1193
1987
|
return;
|
|
1194
1988
|
state.controller.abort(new Error('Native agent disposed'));
|
|
1195
1989
|
state.status = 'closed';
|
|
1196
1990
|
this.#turns.delete(agent.session.id);
|
|
1197
1991
|
this.#clarifications.delete(agent.session.id);
|
|
1198
1992
|
}
|
|
1199
|
-
stop() {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1993
|
+
stop() {
|
|
1994
|
+
this.#clarifications.clear();
|
|
1995
|
+
this.#stopped = true;
|
|
1996
|
+
this.#lifecycle.abort(new Error('On-demand intake stopped'));
|
|
1997
|
+
}
|
|
1998
|
+
async drain() {
|
|
1999
|
+
while (this.#pending.size)
|
|
2000
|
+
await Promise.allSettled([...this.#pending]);
|
|
2001
|
+
}
|
|
2002
|
+
async dispose() {
|
|
2003
|
+
this.stop();
|
|
2004
|
+
await this.drain();
|
|
2005
|
+
for (const dispose of this.#disposers.reverse())
|
|
2006
|
+
dispose();
|
|
2007
|
+
}
|
|
1204
2008
|
}
|
|
1205
2009
|
//# sourceMappingURL=on-demand-intake.js.map
|