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