@duckcodeailabs/dql-agent 1.14.3-rc.1 → 1.14.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-run-engine.d.ts +177 -3
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +2067 -57
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/agent-run-gates.d.ts.map +1 -1
- package/dist/agent-run-gates.js +51 -3
- package/dist/agent-run-gates.js.map +1 -1
- package/dist/agent-run-store.d.ts.map +1 -1
- package/dist/agent-run-store.js +544 -9
- package/dist/agent-run-store.js.map +1 -1
- package/dist/agentic/analyst-loop.d.ts +2 -0
- package/dist/agentic/analyst-loop.d.ts.map +1 -1
- package/dist/agentic/analyst-loop.js +41 -4
- package/dist/agentic/analyst-loop.js.map +1 -1
- package/dist/agentic/ledger-tools.d.ts.map +1 -1
- package/dist/agentic/ledger-tools.js +8 -1
- package/dist/agentic/ledger-tools.js.map +1 -1
- package/dist/agentic/orchestrator-policy.d.ts +3 -1
- package/dist/agentic/orchestrator-policy.d.ts.map +1 -1
- package/dist/agentic/orchestrator-policy.js +1 -1
- package/dist/agentic/orchestrator-policy.js.map +1 -1
- package/dist/agentic/tool-loop.d.ts +13 -1
- package/dist/agentic/tool-loop.d.ts.map +1 -1
- package/dist/agentic/tool-loop.js +441 -32
- package/dist/agentic/tool-loop.js.map +1 -1
- package/dist/analytical-frame.d.ts +19 -0
- package/dist/analytical-frame.d.ts.map +1 -1
- package/dist/analytical-frame.js +170 -12
- package/dist/analytical-frame.js.map +1 -1
- package/dist/analytical-orchestration.d.ts +949 -10
- package/dist/analytical-orchestration.d.ts.map +1 -1
- package/dist/analytical-orchestration.js +499 -56
- package/dist/analytical-orchestration.js.map +1 -1
- package/dist/answer-loop.d.ts +166 -2
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +654 -18
- package/dist/answer-loop.js.map +1 -1
- package/dist/answer-shape.d.ts +27 -0
- package/dist/answer-shape.d.ts.map +1 -1
- package/dist/answer-shape.js +55 -0
- package/dist/answer-shape.js.map +1 -1
- package/dist/ask-observability/instrumentation.d.ts.map +1 -1
- package/dist/ask-observability/instrumentation.js +52 -1
- package/dist/ask-observability/instrumentation.js.map +1 -1
- package/dist/ask-observability/portable.js +562 -0
- package/dist/ask-observability/portable.js.map +1 -1
- package/dist/ask-observability/types.d.ts +26 -3
- package/dist/ask-observability/types.d.ts.map +1 -1
- package/dist/ask-observability/types.js.map +1 -1
- package/dist/ask-runtime/ask-agent-runtime-v2.d.ts +850 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.d.ts.map +1 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.js +1716 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.js.map +1 -0
- package/dist/ask-runtime/ask-analyst-runtime.d.ts +95 -0
- package/dist/ask-runtime/ask-analyst-runtime.d.ts.map +1 -0
- package/dist/ask-runtime/ask-analyst-runtime.js +8280 -0
- package/dist/ask-runtime/ask-analyst-runtime.js.map +1 -0
- package/dist/conversation/session-store.d.ts +31 -0
- package/dist/conversation/session-store.d.ts.map +1 -1
- package/dist/conversation/session-store.js +38 -0
- package/dist/conversation/session-store.js.map +1 -1
- package/dist/conversation/snapshot.d.ts +4 -1
- package/dist/conversation/snapshot.d.ts.map +1 -1
- package/dist/conversation/snapshot.js +85 -3
- package/dist/conversation/snapshot.js.map +1 -1
- package/dist/conversation/turn-trust.d.ts.map +1 -1
- package/dist/conversation/turn-trust.js +2 -0
- package/dist/conversation/turn-trust.js.map +1 -1
- package/dist/embeddings/provider.d.ts +1 -0
- package/dist/embeddings/provider.d.ts.map +1 -1
- package/dist/embeddings/provider.js +18 -0
- package/dist/embeddings/provider.js.map +1 -1
- package/dist/generated-analytical-proposal.js +37 -2
- package/dist/generated-analytical-proposal.js.map +1 -1
- package/dist/index.d.ts +15 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +77 -1
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +1 -1
- package/dist/intent-controller.js.map +1 -1
- package/dist/kg/build.js +32 -2
- package/dist/kg/build.js.map +1 -1
- package/dist/meaning-resolution.d.ts +129 -0
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +108 -15
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.js +57 -6
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/catalog.d.ts +14 -0
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +28 -1
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts +19 -0
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +113 -6
- package/dist/metadata/meaning-evidence.js.map +1 -1
- package/dist/provider-egress.d.ts +4 -1
- package/dist/provider-egress.d.ts.map +1 -1
- package/dist/provider-egress.js +50 -5
- package/dist/provider-egress.js.map +1 -1
- package/dist/providers/claude.d.ts +2 -2
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +191 -28
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.d.ts +2 -2
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +191 -21
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/types.d.ts +79 -1
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/requirement-clauses.d.ts +23 -0
- package/dist/requirement-clauses.d.ts.map +1 -0
- package/dist/requirement-clauses.js +70 -0
- package/dist/requirement-clauses.js.map +1 -0
- package/dist/resolved-analytical-plan.d.ts.map +1 -1
- package/dist/resolved-analytical-plan.js +201 -15
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +109 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +753 -76
- package/dist/router.js.map +1 -1
- package/package.json +4 -4
|
@@ -39,9 +39,11 @@ export async function runAgenticToolLoopDetailed(provider, messages, tools, opti
|
|
|
39
39
|
const usable = tools
|
|
40
40
|
.filter((tool) => tool.name && tool.description)
|
|
41
41
|
.map((tool) => guardToolOutput(tool, options, resultRowBudgetUsage));
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
: []
|
|
42
|
+
const initialToolPolicy = renderCurrentToolPolicy(options, usable);
|
|
43
|
+
const policyMessages = [
|
|
44
|
+
...(options.toolPolicy ? [{ role: 'system', content: options.toolPolicy }] : []),
|
|
45
|
+
...(initialToolPolicy ? [{ role: 'system', content: initialToolPolicy }] : []),
|
|
46
|
+
];
|
|
45
47
|
if (usable.length === 0) {
|
|
46
48
|
try {
|
|
47
49
|
return { text: await provider.generate([...messages, ...policyMessages], options), stop: 'final', toolCalls: 0 };
|
|
@@ -56,8 +58,16 @@ export async function runAgenticToolLoopDetailed(provider, messages, tools, opti
|
|
|
56
58
|
// Native tool use owns its own loop; hand it the same policy + tools.
|
|
57
59
|
if (provider.generateWithTools) {
|
|
58
60
|
try {
|
|
61
|
+
const native = await provider.generateWithTools([...messages, ...policyMessages], usable, options);
|
|
62
|
+
if (isNativeToolLoopStop(native)) {
|
|
63
|
+
return {
|
|
64
|
+
text: native.text,
|
|
65
|
+
stop: native.kind,
|
|
66
|
+
toolCalls: native.toolCalls,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
59
69
|
return {
|
|
60
|
-
text:
|
|
70
|
+
text: native,
|
|
61
71
|
stop: 'final',
|
|
62
72
|
toolCalls: 0,
|
|
63
73
|
};
|
|
@@ -71,13 +81,80 @@ export async function runAgenticToolLoopDetailed(provider, messages, tools, opti
|
|
|
71
81
|
}
|
|
72
82
|
return runTextProtocolToolLoopDetailed(provider, [...messages, ...policyMessages], usable, options);
|
|
73
83
|
}
|
|
84
|
+
function isNativeToolLoopStop(value) {
|
|
85
|
+
return Boolean(value
|
|
86
|
+
&& typeof value === 'object'
|
|
87
|
+
&& !Array.isArray(value)
|
|
88
|
+
&& value.version === 1
|
|
89
|
+
&& (value.kind === 'tool_budget_exhausted'
|
|
90
|
+
|| value.kind === 'provider_dispatch_budget_exhausted'
|
|
91
|
+
|| value.kind === 'invalid_tool_response'
|
|
92
|
+
|| value.kind === 'run_soft_target_exceeded'
|
|
93
|
+
|| value.kind === 'run_deadline_insufficient')
|
|
94
|
+
&& typeof value.text === 'string'
|
|
95
|
+
&& typeof value.toolCalls === 'number');
|
|
96
|
+
}
|
|
97
|
+
function currentToolPolicy(options, tools) {
|
|
98
|
+
const policy = options.getCurrentToolPolicy?.();
|
|
99
|
+
const available = new Set(tools.map((tool) => tool.name));
|
|
100
|
+
const allowedToolNames = new Set((policy?.allowedToolNames ?? tools.map((tool) => tool.name))
|
|
101
|
+
.filter((name) => available.has(name)));
|
|
102
|
+
const terminalActionToolNames = new Set((policy?.terminalActionToolNames ?? [])
|
|
103
|
+
.filter((name) => allowedToolNames.has(name)));
|
|
104
|
+
return { policy, allowedToolNames, terminalActionToolNames };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Keep the text protocol honest about the same narrowing that native
|
|
108
|
+
* transports receive in their next API `tools` declaration. This is a safe
|
|
109
|
+
* controller instruction, never hidden reasoning or mutable business context.
|
|
110
|
+
*/
|
|
111
|
+
function renderCurrentToolPolicy(options, tools) {
|
|
112
|
+
const { policy, allowedToolNames, terminalActionToolNames } = currentToolPolicy(options, tools);
|
|
113
|
+
if (!policy)
|
|
114
|
+
return undefined;
|
|
115
|
+
const allowed = [...allowedToolNames];
|
|
116
|
+
const terminal = [...terminalActionToolNames];
|
|
117
|
+
const instruction = policy.instruction?.trim();
|
|
118
|
+
// Re-state the SIGNATURES of the tools still on the table, not just their
|
|
119
|
+
// names. The response contract is sent once, at the start, listing every
|
|
120
|
+
// tool; a model reading it later has no way to tell that the host has since
|
|
121
|
+
// narrowed the set, so it keeps proposing a tool that can only be refused —
|
|
122
|
+
// and each refusal costs a dispatch until the turn dies with nothing run.
|
|
123
|
+
// Naming the remaining options in full makes the next legal move the
|
|
124
|
+
// easiest one to make.
|
|
125
|
+
const allowedSignatures = allowed.length && allowed.length < tools.length
|
|
126
|
+
? tools
|
|
127
|
+
.filter((tool) => allowedToolNames.has(tool.name))
|
|
128
|
+
.map((tool) => {
|
|
129
|
+
const props = tool.inputSchema?.properties;
|
|
130
|
+
return `- ${tool.name}(${props ? Object.keys(props).join(', ') : ''}): ${tool.description}`;
|
|
131
|
+
})
|
|
132
|
+
: [];
|
|
133
|
+
return [
|
|
134
|
+
`Runtime tool availability update. You may call only: ${allowed.length ? allowed.join(', ') : 'no tools'}.`,
|
|
135
|
+
terminal.length ? `If this is the final controller turn, use only: ${terminal.join(', ')}.` : undefined,
|
|
136
|
+
instruction,
|
|
137
|
+
allowedSignatures.length ? `\nStill available to you:\n${allowedSignatures.join('\n')}` : undefined,
|
|
138
|
+
].filter((part) => Boolean(part)).join(' ');
|
|
139
|
+
}
|
|
74
140
|
function providerDispatchTerminal(error) {
|
|
141
|
+
const code = toolLoopErrorCode(error);
|
|
142
|
+
if (code === 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED') {
|
|
143
|
+
return { text: '', stop: 'provider_dispatch_budget_exhausted', toolCalls: 0 };
|
|
144
|
+
}
|
|
145
|
+
if (code === 'RUN_SOFT_TARGET_EXCEEDED') {
|
|
146
|
+
return { text: '', stop: 'run_soft_target_exceeded', toolCalls: 0 };
|
|
147
|
+
}
|
|
148
|
+
if (code === 'RUN_DEADLINE_INSUFFICIENT') {
|
|
149
|
+
return { text: '', stop: 'run_deadline_insufficient', toolCalls: 0 };
|
|
150
|
+
}
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
function toolLoopErrorCode(error) {
|
|
75
154
|
const code = error && typeof error === 'object' && 'code' in error
|
|
76
155
|
? error.code
|
|
77
156
|
: undefined;
|
|
78
|
-
return code === '
|
|
79
|
-
? { text: '', stop: 'provider_dispatch_budget_exhausted', toolCalls: 0 }
|
|
80
|
-
: undefined;
|
|
157
|
+
return typeof code === 'string' ? code : undefined;
|
|
81
158
|
}
|
|
82
159
|
function guardToolOutput(tool, options, resultRowBudgetUsage) {
|
|
83
160
|
const policy = options.providerPayloadGuard;
|
|
@@ -91,10 +168,12 @@ function guardToolOutput(tool, options, resultRowBudgetUsage) {
|
|
|
91
168
|
const budgetGroup = policy.resultRowBudgetGroupByTool?.[tool.name] ?? tool.name;
|
|
92
169
|
const cumulativeLimit = policy.cumulativeResultRowBudgets?.[budgetGroup] ?? maxResultRows;
|
|
93
170
|
const alreadyUsed = resultRowBudgetUsage.get(budgetGroup) ?? 0;
|
|
94
|
-
const bounded = boundProviderResultRows(output, Math.max(0, cumulativeLimit - alreadyUsed));
|
|
171
|
+
const bounded = boundProviderResultRows(output, Math.max(0, cumulativeLimit - alreadyUsed), policy.maxResultColumns, policy.maxResultCells);
|
|
95
172
|
const shape = assertProviderPayloadAllowed(bounded.value, {
|
|
96
173
|
allowResultRows: maxResultRows > 0,
|
|
97
174
|
maxResultRows: Math.max(0, cumulativeLimit - alreadyUsed),
|
|
175
|
+
...(typeof policy.maxResultColumns === 'number' ? { maxResultColumns: policy.maxResultColumns } : {}),
|
|
176
|
+
...(typeof policy.maxResultCells === 'number' ? { maxResultCells: policy.maxResultCells } : {}),
|
|
98
177
|
purpose: policy.purpose,
|
|
99
178
|
});
|
|
100
179
|
const cumulativeResultRowCount = alreadyUsed + shape.resultRowCount;
|
|
@@ -131,6 +210,13 @@ export async function runTextProtocolToolLoopDetailed(provider, baseMessages, to
|
|
|
131
210
|
// composition instead of silently spending it on another observation.
|
|
132
211
|
const dispatchLimit = Math.max(1, Math.min(30, options.maxProviderDispatches ?? (maxToolCalls + 1)));
|
|
133
212
|
const effectiveToolBudget = Math.min(maxToolCalls, Math.max(0, dispatchLimit - 1));
|
|
213
|
+
// `onProviderDispatch` is an optional transport callback. Subscription and
|
|
214
|
+
// test providers can legitimately be callback-silent, so it cannot be the
|
|
215
|
+
// source of truth for whether this is the first model turn or a tool
|
|
216
|
+
// follow-up. Keep a local invocation count for the text protocol itself;
|
|
217
|
+
// the server-side wrapper remains the authority for physical admission and
|
|
218
|
+
// the hard send cap.
|
|
219
|
+
let providerTurns = 0;
|
|
134
220
|
let physicalDispatches = 0;
|
|
135
221
|
const outerObserver = options.onProviderDispatch;
|
|
136
222
|
const runOptions = {
|
|
@@ -147,39 +233,143 @@ export async function runTextProtocolToolLoopDetailed(provider, baseMessages, to
|
|
|
147
233
|
...baseMessages,
|
|
148
234
|
// Tell the model the *effective* ceiling, not a larger policy cap that
|
|
149
235
|
// cannot physically leave room for its final response.
|
|
150
|
-
{ role: 'system', content: buildTextToolContract(tools, effectiveToolBudget) },
|
|
236
|
+
{ role: 'system', content: (options.textToolContract ?? buildTextToolContract)(tools, effectiveToolBudget) },
|
|
151
237
|
];
|
|
238
|
+
const initialPolicy = renderCurrentToolPolicy(options, tools);
|
|
239
|
+
if (initialPolicy)
|
|
240
|
+
messages.push({ role: 'system', content: initialPolicy });
|
|
152
241
|
let lastText = '';
|
|
153
242
|
let toolCalls = 0;
|
|
243
|
+
let requiredActionSignature = '';
|
|
244
|
+
let requiredActionProseRetries = 0;
|
|
245
|
+
// Only canonical V2 lanes with a real host finish control need to reserve a
|
|
246
|
+
// physical send after execution. Generic tool users retain their historical
|
|
247
|
+
// final-action behavior at the cap.
|
|
248
|
+
const requiresPostExecutionFinish = Boolean(options.getCurrentToolPolicy)
|
|
249
|
+
&& tools.some((tool) => tool.name === 'finish_answer');
|
|
154
250
|
while (true) {
|
|
251
|
+
// Keep the physical Ask V2 budget meaningful. With the standard six
|
|
252
|
+
// sends, the fifth send may be the controller-selected execution action
|
|
253
|
+
// and the sixth is reserved for the host-required finish/narration
|
|
254
|
+
// control. This is a transport constraint only: the kernel still gives
|
|
255
|
+
// the model the candidate-bound execution choices.
|
|
256
|
+
const livePolicyBeforeDispatch = currentToolPolicy(options, tools);
|
|
257
|
+
const nextRequiredActionSignature = [...livePolicyBeforeDispatch.terminalActionToolNames].sort().join('|');
|
|
258
|
+
if (nextRequiredActionSignature !== requiredActionSignature) {
|
|
259
|
+
requiredActionSignature = nextRequiredActionSignature;
|
|
260
|
+
requiredActionProseRetries = 0;
|
|
261
|
+
}
|
|
262
|
+
const narrationControlRound = livePolicyBeforeDispatch.terminalActionToolNames.has('finish_answer');
|
|
263
|
+
// Only an execution action needs a second, post-result finish/narration
|
|
264
|
+
// send. A host-issued clarification is itself the terminal control, so
|
|
265
|
+
// reserving a phantom narration slot would prematurely reject a malformed
|
|
266
|
+
// clarification instead of returning its typed observation to the model.
|
|
267
|
+
const terminalExecutionAction = [...livePolicyBeforeDispatch.terminalActionToolNames]
|
|
268
|
+
.some((name) => !isAskV2TerminalControlTool(name));
|
|
269
|
+
const reservePostExecutionNarration = requiresPostExecutionFinish && terminalExecutionAction;
|
|
270
|
+
const finalExecutionActionRound = !narrationControlRound
|
|
271
|
+
&& livePolicyBeforeDispatch.terminalActionToolNames.size > 0
|
|
272
|
+
&& providerTurns >= Math.max(0, dispatchLimit - (reservePostExecutionNarration ? 2 : 1));
|
|
273
|
+
const terminalActionRound = livePolicyBeforeDispatch.terminalActionToolNames.size > 0
|
|
274
|
+
&& (narrationControlRound
|
|
275
|
+
? providerTurns >= Math.max(0, dispatchLimit - 1)
|
|
276
|
+
: finalExecutionActionRound);
|
|
277
|
+
if (terminalActionRound) {
|
|
278
|
+
messages.push({
|
|
279
|
+
role: 'system',
|
|
280
|
+
content: `Final controller action for this phase. Call exactly one of: ${[...livePolicyBeforeDispatch.terminalActionToolNames].join(', ')}. Do not inspect more context or answer in prose.`,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
155
283
|
let text;
|
|
156
284
|
try {
|
|
157
|
-
|
|
285
|
+
// Authoritative Ask V2 labels its first model-controlled transport
|
|
286
|
+
// separately from later tool-follow-up transports. This is only a
|
|
287
|
+
// server-owned accounting detail: it does not grant a different tool,
|
|
288
|
+
// route, or egress policy. Keeping it here makes text-only providers
|
|
289
|
+
// truthful in the same way native multi-tool providers are.
|
|
290
|
+
// Once an execution has completed, the only remaining controller action
|
|
291
|
+
// is host-local finish_answer. Account the request for that action from
|
|
292
|
+
// the narration allowance instead of treating it as more discovery.
|
|
293
|
+
const dispatchOptions = providerTurns > 0 && runOptions.dispatchPhase === 'agent_control'
|
|
294
|
+
? {
|
|
295
|
+
...runOptions,
|
|
296
|
+
dispatchPhase: narrationControlRound
|
|
297
|
+
? 'narration'
|
|
298
|
+
: 'tool_followup',
|
|
299
|
+
}
|
|
300
|
+
: runOptions;
|
|
301
|
+
providerTurns += 1;
|
|
302
|
+
text = await provider.generate(messages, dispatchOptions);
|
|
158
303
|
}
|
|
159
304
|
catch (error) {
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
:
|
|
163
|
-
if (code === 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED') {
|
|
164
|
-
return { text: lastText, stop: 'provider_dispatch_budget_exhausted', toolCalls };
|
|
165
|
-
}
|
|
305
|
+
const terminal = providerDispatchTerminal(error);
|
|
306
|
+
if (terminal)
|
|
307
|
+
return { ...terminal, text: lastText, toolCalls };
|
|
166
308
|
throw error;
|
|
167
309
|
}
|
|
168
|
-
if (text.trim())
|
|
169
|
-
lastText = text;
|
|
170
310
|
const requestedCall = parseTextToolCall(text);
|
|
171
|
-
if (!requestedCall)
|
|
311
|
+
if (!requestedCall) {
|
|
312
|
+
const policy = livePolicyBeforeDispatch;
|
|
313
|
+
// A live V2 policy can require one concrete next action. A prose reply
|
|
314
|
+
// at this point is neither a valid answer nor a safe terminal: discard
|
|
315
|
+
// it and spend the next admissible controller send on the host-approved
|
|
316
|
+
// action only. This prevents a model from escaping the semantic/DQL/SQL
|
|
317
|
+
// boundary just by answering in prose after an inspection.
|
|
318
|
+
if (policy.terminalActionToolNames.size > 0) {
|
|
319
|
+
// One constrained retry is enough to distinguish a transient
|
|
320
|
+
// text-protocol miss from a provider that cannot honor a required
|
|
321
|
+
// host action. Do not burn the remaining Ask budget on repeated
|
|
322
|
+
// prose, and do not mislabel that transport fault as missing context.
|
|
323
|
+
// If the admitted physical send was already the last one, there is
|
|
324
|
+
// no constrained retry to attempt. Preserve the distinct transport
|
|
325
|
+
// boundary: a post-result narration can then retain deterministic
|
|
326
|
+
// facts, while a pre-freeze controller is told precisely that its
|
|
327
|
+
// dispatch reserve is exhausted. `invalid_tool_response` means the
|
|
328
|
+
// provider ignored the same narrowed action *twice*.
|
|
329
|
+
if (providerTurns >= dispatchLimit) {
|
|
330
|
+
return { text: '', stop: 'provider_dispatch_budget_exhausted', toolCalls };
|
|
331
|
+
}
|
|
332
|
+
if (requiredActionProseRetries >= 1) {
|
|
333
|
+
return { text: '', stop: 'invalid_tool_response', toolCalls };
|
|
334
|
+
}
|
|
335
|
+
requiredActionProseRetries += 1;
|
|
336
|
+
messages.push({
|
|
337
|
+
role: 'user',
|
|
338
|
+
content: `Controller progression required. Discard the prior prose and call exactly one of: ${[...policy.terminalActionToolNames].join(', ')}. Do not answer in prose.`,
|
|
339
|
+
});
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (text.trim())
|
|
343
|
+
lastText = text;
|
|
172
344
|
return { text: text || lastText, stop: 'final', toolCalls };
|
|
345
|
+
}
|
|
346
|
+
if (text.trim())
|
|
347
|
+
lastText = text;
|
|
173
348
|
// A tool-shaped reply is not a final answer merely because the host has no
|
|
174
349
|
// dispatch left for another observation. Keep this typed distinction so a
|
|
175
350
|
// caller cannot mistake it for executable SQL or prose.
|
|
176
|
-
|
|
351
|
+
// Ask V2 has two host-owned terminal controls. They may use the reserved
|
|
352
|
+
// final dispatch only when the tool backend confirms the terminal result.
|
|
353
|
+
// A model can still propose either control too early; that rejected
|
|
354
|
+
// proposal is a normal pre-freeze observation which must reach the next
|
|
355
|
+
// controller turn rather than ending the loop as if an answer existed.
|
|
356
|
+
const isTerminalControlCall = isAskV2TerminalControlTool(requestedCall.name);
|
|
357
|
+
const responsePolicy = livePolicyBeforeDispatch;
|
|
358
|
+
if (terminalActionRound && !responsePolicy.terminalActionToolNames.has(requestedCall.name)) {
|
|
359
|
+
// The narrowed final-action send is not another discovery opportunity.
|
|
360
|
+
// Do not execute an out-of-policy request or spend the narration reserve
|
|
361
|
+
// trying to repair it. The lane projects this exact stop as
|
|
362
|
+
// provider/dispatch_budget rather than a metadata gap.
|
|
363
|
+
return { text: '', stop: 'provider_dispatch_budget_exhausted', toolCalls };
|
|
364
|
+
}
|
|
365
|
+
if (toolCalls >= effectiveToolBudget && !isTerminalControlCall) {
|
|
177
366
|
return { text: text || lastText, stop: 'tool_budget_exhausted', toolCalls };
|
|
178
367
|
}
|
|
179
368
|
const call = requestedCall;
|
|
180
369
|
const tool = toolMap.get(call.name);
|
|
181
370
|
let output;
|
|
182
371
|
let isError = false;
|
|
372
|
+
let deadlineStop;
|
|
183
373
|
const startedAt = Date.now();
|
|
184
374
|
if (!tool) {
|
|
185
375
|
output = { error: `Unknown tool: ${call.name}. Available: ${tools.map((t) => t.name).join(', ')}` };
|
|
@@ -187,19 +377,144 @@ export async function runTextProtocolToolLoopDetailed(provider, baseMessages, to
|
|
|
187
377
|
}
|
|
188
378
|
else {
|
|
189
379
|
try {
|
|
190
|
-
assertMayStartToolCall(options);
|
|
380
|
+
assertMayStartToolCall(options, call.name);
|
|
191
381
|
output = await tool.run(call.input ?? {});
|
|
192
382
|
}
|
|
193
383
|
catch (err) {
|
|
194
|
-
|
|
384
|
+
const code = toolLoopErrorCode(err);
|
|
385
|
+
output = {
|
|
386
|
+
error: err instanceof Error ? err.message : String(err),
|
|
387
|
+
...(code ? { code } : {}),
|
|
388
|
+
};
|
|
195
389
|
isError = true;
|
|
390
|
+
deadlineStop = providerDispatchTerminal(err);
|
|
196
391
|
}
|
|
197
392
|
}
|
|
198
393
|
toolCalls += 1;
|
|
199
|
-
|
|
394
|
+
// Tool-call observers are diagnostics only. In particular, Ask V2 records
|
|
395
|
+
// a terminal `finish_answer` through this callback; an observer bug must
|
|
396
|
+
// never turn an already-authorized execution into a second provider turn
|
|
397
|
+
// (or overwrite it as a planner/budget failure).
|
|
398
|
+
notifyToolCall(options, { name: call.name, input: call.input, output, isError, durationMs: Date.now() - startedAt });
|
|
399
|
+
if (deadlineStop)
|
|
400
|
+
return { ...deadlineStop, text: lastText, toolCalls };
|
|
200
401
|
messages.push({ role: 'assistant', content: text });
|
|
201
402
|
messages.push({ role: 'user', content: renderObservation(call.name, output) });
|
|
403
|
+
const progressInstruction = renderCurrentToolPolicy(options, tools);
|
|
404
|
+
if (progressInstruction)
|
|
405
|
+
messages.push({ role: 'system', content: progressInstruction });
|
|
406
|
+
// A *completed* terminal host control carries its final answer or stable
|
|
407
|
+
// clarification in the tool result. Do not spend another provider send
|
|
408
|
+
// to ask the model to repeat it. Crucially, a denied/ineligible terminal
|
|
409
|
+
// proposal does not have this marker: its safe-next-tool observation is
|
|
410
|
+
// fed into the next controller dispatch below.
|
|
411
|
+
if (isTerminalControlCall && !isError && isCompletedAskV2TerminalControlOutput(output)) {
|
|
412
|
+
return { text, stop: 'final', toolCalls };
|
|
413
|
+
}
|
|
414
|
+
// An execution action at the final tool-followup slot must leave the next
|
|
415
|
+
// physical send for `finish_answer`. A failed/ineligible final action has
|
|
416
|
+
// no safe room for a second discovery attempt, so preserve the precise
|
|
417
|
+
// dispatch-budget boundary rather than emitting a misleading coverage
|
|
418
|
+
// terminal. A completed execution loops once more for host narration.
|
|
419
|
+
if (terminalActionRound) {
|
|
420
|
+
const nowRequiresNarration = currentToolPolicy(options, tools).terminalActionToolNames.has('finish_answer');
|
|
421
|
+
if (isError || (isTerminalControlCall && !isCompletedAskV2TerminalControlOutput(output))) {
|
|
422
|
+
return { text, stop: 'provider_dispatch_budget_exhausted', toolCalls };
|
|
423
|
+
}
|
|
424
|
+
if (nowRequiresNarration) {
|
|
425
|
+
// The execution is validated and the next loop iteration emits the
|
|
426
|
+
// sixth, narration-phase physical send with only finish_answer exposed.
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (!reservePostExecutionNarration)
|
|
430
|
+
return { text, stop: 'final', toolCalls };
|
|
431
|
+
return { text, stop: 'provider_dispatch_budget_exhausted', toolCalls };
|
|
432
|
+
}
|
|
202
433
|
if (toolCalls >= effectiveToolBudget) {
|
|
434
|
+
const policy = currentToolPolicy(options, tools);
|
|
435
|
+
// A bounded Ask controller may reserve the last physical send for one
|
|
436
|
+
// terminal *action* (for example, semantic compilation) rather than
|
|
437
|
+
// prose. This remains model-controlled: the host only narrows the
|
|
438
|
+
// tool set after prior observations make repeated discovery unsafe or
|
|
439
|
+
// wasteful. Other tools cannot use this reserve.
|
|
440
|
+
if (policy.terminalActionToolNames.size > 0) {
|
|
441
|
+
messages.push({
|
|
442
|
+
role: 'user',
|
|
443
|
+
content: `Final controller action turn. Call exactly one of: ${[...policy.terminalActionToolNames].join(', ')}. Do not inspect more context or write a prose answer.`,
|
|
444
|
+
});
|
|
445
|
+
const finalDispatchOptions = providerTurns > 0 && runOptions.dispatchPhase === 'agent_control'
|
|
446
|
+
? {
|
|
447
|
+
...runOptions,
|
|
448
|
+
dispatchPhase: policy.terminalActionToolNames.has('finish_answer')
|
|
449
|
+
? 'narration'
|
|
450
|
+
: 'tool_followup',
|
|
451
|
+
}
|
|
452
|
+
: runOptions;
|
|
453
|
+
providerTurns += 1;
|
|
454
|
+
let finalText;
|
|
455
|
+
try {
|
|
456
|
+
finalText = await provider.generate(messages, finalDispatchOptions);
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
const terminal = providerDispatchTerminal(error);
|
|
460
|
+
if (terminal)
|
|
461
|
+
return { ...terminal, text: lastText, toolCalls };
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
if (!finalText.trim()) {
|
|
465
|
+
return { text: lastText, stop: 'provider_dispatch_budget_exhausted', toolCalls };
|
|
466
|
+
}
|
|
467
|
+
const terminalCall = parseTextToolCall(finalText);
|
|
468
|
+
if (!terminalCall || !policy.terminalActionToolNames.has(terminalCall.name)) {
|
|
469
|
+
return {
|
|
470
|
+
text: '',
|
|
471
|
+
stop: terminalCall ? 'tool_budget_exhausted' : 'provider_dispatch_budget_exhausted',
|
|
472
|
+
toolCalls,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
const terminalTool = toolMap.get(terminalCall.name);
|
|
476
|
+
let terminalOutput;
|
|
477
|
+
let terminalError = false;
|
|
478
|
+
let terminalDeadlineStop;
|
|
479
|
+
const terminalStartedAt = Date.now();
|
|
480
|
+
if (!terminalTool) {
|
|
481
|
+
terminalOutput = { error: `Unknown terminal tool: ${terminalCall.name}` };
|
|
482
|
+
terminalError = true;
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
try {
|
|
486
|
+
assertMayStartToolCall(options, terminalCall.name);
|
|
487
|
+
terminalOutput = await terminalTool.run(terminalCall.input ?? {});
|
|
488
|
+
}
|
|
489
|
+
catch (err) {
|
|
490
|
+
const code = toolLoopErrorCode(err);
|
|
491
|
+
terminalOutput = {
|
|
492
|
+
error: err instanceof Error ? err.message : String(err),
|
|
493
|
+
...(code ? { code } : {}),
|
|
494
|
+
};
|
|
495
|
+
terminalError = true;
|
|
496
|
+
terminalDeadlineStop = providerDispatchTerminal(err);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
toolCalls += 1;
|
|
500
|
+
notifyToolCall(options, {
|
|
501
|
+
name: terminalCall.name,
|
|
502
|
+
input: terminalCall.input,
|
|
503
|
+
output: terminalOutput,
|
|
504
|
+
isError: terminalError,
|
|
505
|
+
durationMs: Date.now() - terminalStartedAt,
|
|
506
|
+
});
|
|
507
|
+
if (terminalDeadlineStop)
|
|
508
|
+
return { ...terminalDeadlineStop, text: lastText, toolCalls };
|
|
509
|
+
return {
|
|
510
|
+
text: finalText,
|
|
511
|
+
stop: terminalError || (isAskV2TerminalControlTool(terminalCall.name)
|
|
512
|
+
&& !isCompletedAskV2TerminalControlOutput(terminalOutput))
|
|
513
|
+
? 'tool_budget_exhausted'
|
|
514
|
+
: 'final',
|
|
515
|
+
toolCalls,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
203
518
|
messages.push({
|
|
204
519
|
role: 'user',
|
|
205
520
|
content: 'Tool budget reached — do not call any more tools. Answer now using only the tool results above, as a single ```json fenced object with summary, sql, viz, outputs.',
|
|
@@ -207,14 +522,26 @@ export async function runTextProtocolToolLoopDetailed(provider, baseMessages, to
|
|
|
207
522
|
// The next iteration is the reserved final dispatch. If the model
|
|
208
523
|
// nevertheless emits a tool shape, `call` is deliberately disabled and
|
|
209
524
|
// the caller receives that text as a typed terminal, not an execution.
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
525
|
+
const finalPolicy = currentToolPolicy(options, tools);
|
|
526
|
+
const finalDispatchOptions = providerTurns > 0 && runOptions.dispatchPhase === 'agent_control'
|
|
527
|
+
? {
|
|
528
|
+
...runOptions,
|
|
529
|
+
dispatchPhase: finalPolicy.terminalActionToolNames.has('finish_answer')
|
|
530
|
+
? 'narration'
|
|
531
|
+
: 'tool_followup',
|
|
532
|
+
}
|
|
533
|
+
: runOptions;
|
|
534
|
+
providerTurns += 1;
|
|
535
|
+
let finalText;
|
|
536
|
+
try {
|
|
537
|
+
finalText = await provider.generate(messages, finalDispatchOptions);
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
const terminal = providerDispatchTerminal(error);
|
|
541
|
+
if (terminal)
|
|
542
|
+
return { ...terminal, text: lastText, toolCalls };
|
|
216
543
|
throw error;
|
|
217
|
-
}
|
|
544
|
+
}
|
|
218
545
|
if (!finalText.trim()) {
|
|
219
546
|
return {
|
|
220
547
|
text: lastText,
|
|
@@ -222,21 +549,103 @@ export async function runTextProtocolToolLoopDetailed(provider, baseMessages, to
|
|
|
222
549
|
toolCalls,
|
|
223
550
|
};
|
|
224
551
|
}
|
|
552
|
+
const terminalCall = parseTextToolCall(finalText);
|
|
553
|
+
// The reserved composition dispatch may legally be an Ask V2 terminal
|
|
554
|
+
// control. Execute it locally only when it reports a completed terminal
|
|
555
|
+
// outcome. A premature/denied finish or clarification has consumed the
|
|
556
|
+
// final physical send, so preserve the precise budget stop rather than
|
|
557
|
+
// pretending that it produced a final answer.
|
|
558
|
+
if (terminalCall && isAskV2TerminalControlTool(terminalCall.name)) {
|
|
559
|
+
const terminalTool = toolMap.get(terminalCall.name);
|
|
560
|
+
let terminalOutput;
|
|
561
|
+
let terminalError = false;
|
|
562
|
+
let terminalDeadlineStop;
|
|
563
|
+
const terminalStartedAt = Date.now();
|
|
564
|
+
if (!terminalTool) {
|
|
565
|
+
terminalOutput = { error: 'Unknown tool: finish_answer' };
|
|
566
|
+
terminalError = true;
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
try {
|
|
570
|
+
assertMayStartToolCall(options, terminalCall.name);
|
|
571
|
+
terminalOutput = await terminalTool.run(terminalCall.input ?? {});
|
|
572
|
+
}
|
|
573
|
+
catch (err) {
|
|
574
|
+
const code = toolLoopErrorCode(err);
|
|
575
|
+
terminalOutput = {
|
|
576
|
+
error: err instanceof Error ? err.message : String(err),
|
|
577
|
+
...(code ? { code } : {}),
|
|
578
|
+
};
|
|
579
|
+
terminalError = true;
|
|
580
|
+
terminalDeadlineStop = providerDispatchTerminal(err);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
toolCalls += 1;
|
|
584
|
+
notifyToolCall(options, {
|
|
585
|
+
name: terminalCall.name,
|
|
586
|
+
input: terminalCall.input,
|
|
587
|
+
output: terminalOutput,
|
|
588
|
+
isError: terminalError,
|
|
589
|
+
durationMs: Date.now() - terminalStartedAt,
|
|
590
|
+
});
|
|
591
|
+
if (terminalDeadlineStop)
|
|
592
|
+
return { ...terminalDeadlineStop, text: lastText, toolCalls };
|
|
593
|
+
return {
|
|
594
|
+
text: finalText,
|
|
595
|
+
stop: terminalError || !isCompletedAskV2TerminalControlOutput(terminalOutput)
|
|
596
|
+
? 'tool_budget_exhausted'
|
|
597
|
+
: 'final',
|
|
598
|
+
toolCalls,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
225
601
|
return {
|
|
226
602
|
text: finalText,
|
|
227
|
-
stop:
|
|
603
|
+
stop: terminalCall ? 'tool_budget_exhausted' : 'final',
|
|
228
604
|
toolCalls,
|
|
229
605
|
};
|
|
230
606
|
}
|
|
231
607
|
}
|
|
232
608
|
}
|
|
233
|
-
|
|
609
|
+
/**
|
|
610
|
+
* Only explicit Ask V2 controls can terminate a transport early. Execution
|
|
611
|
+
* and retrieval tools may return useful `{ finished: true }`-shaped payloads
|
|
612
|
+
* for their own protocols, but they do not own final answer authority.
|
|
613
|
+
*/
|
|
614
|
+
function isAskV2TerminalControlTool(name) {
|
|
615
|
+
return name === 'finish_answer' || name === 'request_clarification';
|
|
616
|
+
}
|
|
617
|
+
function isCompletedAskV2TerminalControlOutput(value) {
|
|
618
|
+
return Boolean(value
|
|
619
|
+
&& typeof value === 'object'
|
|
620
|
+
&& !Array.isArray(value)
|
|
621
|
+
&& value.finished === true);
|
|
622
|
+
}
|
|
623
|
+
function assertMayStartToolCall(options, toolName) {
|
|
624
|
+
// finish_answer is a host-local terminal control following an already
|
|
625
|
+
// admitted provider response. It cannot start discovery or a warehouse
|
|
626
|
+
// operation, so the final control itself may consume the narration reserve.
|
|
627
|
+
if (toolName === 'finish_answer')
|
|
628
|
+
return;
|
|
234
629
|
if (options.mayStartToolCall?.() === false) {
|
|
235
630
|
throw Object.assign(new Error('The run soft target elapsed before this tool branch could start.'), {
|
|
236
631
|
code: 'RUN_SOFT_TARGET_EXCEEDED',
|
|
237
632
|
});
|
|
238
633
|
}
|
|
239
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* Observability must be fail-open with respect to the bounded tool runtime.
|
|
637
|
+
* Provider/tool callbacks are outside the execution authority and cannot be
|
|
638
|
+
* allowed to reopen a finished response or alter its terminal result.
|
|
639
|
+
*/
|
|
640
|
+
function notifyToolCall(options, event) {
|
|
641
|
+
try {
|
|
642
|
+
options.onToolCall?.(event);
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
// Receipt recording has its own error handling at the host boundary. The
|
|
646
|
+
// transport still has a valid, typed tool outcome to return to the caller.
|
|
647
|
+
}
|
|
648
|
+
}
|
|
240
649
|
/**
|
|
241
650
|
* Parse a text-protocol tool call. A tool call is a JSON object with a `tool`
|
|
242
651
|
* (string) field, optionally in a ```json fence. Anything else — prose, or a JSON
|