deepclause-sdk 0.2.2 → 0.4.0
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/README.md +86 -3
- package/dist/agent.d.ts +17 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +148 -42
- package/dist/agent.js.map +1 -1
- package/dist/cli/config.d.ts +12 -12
- package/dist/cli/repl.d.ts +13 -1
- package/dist/cli/repl.d.ts.map +1 -1
- package/dist/cli/repl.js +177 -5
- package/dist/cli/repl.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +4 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/forms/index.d.ts +3 -0
- package/dist/forms/index.d.ts.map +1 -0
- package/dist/forms/index.js +3 -0
- package/dist/forms/index.js.map +1 -0
- package/dist/forms/registry.d.ts +119 -0
- package/dist/forms/registry.d.ts.map +1 -0
- package/dist/forms/registry.js +256 -0
- package/dist/forms/registry.js.map +1 -0
- package/dist/forms/spec.d.ts +193 -0
- package/dist/forms/spec.d.ts.map +1 -0
- package/dist/forms/spec.js +295 -0
- package/dist/forms/spec.js.map +1 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/prolog/loader.d.ts +18 -0
- package/dist/prolog/loader.d.ts.map +1 -1
- package/dist/prolog/loader.js +26 -1
- package/dist/prolog/loader.js.map +1 -1
- package/dist/prolog-src/deepclause_mi.pl +786 -40
- package/dist/repl/agent.d.ts +11 -0
- package/dist/repl/agent.d.ts.map +1 -1
- package/dist/repl/agent.js +23 -9
- package/dist/repl/agent.js.map +1 -1
- package/dist/repl/form-widget.d.ts +56 -0
- package/dist/repl/form-widget.d.ts.map +1 -0
- package/dist/repl/form-widget.js +289 -0
- package/dist/repl/form-widget.js.map +1 -0
- package/dist/repl/harness-tools.d.ts +4 -1
- package/dist/repl/harness-tools.d.ts.map +1 -1
- package/dist/repl/harness-tools.js +15 -58
- package/dist/repl/harness-tools.js.map +1 -1
- package/dist/repl/history.d.ts +24 -1
- package/dist/repl/history.d.ts.map +1 -1
- package/dist/repl/history.js +76 -8
- package/dist/repl/history.js.map +1 -1
- package/dist/repl/index.d.ts +20 -2
- package/dist/repl/index.d.ts.map +1 -1
- package/dist/repl/index.js +9 -0
- package/dist/repl/index.js.map +1 -1
- package/dist/repl/process-backend.d.ts +2 -2
- package/dist/repl/process-backend.d.ts.map +1 -1
- package/dist/repl/process-executor.d.ts +11 -2
- package/dist/repl/process-executor.d.ts.map +1 -1
- package/dist/repl/process-executor.js +67 -18
- package/dist/repl/process-executor.js.map +1 -1
- package/dist/repl/process-worker.d.ts +35 -4
- package/dist/repl/process-worker.d.ts.map +1 -1
- package/dist/repl/process-worker.js +78 -38
- package/dist/repl/process-worker.js.map +1 -1
- package/dist/repl/processes.d.ts +4 -0
- package/dist/repl/processes.d.ts.map +1 -1
- package/dist/repl/processes.js +20 -0
- package/dist/repl/processes.js.map +1 -1
- package/dist/repl/render.d.ts +17 -0
- package/dist/repl/render.d.ts.map +1 -1
- package/dist/repl/render.js +60 -0
- package/dist/repl/render.js.map +1 -1
- package/dist/repl/session.d.ts +143 -7
- package/dist/repl/session.d.ts.map +1 -1
- package/dist/repl/session.js +317 -20
- package/dist/repl/session.js.map +1 -1
- package/dist/repl/tui/form-view.d.ts +17 -0
- package/dist/repl/tui/form-view.d.ts.map +1 -0
- package/dist/repl/tui/form-view.js +25 -0
- package/dist/repl/tui/form-view.js.map +1 -0
- package/dist/repl/tui/renderer.d.ts +4 -0
- package/dist/repl/tui/renderer.d.ts.map +1 -1
- package/dist/repl/tui/renderer.js +12 -0
- package/dist/repl/tui/renderer.js.map +1 -1
- package/dist/repl/worker-host.d.ts +16 -2
- package/dist/repl/worker-host.d.ts.map +1 -1
- package/dist/repl/worker-host.js +38 -6
- package/dist/repl/worker-host.js.map +1 -1
- package/dist/repl/worker-protocol.d.ts +14 -0
- package/dist/repl/worker-protocol.d.ts.map +1 -1
- package/dist/repl/worker-protocol.js +8 -1
- package/dist/repl/worker-protocol.js.map +1 -1
- package/dist/runner.d.ts +69 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +381 -44
- package/dist/runner.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +12 -0
- package/dist/sdk.js.map +1 -1
- package/dist/system/assets/docs/AGENT_PROMPT.md +452 -18
- package/dist/system/assets/docs/CHAT_PROMPT.md +28 -0
- package/dist/system/assets/docs/DML_AUTHORING.md +8 -4
- package/dist/system/assets/docs/DML_FORMS.md +740 -0
- package/dist/system/assets/docs/DML_REFERENCE.md +239 -4
- package/dist/system/assets/docs/HARNESS_EXAMPLES.md +94 -0
- package/dist/system/assets/index.d.ts +2 -2
- package/dist/system/assets/index.d.ts.map +1 -1
- package/dist/system/assets/index.js +4 -0
- package/dist/system/assets/index.js.map +1 -1
- package/dist/system/environment.d.ts +6 -0
- package/dist/system/environment.d.ts.map +1 -1
- package/dist/system/environment.js +5 -4
- package/dist/system/environment.js.map +1 -1
- package/dist/types.d.ts +48 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/prolog-src/deepclause_mi.pl +786 -40
package/dist/runner.js
CHANGED
|
@@ -5,6 +5,7 @@ import { mountWorkspace } from './prolog/loader.js';
|
|
|
5
5
|
import { runAgentLoop } from './agent.js';
|
|
6
6
|
import { takePrologMessages } from './prolog/loader.js';
|
|
7
7
|
import { validateToolArguments } from './repl/tool-arguments.js';
|
|
8
|
+
import { normalizeSpec, validateValues } from './forms/spec.js';
|
|
8
9
|
import { judgeAnswersToValues, judgeCacheKey, normalizeJudgeState, parseJudgeQuestions, validateJudgeAnswers, valuesToPrologList } from './judge/marshal.js';
|
|
9
10
|
import { buildToolCompletionEvent, buildToolFailureEvent, buildToolStartEvent, } from './system/runtime/shell-tool-events.js';
|
|
10
11
|
import { checkToolPolicy } from './tools.js';
|
|
@@ -137,6 +138,37 @@ export function decodeAgentOutputVars(rawOutputVars) {
|
|
|
137
138
|
return String(value);
|
|
138
139
|
});
|
|
139
140
|
}
|
|
141
|
+
/** Decode `issue{path, level, message}` dicts from Prolog into `FormIssue[]`. */
|
|
142
|
+
function toFormIssues(raw) {
|
|
143
|
+
const value = toJsValue(raw);
|
|
144
|
+
if (!Array.isArray(value))
|
|
145
|
+
return [];
|
|
146
|
+
return value.map((entry) => {
|
|
147
|
+
const obj = toJsValue(entry);
|
|
148
|
+
return {
|
|
149
|
+
path: String(obj?.path ?? ''),
|
|
150
|
+
level: obj?.level === 'warning' ? 'warning' : 'error',
|
|
151
|
+
message: String(obj?.message ?? ''),
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Does this action run the harness's hard rules? `submit` by default; an action
|
|
157
|
+
* may opt out with `validates: []` (or opt in with `all`/`hard`/a path list).
|
|
158
|
+
*/
|
|
159
|
+
function shouldValidate(spec, action) {
|
|
160
|
+
if (['cancel', 'closed', 'timeout', 'unavailable', 'cancelled', 'invalid'].includes(action))
|
|
161
|
+
return false;
|
|
162
|
+
const declared = spec?.actions?.find((entry) => entry.name === action);
|
|
163
|
+
if (!declared)
|
|
164
|
+
return action === 'submit';
|
|
165
|
+
const validates = declared.validates;
|
|
166
|
+
if (validates === undefined)
|
|
167
|
+
return action === 'submit';
|
|
168
|
+
if (Array.isArray(validates))
|
|
169
|
+
return validates.length > 0;
|
|
170
|
+
return validates === 'all' || validates === 'hard';
|
|
171
|
+
}
|
|
140
172
|
/**
|
|
141
173
|
* User tool info from Prolog
|
|
142
174
|
*/
|
|
@@ -201,6 +233,19 @@ export class DMLRunner {
|
|
|
201
233
|
sessionId = '';
|
|
202
234
|
currentMemory = [];
|
|
203
235
|
stepCounter = 0;
|
|
236
|
+
/**
|
|
237
|
+
* Conversation inbox (docs/DML_CONVERSATION.md §5).
|
|
238
|
+
*
|
|
239
|
+
* A conversation waits on two sources that a harness cannot see at once: the
|
|
240
|
+
* person (the host's input channel) and the session's mailbox (a worker's
|
|
241
|
+
* message). The runner merges them here, so `next/1` is one blocking read and
|
|
242
|
+
* `input/1` is the same read restricted to the person. Items are queued, never
|
|
243
|
+
* dropped: an event that arrives while the person is typing waits its turn.
|
|
244
|
+
*/
|
|
245
|
+
chatInbox = [];
|
|
246
|
+
/** One outstanding host input request at a time, reused across waits. */
|
|
247
|
+
chatInputPending = null;
|
|
248
|
+
chatInboxWake = null;
|
|
204
249
|
judgeCache = new Map();
|
|
205
250
|
/**
|
|
206
251
|
* DML tools currently executing, innermost last.
|
|
@@ -368,6 +413,14 @@ export class DMLRunner {
|
|
|
368
413
|
};
|
|
369
414
|
break;
|
|
370
415
|
}
|
|
416
|
+
case 'html': {
|
|
417
|
+
const payload = (step.payload ?? {});
|
|
418
|
+
yield {
|
|
419
|
+
type: 'html',
|
|
420
|
+
html: { path: typeof payload.path === 'string' ? payload.path : '' },
|
|
421
|
+
};
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
371
424
|
case 'log':
|
|
372
425
|
if (step.content) {
|
|
373
426
|
yield { type: 'log', content: step.content };
|
|
@@ -376,6 +429,21 @@ export class DMLRunner {
|
|
|
376
429
|
case 'answer':
|
|
377
430
|
yield { type: 'answer', content: step.content };
|
|
378
431
|
break;
|
|
432
|
+
case 'suspend': {
|
|
433
|
+
const payload = (step.payload ?? {});
|
|
434
|
+
const continuation = String(toJsValue(payload.continuation) ?? '');
|
|
435
|
+
// The memory arrives as Prolog dicts; serialize it as Prolog text, the
|
|
436
|
+
// form the MI can load back. `String(list)` produced
|
|
437
|
+
// "[object Object],[object Object]", which then failed to load — so a
|
|
438
|
+
// suspended turn used to lose the whole conversation.
|
|
439
|
+
// Bracketed: the record is the *term text* a resume loads back
|
|
440
|
+
// (`repl_set_memory/3`), so it must be a list, not a bare sequence.
|
|
441
|
+
const memory = `[${this.serializeMemoryMessages(this.extractMemoryFromPayload(payload))}]`;
|
|
442
|
+
if (options.onSuspend)
|
|
443
|
+
await options.onSuspend({ continuation, memory });
|
|
444
|
+
yield { type: 'suspend', suspend: { continuation, memory } };
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
379
447
|
case 'solution': {
|
|
380
448
|
const payload = (step.payload ?? {});
|
|
381
449
|
yield {
|
|
@@ -433,6 +501,37 @@ export class DMLRunner {
|
|
|
433
501
|
}
|
|
434
502
|
break;
|
|
435
503
|
}
|
|
504
|
+
case 'request_form': {
|
|
505
|
+
// A form operation. The host owns the registry; the result is posted
|
|
506
|
+
// back so Prolog/MI keeps the encoding (docs/DML_FORMS.md §7).
|
|
507
|
+
const payload = (step.payload ?? {});
|
|
508
|
+
const event = this.handleFormRequest(payload, options.forms);
|
|
509
|
+
if (event) {
|
|
510
|
+
yield event;
|
|
511
|
+
// `update` re-presents too: a rejected submit (or a model editing the
|
|
512
|
+
// form) must reach the person filling it in, with the errors on it.
|
|
513
|
+
if ((event.form?.op === 'open' || event.form?.op === 'update') && options.onForm) {
|
|
514
|
+
await options.onForm(event.form);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
case 'wait_form': {
|
|
520
|
+
const payload = (step.payload ?? {});
|
|
521
|
+
const formId = String(toJsValue(payload.formId) ?? '');
|
|
522
|
+
const since = Number(toJsValue(payload.since) ?? 0);
|
|
523
|
+
const timeoutMs = Number(toJsValue(payload.timeout) ?? 0);
|
|
524
|
+
this.postFormResult(await this.handleFormWait(formId, since, timeoutMs, options.forms, options.signal));
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
case 'wait_next': {
|
|
528
|
+
const accept = String(toJsValue(step.payload?.accept) ?? 'any');
|
|
529
|
+
if (this.armChatInput(options))
|
|
530
|
+
yield { type: 'input_required', prompt: '' };
|
|
531
|
+
const item = await this.waitForChatItem(accept, options);
|
|
532
|
+
this.postNextResult(item.term);
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
436
535
|
case 'request_sample_token':
|
|
437
536
|
yield* this.handleSampleToken(step.payload, options);
|
|
438
537
|
break;
|
|
@@ -715,6 +814,9 @@ export class DMLRunner {
|
|
|
715
814
|
options, _frame) {
|
|
716
815
|
const rawPayload = payload;
|
|
717
816
|
const taskDescription = String(toJsValue(rawPayload.taskDescription) ?? '');
|
|
817
|
+
// `chat` is a conversational turn: tools, no finish, ends when the model
|
|
818
|
+
// stops calling tools (see agent.ts `conversational`).
|
|
819
|
+
const conversational = String(toJsValue(rawPayload.mode) ?? '') === 'chat';
|
|
718
820
|
const stepId = `step_${this.stepCounter++}`;
|
|
719
821
|
yield { type: 'task_activity', taskState: 'started', taskDescription, taskId: stepId };
|
|
720
822
|
let stepSucceeded = null;
|
|
@@ -875,16 +977,40 @@ export class DMLRunner {
|
|
|
875
977
|
toolPolicy: options.toolPolicy,
|
|
876
978
|
onModelCall: options.onModelCall,
|
|
877
979
|
onProcessRequest: options.onProcessRequest,
|
|
980
|
+
forms: options.forms,
|
|
981
|
+
onForm: options.onForm,
|
|
878
982
|
}, withheld, emitToolCall, emitToolEvent);
|
|
879
983
|
}, withheld);
|
|
880
984
|
// Run agent loop with streaming support
|
|
881
985
|
const resultPromise = runAgentLoop({
|
|
882
986
|
taskDescription,
|
|
883
987
|
outputVars,
|
|
988
|
+
conversational,
|
|
989
|
+
// A chat turn does not end while a form it opened is still unanswered: the
|
|
990
|
+
// model may have rendered the form and then replied without waiting (it
|
|
991
|
+
// does not have to know the two-call protocol). Wait for the submission and
|
|
992
|
+
// hand it back as the next turn's input, so nothing is lost.
|
|
993
|
+
onIdleTurn: async () => {
|
|
994
|
+
if (!conversational || !options.forms)
|
|
995
|
+
return null;
|
|
996
|
+
const awaiting = options.forms.awaitingForms();
|
|
997
|
+
if (awaiting.length === 0)
|
|
998
|
+
return null;
|
|
999
|
+
const waits = await Promise.all(awaiting.map((record) => options.forms.wait(record.formId, -1, { signal: options.signal })));
|
|
1000
|
+
return waits
|
|
1001
|
+
.map((change) => {
|
|
1002
|
+
const values = JSON.stringify(change.values ?? {});
|
|
1003
|
+
return change.action === 'submit'
|
|
1004
|
+
? `The person submitted the form ${change.formId}: ${values}`
|
|
1005
|
+
: `The person dismissed the form ${change.formId} (action: ${change.action}).`;
|
|
1006
|
+
})
|
|
1007
|
+
.join('\n');
|
|
1008
|
+
},
|
|
884
1009
|
memory: effectiveMemory,
|
|
885
1010
|
tools: availableTools,
|
|
886
1011
|
workspacePath: options.workspacePath,
|
|
887
1012
|
environment: this.options.environment,
|
|
1013
|
+
capabilityTool: this.options.capabilityTool,
|
|
888
1014
|
resolveMedia: (refs) => this.readMediaRefs(options.workspacePath, refs),
|
|
889
1015
|
modelOptions: {
|
|
890
1016
|
model: scopedModel?.id ?? this.options.model,
|
|
@@ -950,8 +1076,12 @@ export class DMLRunner {
|
|
|
950
1076
|
signal: options.signal,
|
|
951
1077
|
llmBackend: this.options.llmBackend,
|
|
952
1078
|
});
|
|
953
|
-
//
|
|
954
|
-
|
|
1079
|
+
// Drain host events as they arrive. This must run even when not streaming:
|
|
1080
|
+
// a tool can block on a host request — a form wait, an `ask_user` — and that
|
|
1081
|
+
// request must reach the consumer *before* the tool returns, or the run
|
|
1082
|
+
// deadlocks. Non-streaming queues no stream/reasoning chunks, so yielding
|
|
1083
|
+
// everything here is equivalent.
|
|
1084
|
+
{
|
|
955
1085
|
while (true) {
|
|
956
1086
|
// Process any queued events
|
|
957
1087
|
while (streamQueue.length > 0) {
|
|
@@ -959,18 +1089,35 @@ export class DMLRunner {
|
|
|
959
1089
|
}
|
|
960
1090
|
// Check if result is ready
|
|
961
1091
|
const raceResult = await Promise.race([
|
|
962
|
-
resultPromise.then(r => ({ type: 'done', result: r })),
|
|
1092
|
+
resultPromise.then(r => ({ type: 'done', result: r }), (error) => ({ type: 'failed', error })),
|
|
963
1093
|
new Promise(resolve => {
|
|
964
1094
|
streamResolve = () => resolve({ type: 'stream' });
|
|
965
1095
|
}),
|
|
966
1096
|
]);
|
|
1097
|
+
if (raceResult.type === 'failed') {
|
|
1098
|
+
const message = raceResult.error instanceof Error
|
|
1099
|
+
? raceResult.error.message
|
|
1100
|
+
: String(raceResult.error);
|
|
1101
|
+
if (!conversational)
|
|
1102
|
+
throw raceResult.error;
|
|
1103
|
+
// A conversation must survive a failed turn: report it and keep the
|
|
1104
|
+
// memory as it was, so the person can simply type again. Failing the
|
|
1105
|
+
// turn here would end `loop :- input(A), chat(A), loop.`
|
|
1106
|
+
this.postAgentResult({ success: false, outputs: [], variables: {}, messages: [] }, preparedMemory.filter((entry) => entry.role === 'system'), message);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
967
1109
|
if (raceResult.type === 'done') {
|
|
968
1110
|
// Drain remaining queue
|
|
969
1111
|
while (streamQueue.length > 0) {
|
|
970
1112
|
yield streamQueue.shift();
|
|
971
1113
|
}
|
|
972
|
-
// In streaming mode, output was already shown via onStream - don't
|
|
973
|
-
// The outputs array is kept for memory/history purposes
|
|
1114
|
+
// In streaming mode, output was already shown via onStream - don't
|
|
1115
|
+
// repeat it. The outputs array is kept for memory/history purposes.
|
|
1116
|
+
if (!this.options.streaming) {
|
|
1117
|
+
for (const output of raceResult.result.outputs) {
|
|
1118
|
+
yield { type: 'output', content: output };
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
974
1121
|
const finalizedMemory = [
|
|
975
1122
|
...preparedMemory.filter((message) => message.role === 'system'),
|
|
976
1123
|
...raceResult.result.messages,
|
|
@@ -988,39 +1135,6 @@ export class DMLRunner {
|
|
|
988
1135
|
// Otherwise continue loop to yield queued stream events
|
|
989
1136
|
}
|
|
990
1137
|
}
|
|
991
|
-
else {
|
|
992
|
-
// Non-streaming mode - wait for completion
|
|
993
|
-
const result = await resultPromise;
|
|
994
|
-
// Drain tool_call events from queue (they were queued during agent execution)
|
|
995
|
-
if (process.env.DEBUG_QUEUE) {
|
|
996
|
-
console.log('[RUNNER] Non-streaming: streamQueue length before drain:', streamQueue.length);
|
|
997
|
-
if (streamQueue.length > 0) {
|
|
998
|
-
console.log('[RUNNER] streamQueue contents:', streamQueue.map(e => `${e.type}:${e.content?.substring(0, 40)}`));
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
while (streamQueue.length > 0) {
|
|
1002
|
-
yield streamQueue.shift();
|
|
1003
|
-
}
|
|
1004
|
-
if (process.env.DEBUG_QUEUE) {
|
|
1005
|
-
console.log('[RUNNER] Non-streaming agent result.outputs:', result.outputs);
|
|
1006
|
-
}
|
|
1007
|
-
// Stream any output from the agent
|
|
1008
|
-
for (const output of result.outputs) {
|
|
1009
|
-
yield { type: 'output', content: output };
|
|
1010
|
-
}
|
|
1011
|
-
const finalizedMemory = [
|
|
1012
|
-
...preparedMemory.filter((message) => message.role === 'system'),
|
|
1013
|
-
...result.messages,
|
|
1014
|
-
];
|
|
1015
|
-
while (streamQueue.length > 0) {
|
|
1016
|
-
yield streamQueue.shift();
|
|
1017
|
-
}
|
|
1018
|
-
// Update memory with full agent conversation
|
|
1019
|
-
this.currentMemory = finalizedMemory;
|
|
1020
|
-
// Post result back to Prolog
|
|
1021
|
-
stepSucceeded = result.success;
|
|
1022
|
-
this.postAgentResult(result, finalizedMemory);
|
|
1023
|
-
}
|
|
1024
1138
|
}
|
|
1025
1139
|
finally {
|
|
1026
1140
|
yield {
|
|
@@ -1842,6 +1956,10 @@ export class DMLRunner {
|
|
|
1842
1956
|
// Extract memory
|
|
1843
1957
|
const memory = this.extractMemoryFromPayload(payload);
|
|
1844
1958
|
const preparedMemory = memory;
|
|
1959
|
+
const nestedConversational = String(toJsValue(payload?.mode) ?? '') === 'chat';
|
|
1960
|
+
// Nested tasks/prompts carry the same with_model scope as top-level
|
|
1961
|
+
// ones; resolve it through the session resolver.
|
|
1962
|
+
const nestedScopedModel = await this.resolveScope(payload?.modelSpec);
|
|
1845
1963
|
// Extract reasoning effort and recipe context for nested loops
|
|
1846
1964
|
const nestedReasoningEffort = String(toJsValue(payload?.reasoningEffort) ?? 'none');
|
|
1847
1965
|
const nestedRecipeContext = String(toJsValue(payload?.recipeContext) ?? '');
|
|
@@ -1884,18 +2002,21 @@ export class DMLRunner {
|
|
|
1884
2002
|
const result = await runAgentLoop({
|
|
1885
2003
|
taskDescription,
|
|
1886
2004
|
outputVars,
|
|
2005
|
+
conversational: nestedConversational,
|
|
1887
2006
|
memory: nestedEffectiveMemory,
|
|
1888
2007
|
tools: availableTools,
|
|
1889
2008
|
// Withheld to break a call cycle; declared to the model so a
|
|
1890
2009
|
// call can be answered with the reason rather than "not found".
|
|
1891
2010
|
excludedTools: nestedExcludedTools,
|
|
2011
|
+
capabilityTool: this.options.capabilityTool,
|
|
1892
2012
|
modelOptions: {
|
|
1893
|
-
model: this.options.model,
|
|
1894
|
-
provider: this.options.provider,
|
|
1895
|
-
temperature: this.options.temperature,
|
|
1896
|
-
maxOutputTokens: this.options.maxTokens,
|
|
2013
|
+
model: nestedScopedModel?.id ?? this.options.model,
|
|
2014
|
+
provider: nestedScopedModel?.provider ?? this.options.provider,
|
|
2015
|
+
temperature: nestedScopedModel?.temperature ?? this.options.temperature,
|
|
2016
|
+
maxOutputTokens: nestedScopedModel?.maxTokens ?? this.options.maxTokens,
|
|
1897
2017
|
baseUrl: this.options.baseUrl,
|
|
1898
2018
|
providerOptions: nestedProviderOptions,
|
|
2019
|
+
target: nestedScopedModel,
|
|
1899
2020
|
},
|
|
1900
2021
|
streaming: this.options.streaming,
|
|
1901
2022
|
debug: this.options.debug,
|
|
@@ -1988,7 +2109,41 @@ export class DMLRunner {
|
|
|
1988
2109
|
}
|
|
1989
2110
|
break;
|
|
1990
2111
|
}
|
|
2112
|
+
case 'request_form': {
|
|
2113
|
+
// A DML tool reached a form primitive. Resolve it through the same
|
|
2114
|
+
// host registry as the enclosing run (docs/DML_FORMS.md §4).
|
|
2115
|
+
const formPayload = (payload ?? {});
|
|
2116
|
+
const event = this.handleFormRequest(formPayload, runContext.forms);
|
|
2117
|
+
if (event) {
|
|
2118
|
+
onToolEvent?.(event);
|
|
2119
|
+
// The host interaction must run here too: a form opened by a tool
|
|
2120
|
+
// (`render_form`) is the common case, and without this nothing
|
|
2121
|
+
// prompts and the run just blocks on the wait.
|
|
2122
|
+
if ((event.form?.op === 'open' || event.form?.op === 'update') && runContext.onForm) {
|
|
2123
|
+
await runContext.onForm(event.form);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
break;
|
|
2127
|
+
}
|
|
2128
|
+
case 'wait_form': {
|
|
2129
|
+
const formPayload = (payload ?? {});
|
|
2130
|
+
const formId = String(toJsValue(formPayload.formId) ?? '');
|
|
2131
|
+
const since = Number(toJsValue(formPayload.since) ?? 0);
|
|
2132
|
+
const timeoutMs = Number(toJsValue(formPayload.timeout) ?? 0);
|
|
2133
|
+
this.postFormResult(await this.handleFormWait(formId, since, timeoutMs, runContext.forms, runContext.signal));
|
|
2134
|
+
break;
|
|
2135
|
+
}
|
|
2136
|
+
case 'wait_next': {
|
|
2137
|
+
// A conversation turn inside a tool (rare, but the same contract).
|
|
2138
|
+
const accept = String(toJsValue(payload?.accept) ?? 'any');
|
|
2139
|
+
const item = await this.waitForChatItem(accept, runContext);
|
|
2140
|
+
this.postNextResult(item.term);
|
|
2141
|
+
break;
|
|
2142
|
+
}
|
|
1991
2143
|
case 'error': {
|
|
2144
|
+
// A tool-engine error (a thrown body, or a tool that returned no
|
|
2145
|
+
// result). Report it as a failure so it is not mistaken for success.
|
|
2146
|
+
onToolEvent?.(buildToolFailureEvent(toolName, {}, new Error(content)));
|
|
1992
2147
|
return { result: { error: content }, outputs };
|
|
1993
2148
|
}
|
|
1994
2149
|
default: {
|
|
@@ -2074,7 +2229,7 @@ export class DMLRunner {
|
|
|
2074
2229
|
/**
|
|
2075
2230
|
* Post agent loop result back to Prolog
|
|
2076
2231
|
*/
|
|
2077
|
-
postAgentResult(result, fullMemory) {
|
|
2232
|
+
postAgentResult(result, fullMemory, error) {
|
|
2078
2233
|
// Keys need to be quoted because they start with uppercase (Var1, Var2, etc.)
|
|
2079
2234
|
const varsStr = Object.entries(result.variables)
|
|
2080
2235
|
.map(([k, v]) => `'${k}': ${this.toPrologTerm(v)}`)
|
|
@@ -2082,12 +2237,194 @@ export class DMLRunner {
|
|
|
2082
2237
|
// Convert messages to Prolog list format
|
|
2083
2238
|
const messagesStr = this.serializeMemoryMessages(result.messages);
|
|
2084
2239
|
const fullMemoryStr = this.serializeMemoryMessages(fullMemory ?? []);
|
|
2085
|
-
this.query(`deepclause_mi:post_agent_result('${this.sessionId}', ${result.success}, vars{${varsStr}}, [${messagesStr}], [${fullMemoryStr}])`);
|
|
2240
|
+
this.query(`deepclause_mi:post_agent_result('${this.sessionId}', ${result.success}, vars{${varsStr}}, [${messagesStr}], [${fullMemoryStr}], ${this.toPrologTerm(error ?? '')})`);
|
|
2086
2241
|
}
|
|
2087
2242
|
/**
|
|
2088
2243
|
* Post a process primitive result back to Prolog under a named slot.
|
|
2089
2244
|
* `term` is a raw Prolog term string, so the caller controls encoding.
|
|
2090
2245
|
*/
|
|
2246
|
+
/**
|
|
2247
|
+
* Apply a form operation and return the `form` event to emit, if any.
|
|
2248
|
+
* The registry owns state; the result is posted back to Prolog as a term so
|
|
2249
|
+
* the MI keeps the encoding, like `request_proc` (docs/DML_FORMS.md §7).
|
|
2250
|
+
*/
|
|
2251
|
+
handleFormRequest(payload, forms) {
|
|
2252
|
+
if (!forms) {
|
|
2253
|
+
this.postFormResult('error(no_form_host)');
|
|
2254
|
+
return undefined;
|
|
2255
|
+
}
|
|
2256
|
+
const op = String(toJsValue(payload.op) ?? '');
|
|
2257
|
+
if (op === 'open') {
|
|
2258
|
+
// A caller (a harness, or a model that designed the form) may leave out the
|
|
2259
|
+
// boilerplate: `version`, and each field's `type`.
|
|
2260
|
+
const spec = normalizeSpec(toJsValue(payload.spec));
|
|
2261
|
+
const opened = forms.open(spec);
|
|
2262
|
+
this.postFormResult(`ok(${this.toPrologTerm({ formId: opened.formId, revision: opened.revision })})`);
|
|
2263
|
+
return {
|
|
2264
|
+
type: 'form',
|
|
2265
|
+
form: { op: 'open', formId: opened.formId, revision: opened.revision, spec, values: opened.values, issues: [] },
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
const formId = String(toJsValue(payload.formId) ?? '');
|
|
2269
|
+
if (op === 'update') {
|
|
2270
|
+
const patch = (toJsValue(payload.patch) ?? {});
|
|
2271
|
+
forms.update(formId, patch);
|
|
2272
|
+
this.postFormResult('ok');
|
|
2273
|
+
const record = forms.get(formId);
|
|
2274
|
+
// `spec` travels with every form event: a renderer that only gets an update
|
|
2275
|
+
// still needs the fields to draw (and to show the issues against them).
|
|
2276
|
+
return record
|
|
2277
|
+
? { type: 'form', form: { op: 'update', formId, revision: record.revision, spec: record.spec, patch, values: record.values, issues: record.issues } }
|
|
2278
|
+
: undefined;
|
|
2279
|
+
}
|
|
2280
|
+
if (op === 'errors') {
|
|
2281
|
+
const issues = toFormIssues(payload.issues);
|
|
2282
|
+
forms.setIssues(formId, issues);
|
|
2283
|
+
this.postFormResult('ok');
|
|
2284
|
+
const record = forms.get(formId);
|
|
2285
|
+
return record
|
|
2286
|
+
? { type: 'form', form: { op: 'update', formId, revision: record.revision, spec: record.spec, values: record.values, issues } }
|
|
2287
|
+
: undefined;
|
|
2288
|
+
}
|
|
2289
|
+
if (op === 'close') {
|
|
2290
|
+
forms.close(formId, 'submitted');
|
|
2291
|
+
this.postFormResult('ok');
|
|
2292
|
+
const record = forms.get(formId);
|
|
2293
|
+
return { type: 'form', form: { op: 'close', formId, revision: record?.revision ?? 0, state: 'submitted' } };
|
|
2294
|
+
}
|
|
2295
|
+
this.postFormResult(`error(unknown_form_op(${op}))`);
|
|
2296
|
+
return undefined;
|
|
2297
|
+
}
|
|
2298
|
+
/** Await a form change and return the result term to post to Prolog. */
|
|
2299
|
+
/**
|
|
2300
|
+
* Wait for a UI change, enforcing the rules the host owns.
|
|
2301
|
+
*
|
|
2302
|
+
* A submit the rules reject comes back as `invalid` with the errors. The DML
|
|
2303
|
+
* side (see `mi_call(await_form(...))`) turns that into "show the errors, wait
|
|
2304
|
+
* again", so neither a harness nor a model ever acts on a refused submit — and
|
|
2305
|
+
* neither has to write the re-show loop.
|
|
2306
|
+
*/
|
|
2307
|
+
async handleFormWait(formId, since, timeoutMs, forms, signal) {
|
|
2308
|
+
if (!forms)
|
|
2309
|
+
return 'error(no_form_host)';
|
|
2310
|
+
try {
|
|
2311
|
+
const change = await forms.wait(formId, since, {
|
|
2312
|
+
timeoutMs: timeoutMs > 0 ? timeoutMs : undefined,
|
|
2313
|
+
signal,
|
|
2314
|
+
});
|
|
2315
|
+
// Layer 1: the spec's own constraints (required, enum, min/max, pattern).
|
|
2316
|
+
// Layer 2: the harness's `form_validate/2` domain rules. Both block.
|
|
2317
|
+
const record = forms.get(formId);
|
|
2318
|
+
if (record && shouldValidate(record.spec, change.action)) {
|
|
2319
|
+
const errors = [
|
|
2320
|
+
...validateValues(record.spec, change.values),
|
|
2321
|
+
...this.validateFormValues(change.values),
|
|
2322
|
+
];
|
|
2323
|
+
if (errors.length > 0) {
|
|
2324
|
+
return `ok(${this.toPrologTerm({ ...change, action: 'invalid', errors })})`;
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
return `ok(${this.toPrologTerm(change)})`;
|
|
2328
|
+
}
|
|
2329
|
+
catch (error) {
|
|
2330
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2331
|
+
return `error(${JSON.stringify(message)})`;
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
/** Run the harness's `form_validate/2` and return the issue dicts it produced. */
|
|
2335
|
+
validateFormValues(values) {
|
|
2336
|
+
const result = this.query(`deepclause_mi:form_validate_values('${this.sessionId}', ${this.toPrologTerm(values)}, Issues)`);
|
|
2337
|
+
return toFormIssues(result?.Issues);
|
|
2338
|
+
}
|
|
2339
|
+
// ---- Conversation inbox (docs/DML_CONVERSATION.md §5) ------------------
|
|
2340
|
+
/** Start the host's input request, unless one is already outstanding. */
|
|
2341
|
+
armChatInput(options) {
|
|
2342
|
+
if (this.chatInputPending)
|
|
2343
|
+
return false;
|
|
2344
|
+
if (!options.onInputRequired)
|
|
2345
|
+
return false;
|
|
2346
|
+
this.chatInputPending = options
|
|
2347
|
+
.onInputRequired('')
|
|
2348
|
+
.then((text) => {
|
|
2349
|
+
this.chatInputPending = null;
|
|
2350
|
+
// No answer means nobody is there: close the inbox, which ends a
|
|
2351
|
+
// conversation loop instead of spinning on empty turns.
|
|
2352
|
+
this.pushChatItem(text === undefined || text === null
|
|
2353
|
+
? { kind: 'closed', term: 'closed' }
|
|
2354
|
+
: { kind: 'user', term: `user(${this.toPrologTerm(text)})` });
|
|
2355
|
+
})
|
|
2356
|
+
.catch(() => {
|
|
2357
|
+
this.chatInputPending = null;
|
|
2358
|
+
this.pushChatItem({ kind: 'closed', term: 'closed' });
|
|
2359
|
+
});
|
|
2360
|
+
return true;
|
|
2361
|
+
}
|
|
2362
|
+
pushChatItem(item) {
|
|
2363
|
+
this.chatInbox.push(item);
|
|
2364
|
+
const wake = this.chatInboxWake;
|
|
2365
|
+
this.chatInboxWake = null;
|
|
2366
|
+
wake?.();
|
|
2367
|
+
}
|
|
2368
|
+
/**
|
|
2369
|
+
* The next item the caller accepts; `user` skips (keeps) worker messages.
|
|
2370
|
+
* A closed inbox is the last resort: a message that arrived is worth more than
|
|
2371
|
+
* "nobody is typing", so it is handed out only when nothing else is queued.
|
|
2372
|
+
*/
|
|
2373
|
+
takeChatItem(accept) {
|
|
2374
|
+
const open = this.chatInbox.findIndex((item) => item.kind !== 'closed' && (accept === 'any' || item.kind === 'user'));
|
|
2375
|
+
if (open >= 0)
|
|
2376
|
+
return this.chatInbox.splice(open, 1)[0];
|
|
2377
|
+
const closed = this.chatInbox.findIndex((item) => item.kind === 'closed');
|
|
2378
|
+
return closed >= 0 ? this.chatInbox.splice(closed, 1)[0] : undefined;
|
|
2379
|
+
}
|
|
2380
|
+
/**
|
|
2381
|
+
* Wait for the next inbox item: the person's message, or one a worker sent this
|
|
2382
|
+
* session. The mailbox is polled with a short timeout rather than held open, so
|
|
2383
|
+
* a run that ends mid-wait leaves no receiver behind.
|
|
2384
|
+
*/
|
|
2385
|
+
async waitForChatItem(accept, options) {
|
|
2386
|
+
for (;;) {
|
|
2387
|
+
const ready = this.takeChatItem(accept);
|
|
2388
|
+
if (ready && ready.kind !== 'closed')
|
|
2389
|
+
return ready;
|
|
2390
|
+
if (ready?.kind === 'closed') {
|
|
2391
|
+
// The person is gone, but a worker's message may still be in flight: give
|
|
2392
|
+
// the mailbox one last chance so closing the input does not swallow it.
|
|
2393
|
+
await this.pollChatMessage(options);
|
|
2394
|
+
return this.takeChatItem(accept) ?? ready;
|
|
2395
|
+
}
|
|
2396
|
+
const wake = new Promise((resolve) => {
|
|
2397
|
+
this.chatInboxWake = resolve;
|
|
2398
|
+
});
|
|
2399
|
+
await Promise.race([wake, this.pollChatMessage(options)]);
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
/** One mailbox poll, so the inbox sees worker messages as they arrive. */
|
|
2403
|
+
async pollChatMessage(options) {
|
|
2404
|
+
if (!options.onProcessRequest) {
|
|
2405
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2408
|
+
try {
|
|
2409
|
+
const result = await options.onProcessRequest({ op: 'receive', timeout: 200 }, options.signal);
|
|
2410
|
+
if (result.term.startsWith('ok(')) {
|
|
2411
|
+
// `ok(<term>)` — the message a worker sent to this session.
|
|
2412
|
+
this.pushChatItem({ kind: 'message', term: `message(${result.term.slice(3, -1)})` });
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
catch {
|
|
2416
|
+
// No process host in this run: the person is the only source.
|
|
2417
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
/** Post the conversation inbox item back to Prolog under its slot. */
|
|
2421
|
+
postNextResult(term) {
|
|
2422
|
+
this.query(`deepclause_mi:post_next_result('${this.sessionId}', ${term})`);
|
|
2423
|
+
}
|
|
2424
|
+
/** Post a form primitive result back to Prolog under the form slot. */
|
|
2425
|
+
postFormResult(term) {
|
|
2426
|
+
this.query(`deepclause_mi:post_form_result('${this.sessionId}', ${term})`);
|
|
2427
|
+
}
|
|
2091
2428
|
postProcessResult(key, term) {
|
|
2092
2429
|
const atom = /^[a-z][A-Za-z0-9_]*$/.test(key) ? key : `'${key.replace(/'/g, "''")}'`;
|
|
2093
2430
|
this.query(`deepclause_mi:post_proc_result('${this.sessionId}', ${atom}, ${term})`);
|