@ouro.bot/cli 0.1.0-alpha.806 → 0.1.0-alpha.808
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/changelog.json +16 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/core.js +156 -188
- package/dist/heart/daemon/cli-exec.js +3 -0
- package/dist/heart/mailbox/readers/sessions.js +2 -3
- package/dist/heart/session-activity.js +3 -1
- package/dist/heart/session-events.js +167 -55
- package/dist/heart/session-redaction-repair-cli-main.js +31 -0
- package/dist/heart/session-redaction-repair.js +543 -0
- package/dist/heart/session-transcript.js +1 -1
- package/dist/mind/session-transaction.js +180 -31
- package/dist/senses/shared-turn.js +5 -3
- package/npm-shrinkwrap.json +94 -100
- package/package.json +2 -2
package/dist/heart/core.js
CHANGED
|
@@ -553,11 +553,6 @@ function messageContentText(content) {
|
|
|
553
553
|
.filter(Boolean)
|
|
554
554
|
.join("\n");
|
|
555
555
|
}
|
|
556
|
-
function isHarnessCorrectiveUserText(text) {
|
|
557
|
-
return text.startsWith("no tool was called this turn. you must end every turn")
|
|
558
|
-
|| text.startsWith("private-return acknowledgement claimed work was queued, but no ponder packet was created this turn.")
|
|
559
|
-
|| text.startsWith("this exact request previously reached a tool that is advertised again now.");
|
|
560
|
-
}
|
|
561
556
|
function toolResultIndicatesFailure(content) {
|
|
562
557
|
const normalized = content.trim().toLowerCase();
|
|
563
558
|
return normalized.startsWith("error:")
|
|
@@ -598,7 +593,7 @@ function historicalFailedEffectsForExactRepeatedRequest(messages) {
|
|
|
598
593
|
if (message?.role !== "user")
|
|
599
594
|
continue;
|
|
600
595
|
const text = messageContentText(message.content).replace(/\s+/g, " ").trim();
|
|
601
|
-
if (!text
|
|
596
|
+
if (!text)
|
|
602
597
|
continue;
|
|
603
598
|
userRequests.push({ index, text });
|
|
604
599
|
}
|
|
@@ -643,8 +638,6 @@ function latestUserMessageText(messages) {
|
|
|
643
638
|
if (message?.role !== "user")
|
|
644
639
|
continue;
|
|
645
640
|
const text = messageContentText(message.content).trim();
|
|
646
|
-
if (isHarnessCorrectiveUserText(text))
|
|
647
|
-
continue;
|
|
648
641
|
if (text.length > 0)
|
|
649
642
|
return text;
|
|
650
643
|
}
|
|
@@ -960,9 +953,15 @@ function buildAuthFailureGuidance(provider, model, agentName, detail) {
|
|
|
960
953
|
}
|
|
961
954
|
async function runAgent(messages, callbacks, channel, signal, options) {
|
|
962
955
|
const generatedMessages = [];
|
|
956
|
+
let nextAttemptControls = [];
|
|
957
|
+
let rejectedAttempt = [];
|
|
963
958
|
const pushGenerated = (...next) => {
|
|
964
959
|
messages.push(...next);
|
|
965
960
|
generatedMessages.push(...structuredClone(next));
|
|
961
|
+
if (rejectedAttempt.length > 0) {
|
|
962
|
+
rejectedAttempt = [];
|
|
963
|
+
providerRuntime.resetTurnState(messages);
|
|
964
|
+
}
|
|
966
965
|
};
|
|
967
966
|
const facing = (0, friends_1.channelToFacing)(channel);
|
|
968
967
|
let providerRuntime = options?.providerRuntimeOverride ?? await getProviderRuntime(facing);
|
|
@@ -1127,10 +1126,14 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1127
1126
|
let providerIterations = 0;
|
|
1128
1127
|
const requiredToolCallNames = [...new Set(options?.requiredToolCalls?.names ?? [])];
|
|
1129
1128
|
const dispatchedRequiredToolCalls = new Set();
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1129
|
+
const rejectAttempt = (assistant, controls) => {
|
|
1130
|
+
rejectedAttempt = [structuredClone(assistant)];
|
|
1131
|
+
nextAttemptControls = controls;
|
|
1132
|
+
};
|
|
1133
|
+
const rejectToolBatch = (assistant, reasons) => {
|
|
1134
|
+
rejectAttempt(assistant, assistant.tool_calls.map((call, index) => ({
|
|
1135
|
+
role: "tool", tool_call_id: call.id, content: reasons[index],
|
|
1136
|
+
})));
|
|
1134
1137
|
};
|
|
1135
1138
|
const pendingRequiredToolCalls = () => {
|
|
1136
1139
|
const missing = requiredToolCallNames.filter((name) => !dispatchedRequiredToolCalls.has(name));
|
|
@@ -1138,16 +1141,10 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1138
1141
|
? { missing, message: `${options.requiredToolCalls.retryMessage} Missing required tool calls: ${missing.join(", ")}.` }
|
|
1139
1142
|
: null;
|
|
1140
1143
|
};
|
|
1141
|
-
const queueRequiredCorrection = (message, limitContext) => {
|
|
1144
|
+
const queueRequiredCorrection = (assistant, message, limitContext) => {
|
|
1142
1145
|
if (providerIterations >= exports.MAX_PROVIDER_ITERATIONS)
|
|
1143
1146
|
throw new Error(`provider iteration limit exhausted at response ${exports.MAX_PROVIDER_ITERATIONS} ${limitContext}`);
|
|
1144
|
-
|
|
1145
|
-
role: "user",
|
|
1146
|
-
content: message,
|
|
1147
|
-
[requiredCorrectionMarker]: true,
|
|
1148
|
-
};
|
|
1149
|
-
messages.push(correction);
|
|
1150
|
-
providerRuntime.resetTurnState(messages);
|
|
1147
|
+
rejectAttempt(assistant, [{ role: "user", content: message }]);
|
|
1151
1148
|
};
|
|
1152
1149
|
const toolLoopState = (0, tool_loop_1.createToolLoopState)();
|
|
1153
1150
|
const toolFrictionLedger = (0, tool_friction_1.createToolFrictionLedger)();
|
|
@@ -1180,8 +1177,10 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1180
1177
|
summary: (0, error_classification_1.summarizeProviderError)(terminalError, terminalErrorClassification, providerRuntime.id, providerRuntime.model),
|
|
1181
1178
|
},
|
|
1182
1179
|
});
|
|
1183
|
-
|
|
1184
|
-
|
|
1180
|
+
// Only legacy input can still contain unaccepted calls. Generated output
|
|
1181
|
+
// has crossed acceptance; a later provider failure cannot revoke effects.
|
|
1182
|
+
if (generatedMessages.length === 0)
|
|
1183
|
+
stripLastToolCalls(messages);
|
|
1185
1184
|
outcome = "errored";
|
|
1186
1185
|
done = true;
|
|
1187
1186
|
};
|
|
@@ -1304,6 +1303,23 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1304
1303
|
break;
|
|
1305
1304
|
}
|
|
1306
1305
|
try {
|
|
1306
|
+
const budgetOptions = {
|
|
1307
|
+
requiredPromptEvidence: options?.requiredPromptEvidence,
|
|
1308
|
+
provider: providerRuntime.id,
|
|
1309
|
+
model: providerRuntime.model,
|
|
1310
|
+
contextWindowTokens: (0, config_1.getContextConfig)().maxTokens,
|
|
1311
|
+
};
|
|
1312
|
+
const canonicalBudget = (0, prompt_budget_1.applyPromptBudget)({ ...budgetOptions, messages });
|
|
1313
|
+
if (canonicalBudget.status !== "within_budget") {
|
|
1314
|
+
messages.splice(0, messages.length, ...canonicalBudget.messages);
|
|
1315
|
+
}
|
|
1316
|
+
// Preserve canonical evidence object identity while freezing the array
|
|
1317
|
+
// and the one attempt's private control/scratch across operational retries.
|
|
1318
|
+
const attemptMessages = rejectedAttempt.length > 0
|
|
1319
|
+
? [...(0, prompt_budget_1.applyPromptBudget)({ ...budgetOptions, messages: [...messages, ...rejectedAttempt, ...nextAttemptControls] }).messages]
|
|
1320
|
+
: [...messages];
|
|
1321
|
+
if (rejectedAttempt.length > 0 || canonicalBudget.status !== "within_budget")
|
|
1322
|
+
providerRuntime.resetTurnState(attemptMessages);
|
|
1307
1323
|
const turnCallbackBufferRef = { current: null };
|
|
1308
1324
|
const callProviderTurn = async () => {
|
|
1309
1325
|
callbacks.onModelStart();
|
|
@@ -1313,19 +1329,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1313
1329
|
? createFinalOnlyTextBuffer(callbacks)
|
|
1314
1330
|
: null;
|
|
1315
1331
|
try {
|
|
1316
|
-
const promptBudget = (0, prompt_budget_1.applyPromptBudget)({
|
|
1317
|
-
messages,
|
|
1318
|
-
requiredPromptEvidence: options?.requiredPromptEvidence,
|
|
1319
|
-
provider: providerRuntime.id,
|
|
1320
|
-
model: providerRuntime.model,
|
|
1321
|
-
contextWindowTokens: (0, config_1.getContextConfig)().maxTokens,
|
|
1322
|
-
});
|
|
1323
|
-
if (promptBudget.status !== "within_budget") {
|
|
1324
|
-
messages.splice(0, messages.length, ...promptBudget.messages);
|
|
1325
|
-
providerRuntime.resetTurnState(messages);
|
|
1326
|
-
}
|
|
1327
1332
|
return await providerRuntime.streamTurn({
|
|
1328
|
-
messages,
|
|
1333
|
+
messages: attemptMessages,
|
|
1329
1334
|
activeTools,
|
|
1330
1335
|
callbacks: turnCallbackBufferRef.current?.callbacks ?? callbacks,
|
|
1331
1336
|
signal,
|
|
@@ -1353,10 +1358,10 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1353
1358
|
throw error;
|
|
1354
1359
|
if (isContextOverflow(error) && !overflowRetried) {
|
|
1355
1360
|
overflowRetried = true;
|
|
1356
|
-
|
|
1357
|
-
stripLastToolCalls(
|
|
1361
|
+
const overflowMessages = messages.map((message) => message.role === "assistant" ? { ...message } : message);
|
|
1362
|
+
stripLastToolCalls(overflowMessages);
|
|
1358
1363
|
const { maxTokens, contextMargin } = (0, config_1.getContextConfig)();
|
|
1359
|
-
const trimmed = (0, context_1.trimMessages)(
|
|
1364
|
+
const trimmed = (0, context_1.trimMessages)(overflowMessages, maxTokens, contextMargin, maxTokens * 2);
|
|
1360
1365
|
const requiredEvidence = options?.requiredPromptEvidence;
|
|
1361
1366
|
const requiredMessages = new Set([
|
|
1362
1367
|
...(requiredEvidence?.verifiedPredecessorMessage ? [requiredEvidence.verifiedPredecessorMessage] : []),
|
|
@@ -1365,9 +1370,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1365
1370
|
const trimmedMessages = new Set(trimmed);
|
|
1366
1371
|
const overflowRetryMessages = requiredMessages.size === 0
|
|
1367
1372
|
? trimmed
|
|
1368
|
-
:
|
|
1373
|
+
: overflowMessages.filter((message) => trimmedMessages.has(message) || requiredMessages.has(message));
|
|
1369
1374
|
messages.splice(0, messages.length, ...overflowRetryMessages);
|
|
1370
|
-
providerRuntime.resetTurnState(
|
|
1375
|
+
providerRuntime.resetTurnState(attemptMessages);
|
|
1371
1376
|
callbacks.onError(new Error("context trimmed, retrying..."), "transient");
|
|
1372
1377
|
return callProviderTurn();
|
|
1373
1378
|
}
|
|
@@ -1397,7 +1402,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1397
1402
|
});
|
|
1398
1403
|
_providerRuntimeFactories[facing] = null;
|
|
1399
1404
|
providerRuntime = await getProviderRuntime(facing);
|
|
1400
|
-
providerRuntime.resetTurnState(
|
|
1405
|
+
providerRuntime.resetTurnState(attemptMessages);
|
|
1401
1406
|
}
|
|
1402
1407
|
catch (refreshError) {
|
|
1403
1408
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -1412,7 +1417,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1412
1417
|
},
|
|
1413
1418
|
sleep: async (delayMs) => {
|
|
1414
1419
|
await waitForProviderRetry(delayMs, signal);
|
|
1415
|
-
providerRuntime.resetTurnState(
|
|
1420
|
+
providerRuntime.resetTurnState(attemptMessages);
|
|
1416
1421
|
},
|
|
1417
1422
|
});
|
|
1418
1423
|
if (!attempt.ok) {
|
|
@@ -1420,6 +1425,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1420
1425
|
continue;
|
|
1421
1426
|
}
|
|
1422
1427
|
const result = attempt.value;
|
|
1428
|
+
nextAttemptControls = [];
|
|
1423
1429
|
providerIterations += 1;
|
|
1424
1430
|
if (providerIterations === exports.MAX_PROVIDER_ITERATIONS && result.toolCalls.length > 0) {
|
|
1425
1431
|
throw new Error(`provider iteration limit exhausted at response ${exports.MAX_PROVIDER_ITERATIONS} before tool execution`);
|
|
@@ -1526,8 +1532,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1526
1532
|
message: "unresolved historical effect exhausted deterministic tool-call retries; failing closed",
|
|
1527
1533
|
meta: { provider: providerRuntime.id, model: providerRuntime.model, toolNames: retryableHistoricalTools, cap: NO_TOOL_CALL_MAX_RETRIES },
|
|
1528
1534
|
});
|
|
1529
|
-
|
|
1530
|
-
pushGenerated(msg);
|
|
1535
|
+
pushGenerated({ role: "assistant", content: blockedAnswer });
|
|
1531
1536
|
callbacks.onTextChunk(blockedAnswer);
|
|
1532
1537
|
completion = { answer: blockedAnswer, intent: "blocked" };
|
|
1533
1538
|
outcome = "blocked";
|
|
@@ -1548,18 +1553,17 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1548
1553
|
cap: NO_TOOL_CALL_MAX_RETRIES,
|
|
1549
1554
|
},
|
|
1550
1555
|
});
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
});
|
|
1556
|
+
rejectAttempt(msg, [{
|
|
1557
|
+
role: "user",
|
|
1558
|
+
content: `this exact request previously reached a tool that is advertised again now. Its unresolved historical effect is through ${retryableHistoricalTools.join(", ")}. Only that tool is available now. Read current state if needed, then retry the exact failed effect. Do not report completion until the effect succeeds.`,
|
|
1559
|
+
}]);
|
|
1556
1560
|
continue;
|
|
1557
1561
|
}
|
|
1558
1562
|
const requiredToolCallsGate = pendingRequiredToolCalls();
|
|
1559
1563
|
if (requiredToolCallsGate) {
|
|
1560
1564
|
streamCallbackBuffer?.discard();
|
|
1561
1565
|
callbacks.onClearText?.();
|
|
1562
|
-
queueRequiredCorrection(requiredToolCallsGate.message, "before required tool calls completed");
|
|
1566
|
+
queueRequiredCorrection(msg, requiredToolCallsGate.message, "before required tool calls completed");
|
|
1563
1567
|
(0, runtime_1.emitNervesEvent)({
|
|
1564
1568
|
level: "warn",
|
|
1565
1569
|
component: "engine",
|
|
@@ -1573,7 +1577,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1573
1577
|
if (requiredAnswerRejection) {
|
|
1574
1578
|
streamCallbackBuffer?.discard();
|
|
1575
1579
|
callbacks.onClearText?.();
|
|
1576
|
-
queueRequiredCorrection(requiredAnswerRejection, "before required terminal answer validation completed");
|
|
1580
|
+
queueRequiredCorrection(msg, requiredAnswerRejection, "before required terminal answer validation completed");
|
|
1577
1581
|
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "engine", event: "engine.required_tool_answer_rejected", message: "unsupported terminal answer rejected after required reads", meta: { answerLength: String(msg.content ?? "").length } });
|
|
1578
1582
|
continue;
|
|
1579
1583
|
}
|
|
@@ -1596,11 +1600,10 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1596
1600
|
contentLength: result.content.length,
|
|
1597
1601
|
},
|
|
1598
1602
|
});
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
});
|
|
1603
|
+
rejectAttempt(msg, [{
|
|
1604
|
+
role: "user",
|
|
1605
|
+
content: `${privateReturnTextAckRetryError} Emit the ponder(action=create, ...) tool call now, or ask a blocking clarification without saying the private work is queued.`,
|
|
1606
|
+
}]);
|
|
1604
1607
|
continue;
|
|
1605
1608
|
}
|
|
1606
1609
|
const blockedAnswer = "I could not start the private pass. No private-attention packet was created, so no return work was queued.";
|
|
@@ -1616,8 +1619,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1616
1619
|
contentLength: result.content.length,
|
|
1617
1620
|
},
|
|
1618
1621
|
});
|
|
1619
|
-
|
|
1620
|
-
pushGenerated(msg);
|
|
1622
|
+
pushGenerated({ role: "assistant", content: blockedAnswer });
|
|
1621
1623
|
callbacks.onTextChunk(blockedAnswer);
|
|
1622
1624
|
completion = { answer: blockedAnswer, intent: "blocked" };
|
|
1623
1625
|
outcome = "blocked";
|
|
@@ -1645,15 +1647,14 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1645
1647
|
contentLength: result.content.length,
|
|
1646
1648
|
},
|
|
1647
1649
|
});
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
: "no tool was called this turn. you must end every turn by calling
|
|
1655
|
-
|
|
1656
|
-
});
|
|
1650
|
+
rejectAttempt(msg, [{
|
|
1651
|
+
role: "user",
|
|
1652
|
+
content: isPrivateRuntimeChannel
|
|
1653
|
+
? augmentedToolContext?.noSend === true
|
|
1654
|
+
? "no tool was called this turn. this is an immutable no-send turn; call rest now without creating a continuation."
|
|
1655
|
+
: "no tool was called this turn. you must end every turn by calling rest (or surface, ponder, observe). emit the tool call now."
|
|
1656
|
+
: "no tool was called this turn. you must end every turn by calling settle with your answer (or ponder/observe). emit the tool call now.",
|
|
1657
|
+
}]);
|
|
1657
1658
|
continue;
|
|
1658
1659
|
}
|
|
1659
1660
|
// Legitimate text-only response, or cap reached — accept as-is.
|
|
@@ -1664,20 +1665,19 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1664
1665
|
else {
|
|
1665
1666
|
// Reset the retry counter on any successful tool call.
|
|
1666
1667
|
noToolCallRetries = 0;
|
|
1667
|
-
const preCallMessages = structuredClone(
|
|
1668
|
+
const preCallMessages = structuredClone(messages.filter((message) => message.role !== "system"));
|
|
1668
1669
|
const validatedCalls = validateToolCallBatchAtProductionBoundary(result.toolCalls, activeTools);
|
|
1669
1670
|
const invalidCall = validatedCalls.find((entry) => "error" in entry);
|
|
1670
1671
|
if (invalidCall) {
|
|
1671
|
-
|
|
1672
|
-
pushGenerated(msg);
|
|
1672
|
+
streamCallbackBuffer?.discard();
|
|
1673
1673
|
const unadvertisedCall = validatedCalls.find((entry) => !activeToolNames.has(entry.call.name));
|
|
1674
|
+
const rejections = [];
|
|
1674
1675
|
for (const entry of validatedCalls) {
|
|
1675
1676
|
const detail = "error" in entry ? entry.error : "another call in this batch had invalid arguments";
|
|
1676
1677
|
const rejection = unadvertisedCall
|
|
1677
1678
|
? `rejected: ${entry.call.name} was not advertised for this channel; no handler was executed.`
|
|
1678
1679
|
: `invalid tool arguments: ${detail}`;
|
|
1679
|
-
|
|
1680
|
-
providerRuntime.appendToolOutput(entry.call.id, rejection);
|
|
1680
|
+
rejections.push(rejection);
|
|
1681
1681
|
options?.toolBoundaryObserver?.({
|
|
1682
1682
|
name: entry.call.name,
|
|
1683
1683
|
reason: activeToolNames.has(entry.call.name) ? "invalid_arguments" : "profile_excluded",
|
|
@@ -1686,6 +1686,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1686
1686
|
sideEffect: false,
|
|
1687
1687
|
});
|
|
1688
1688
|
}
|
|
1689
|
+
rejectToolBatch(msg, rejections);
|
|
1689
1690
|
if (unadvertisedCall) {
|
|
1690
1691
|
(0, runtime_1.emitNervesEvent)({
|
|
1691
1692
|
level: "warn",
|
|
@@ -1715,12 +1716,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1715
1716
|
if (habitBlockReason) {
|
|
1716
1717
|
streamCallbackBuffer?.discard();
|
|
1717
1718
|
recordBlockedHabitSurfaceAttempts(habitSession, result.toolCalls, habitBlockReason);
|
|
1718
|
-
pushGenerated(msg);
|
|
1719
1719
|
const blockedOutput = `blocked: ${habitBlockReason}. No tool side effects from this assistant message were executed.`;
|
|
1720
|
-
|
|
1721
|
-
pushGenerated({ role: "tool", tool_call_id: call.id, content: blockedOutput });
|
|
1722
|
-
providerRuntime.appendToolOutput(call.id, blockedOutput);
|
|
1723
|
-
}
|
|
1720
|
+
rejectToolBatch(msg, result.toolCalls.map(() => blockedOutput));
|
|
1724
1721
|
(0, runtime_1.emitNervesEvent)({
|
|
1725
1722
|
level: "warn",
|
|
1726
1723
|
component: "engine",
|
|
@@ -1730,6 +1727,15 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1730
1727
|
});
|
|
1731
1728
|
continue;
|
|
1732
1729
|
}
|
|
1730
|
+
const soleViolation = result.toolCalls.length > 1
|
|
1731
|
+
? result.toolCalls.find((call) => SOLE_CALL_REJECTION[call.name] !== undefined || (0, tools_1.resolveToolDefinition)(call.name)?.terminalProjection?.requiresSoleCall === true)
|
|
1732
|
+
: undefined;
|
|
1733
|
+
if (soleViolation) {
|
|
1734
|
+
streamCallbackBuffer?.discard();
|
|
1735
|
+
const reason = SOLE_CALL_REJECTION[soleViolation.name] ?? `rejected: ${soleViolation.name} must be the only tool call.`;
|
|
1736
|
+
rejectToolBatch(msg, result.toolCalls.map(() => reason));
|
|
1737
|
+
continue;
|
|
1738
|
+
}
|
|
1733
1739
|
const soleTerminalCall = result.toolCalls.length === 1
|
|
1734
1740
|
? result.toolCalls[0]
|
|
1735
1741
|
: null;
|
|
@@ -1784,7 +1790,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1784
1790
|
streamCallbackBuffer?.discard();
|
|
1785
1791
|
callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
|
|
1786
1792
|
callbacks.onClearText?.();
|
|
1787
|
-
queueRequiredCorrection(requiredToolCallsGate.message, "before required tool calls completed");
|
|
1793
|
+
queueRequiredCorrection(msg, requiredToolCallsGate.message, "before required tool calls completed");
|
|
1788
1794
|
(0, runtime_1.emitNervesEvent)({
|
|
1789
1795
|
level: "warn",
|
|
1790
1796
|
component: "engine",
|
|
@@ -1800,10 +1806,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1800
1806
|
streamCallbackBuffer?.discard();
|
|
1801
1807
|
callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
|
|
1802
1808
|
callbacks.onClearText?.();
|
|
1803
|
-
pushGenerated(msg);
|
|
1804
1809
|
const gateMessage = "current held-work frame still has unsurfaced items — return each listed item with surface(delegationId=...) before you settle. Older transcript claims are historical; only the current held-work frame is the gate.";
|
|
1805
|
-
|
|
1806
|
-
providerRuntime.appendToolOutput(result.toolCalls[0].id, gateMessage);
|
|
1810
|
+
rejectToolBatch(msg, [gateMessage]);
|
|
1807
1811
|
continue;
|
|
1808
1812
|
}
|
|
1809
1813
|
// Extract answer from the tool call arguments.
|
|
@@ -1814,7 +1818,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1814
1818
|
streamCallbackBuffer?.discard();
|
|
1815
1819
|
callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
|
|
1816
1820
|
callbacks.onClearText?.();
|
|
1817
|
-
queueRequiredCorrection(requiredAnswerRejection, "before required terminal answer validation completed");
|
|
1821
|
+
queueRequiredCorrection(msg, requiredAnswerRejection, "before required terminal answer validation completed");
|
|
1818
1822
|
(0, runtime_1.emitNervesEvent)({ level: "warn", component: "engine", event: "engine.required_tool_answer_rejected", message: "unsupported settle answer rejected after required reads", meta: { answerLength: answer.length } });
|
|
1819
1823
|
continue;
|
|
1820
1824
|
}
|
|
@@ -1882,10 +1886,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1882
1886
|
streamCallbackBuffer?.discard();
|
|
1883
1887
|
callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
|
|
1884
1888
|
callbacks.onClearText?.();
|
|
1885
|
-
|
|
1886
|
-
const toolRetryMessage = retryError;
|
|
1887
|
-
pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: toolRetryMessage });
|
|
1888
|
-
providerRuntime.appendToolOutput(result.toolCalls[0].id, toolRetryMessage);
|
|
1889
|
+
rejectToolBatch(msg, [retryError]);
|
|
1889
1890
|
}
|
|
1890
1891
|
continue;
|
|
1891
1892
|
}
|
|
@@ -1923,19 +1924,15 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1923
1924
|
const attentionQueue = augmentedToolContext?.delegatedOrigins;
|
|
1924
1925
|
if (attentionQueue && attentionQueue.length > 0) {
|
|
1925
1926
|
callbacks.onToolEnd("rest", (0, tools_1.summarizeArgs)("rest", restArgs), false);
|
|
1926
|
-
pushGenerated(msg);
|
|
1927
1927
|
const gateMessage = "current held-work frame still has unsurfaced items — return each listed item with surface(delegationId=...) before you rest. Older transcript claims are historical; only the current held-work frame is the gate.";
|
|
1928
|
-
|
|
1929
|
-
providerRuntime.appendToolOutput(result.toolCalls[0].id, gateMessage);
|
|
1928
|
+
rejectToolBatch(msg, [gateMessage]);
|
|
1930
1929
|
continue;
|
|
1931
1930
|
}
|
|
1932
1931
|
if (hasFreshPendingWork(options) && !freshWorkGateFired) {
|
|
1933
1932
|
freshWorkGateFired = true;
|
|
1934
1933
|
callbacks.onToolEnd("rest", (0, tools_1.summarizeArgs)("rest", restArgs), false);
|
|
1935
|
-
pushGenerated(msg);
|
|
1936
1934
|
const gateMessage = "fresh work arrived for me this turn — inspect the pending messages above and take the next concrete action before you rest.";
|
|
1937
|
-
|
|
1938
|
-
providerRuntime.appendToolOutput(result.toolCalls[0].id, gateMessage);
|
|
1935
|
+
rejectToolBatch(msg, [gateMessage]);
|
|
1939
1936
|
(0, runtime_1.emitNervesEvent)({
|
|
1940
1937
|
level: "info",
|
|
1941
1938
|
component: "engine",
|
|
@@ -1967,7 +1964,56 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1967
1964
|
if (rejection)
|
|
1968
1965
|
requiredDispatchRejections.set(entry.call.id, { name: entry.call.name, args: requiredArgs, message: rejection });
|
|
1969
1966
|
}
|
|
1970
|
-
|
|
1967
|
+
if (requiredDispatchRejections.size > 0) {
|
|
1968
|
+
streamCallbackBuffer?.discard();
|
|
1969
|
+
for (const rejection of requiredDispatchRejections.values()) {
|
|
1970
|
+
callbacks.onToolStart(rejection.name, rejection.args);
|
|
1971
|
+
callbacks.onToolEnd(rejection.name, (0, tools_1.summarizeArgs)(rejection.name, rejection.args), false);
|
|
1972
|
+
options?.toolBoundaryObserver?.({
|
|
1973
|
+
name: rejection.name, reason: "dependency_rejected",
|
|
1974
|
+
globallyResolvable: typeof (0, tools_1.resolveToolDefinition)(rejection.name)?.handler === "function",
|
|
1975
|
+
invoked: false, sideEffect: false,
|
|
1976
|
+
});
|
|
1977
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1978
|
+
level: "warn", component: "engine", event: "engine.required_tool_dispatch_rejected",
|
|
1979
|
+
message: "required tool dependency rejected before approval and handler dispatch",
|
|
1980
|
+
meta: { toolName: rejection.name },
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
rejectToolBatch(msg, result.toolCalls.map((call) => requiredDispatchRejections.get(call.id)?.message
|
|
1984
|
+
?? "rejected: another call in this batch has an unsatisfied required dependency; no handler was executed."));
|
|
1985
|
+
continue;
|
|
1986
|
+
}
|
|
1987
|
+
const executionRejections = new Map();
|
|
1988
|
+
for (const entry of validCalls) {
|
|
1989
|
+
const args = validatedCallArguments.get(entry.call);
|
|
1990
|
+
const fingerprint = effectFingerprint(entry.call.name, entry.call.arguments);
|
|
1991
|
+
if (forcingHistoricalEffect && fingerprint && !unresolvedHistoricalEffects.some((effect) => effect.fingerprint === fingerprint)) {
|
|
1992
|
+
executionRejections.set(entry.call.id, "rejected: this turn is retrying an unresolved historical effect, and these mutation arguments do not match it. Read current state or retry the exact failed effect.");
|
|
1993
|
+
}
|
|
1994
|
+
else if (entry.call.name === "send_message" && args.friendId === "self" && !isPrivateRuntimeChannel && looksLikePrivateReturnRequest(latestUserMessageText(messages))) {
|
|
1995
|
+
executionRejections.set(entry.call.id, "private-return requests must use ponder, not send_message(friendId=self). Create a typed ponder packet with the marker/source request preserved, then only acknowledge that the private pass is queued.");
|
|
1996
|
+
}
|
|
1997
|
+
else if (entry.call.name !== "speak" && entry.call.name !== "ponder") {
|
|
1998
|
+
const loop = (0, tool_loop_1.detectToolLoop)(toolLoopState, entry.call.name, args);
|
|
1999
|
+
if (loop.stuck)
|
|
2000
|
+
executionRejections.set(entry.call.id, `loop guard: ${loop.message}`);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
if (executionRejections.size > 0) {
|
|
2004
|
+
streamCallbackBuffer?.discard();
|
|
2005
|
+
for (const entry of validCalls) {
|
|
2006
|
+
if (!executionRejections.has(entry.call.id))
|
|
2007
|
+
continue;
|
|
2008
|
+
const args = validatedCallArguments.get(entry.call);
|
|
2009
|
+
callbacks.onToolStart(entry.call.name, args);
|
|
2010
|
+
callbacks.onToolEnd(entry.call.name, (0, tools_1.summarizeArgs)(entry.call.name, args), false);
|
|
2011
|
+
}
|
|
2012
|
+
rejectToolBatch(msg, result.toolCalls.map((call) => executionRejections.get(call.id)
|
|
2013
|
+
?? "rejected: another call in this batch was inadmissible; no handler was executed."));
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
const approvalCalls = await Promise.all(validCalls.map(async (entry) => {
|
|
1971
2017
|
const classification = await (0, tools_1.classifyApprovalForInvocation)(entry.call.name, entry.validated.arguments, augmentedToolContext);
|
|
1972
2018
|
return {
|
|
1973
2019
|
...entry,
|
|
@@ -1977,12 +2023,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1977
2023
|
const protectedCall = approvalCalls.find((entry) => entry.policy.kind === "required");
|
|
1978
2024
|
if (protectedCall && result.toolCalls.length !== 1) {
|
|
1979
2025
|
streamCallbackBuffer?.discard();
|
|
1980
|
-
|
|
1981
|
-
for (const call of result.toolCalls) {
|
|
1982
|
-
const rejection = "rejected: approval-eligible tool must be the sole call; no call in this batch was executed.";
|
|
1983
|
-
pushGenerated({ role: "tool", tool_call_id: call.id, content: rejection });
|
|
1984
|
-
providerRuntime.appendToolOutput(call.id, rejection);
|
|
1985
|
-
}
|
|
2026
|
+
rejectToolBatch(msg, result.toolCalls.map(() => "rejected: approval-eligible tool must be the sole call; no call in this batch was executed."));
|
|
1986
2027
|
(0, runtime_1.emitNervesEvent)({
|
|
1987
2028
|
level: "warn",
|
|
1988
2029
|
component: "engine",
|
|
@@ -1995,10 +2036,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1995
2036
|
if (protectedCall && protectedCall.policy.kind === "required") {
|
|
1996
2037
|
if (!options?.approvalCoordinator) {
|
|
1997
2038
|
streamCallbackBuffer?.discard();
|
|
1998
|
-
pushGenerated(msg);
|
|
1999
2039
|
const rejection = "rejected: this protected tool requires approval, but the approval coordinator is unavailable; the handler was not invoked.";
|
|
2000
|
-
|
|
2001
|
-
providerRuntime.appendToolOutput(protectedCall.call.id, rejection);
|
|
2040
|
+
rejectToolBatch(msg, [rejection]);
|
|
2002
2041
|
(0, runtime_1.emitNervesEvent)({
|
|
2003
2042
|
level: "warn",
|
|
2004
2043
|
component: "engine",
|
|
@@ -2009,7 +2048,6 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
2009
2048
|
continue;
|
|
2010
2049
|
}
|
|
2011
2050
|
streamCallbackBuffer?.discard();
|
|
2012
|
-
pushGenerated(msg);
|
|
2013
2051
|
const toolDigest = (0, tool_arguments_1.digestJson)({
|
|
2014
2052
|
name: protectedCall.call.name,
|
|
2015
2053
|
schemaDigest: protectedCall.validated.schemaDigest,
|
|
@@ -2032,6 +2070,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
2032
2070
|
actionClass: protectedCall.policy.actionClass,
|
|
2033
2071
|
liveToolContext: augmentedToolContext,
|
|
2034
2072
|
});
|
|
2073
|
+
pushGenerated(msg);
|
|
2035
2074
|
suspension = {
|
|
2036
2075
|
approvalId: committed.approvalId,
|
|
2037
2076
|
toolCallId: protectedCall.call.id,
|
|
@@ -2048,79 +2087,15 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
2048
2087
|
});
|
|
2049
2088
|
continue;
|
|
2050
2089
|
}
|
|
2051
|
-
|
|
2052
|
-
&& result.toolCalls.some((call) => {
|
|
2053
|
-
const terminalProjection = (0, tools_1.resolveToolDefinition)(call.name)?.terminalProjection;
|
|
2054
|
-
return SOLE_CALL_REJECTION[call.name] !== undefined
|
|
2055
|
-
|| terminalProjection?.requiresSoleCall === true;
|
|
2056
|
-
});
|
|
2057
|
-
if (callbacks.settleOutputMode === "final_only" && containsSoleCallOnlyViolation) {
|
|
2058
|
-
streamCallbackBuffer?.discard();
|
|
2059
|
-
}
|
|
2060
|
-
else {
|
|
2061
|
-
await streamCallbackBuffer?.flush();
|
|
2062
|
-
}
|
|
2090
|
+
await streamCallbackBuffer?.flush();
|
|
2063
2091
|
pushGenerated(msg);
|
|
2064
|
-
//
|
|
2092
|
+
// Every pre-dispatch gate accepted the complete batch.
|
|
2065
2093
|
for (const tc of result.toolCalls) {
|
|
2066
2094
|
if (signal?.aborted)
|
|
2067
2095
|
break;
|
|
2068
|
-
const requiredDispatchRejection = requiredDispatchRejections.get(tc.id);
|
|
2069
|
-
if (requiredDispatchRejection) {
|
|
2070
|
-
callbacks.onToolStart(tc.name, requiredDispatchRejection.args);
|
|
2071
|
-
callbacks.onToolEnd(tc.name, (0, tools_1.summarizeArgs)(tc.name, requiredDispatchRejection.args), false);
|
|
2072
|
-
pushGenerated({ role: "tool", tool_call_id: tc.id, content: requiredDispatchRejection.message });
|
|
2073
|
-
providerRuntime.appendToolOutput(tc.id, requiredDispatchRejection.message);
|
|
2074
|
-
options?.toolBoundaryObserver?.({
|
|
2075
|
-
name: tc.name,
|
|
2076
|
-
reason: "dependency_rejected",
|
|
2077
|
-
globallyResolvable: typeof (0, tools_1.resolveToolDefinition)(tc.name)?.handler === "function",
|
|
2078
|
-
invoked: false,
|
|
2079
|
-
sideEffect: false,
|
|
2080
|
-
});
|
|
2081
|
-
(0, runtime_1.emitNervesEvent)({
|
|
2082
|
-
level: "warn",
|
|
2083
|
-
component: "engine",
|
|
2084
|
-
event: "engine.required_tool_dispatch_rejected",
|
|
2085
|
-
message: "required tool dependency rejected before approval and handler dispatch",
|
|
2086
|
-
meta: { toolName: tc.name },
|
|
2087
|
-
});
|
|
2088
|
-
continue;
|
|
2089
|
-
}
|
|
2090
|
-
// Reject sole-call tools when mixed with other tool calls
|
|
2091
|
-
const terminalProjection = (0, tools_1.resolveToolDefinition)(tc.name)?.terminalProjection;
|
|
2092
|
-
const soleCallRejection = SOLE_CALL_REJECTION[tc.name]
|
|
2093
|
-
?? (terminalProjection?.requiresSoleCall
|
|
2094
|
-
? `rejected: ${tc.name} must be the only tool call.`
|
|
2095
|
-
: undefined);
|
|
2096
|
-
if (soleCallRejection) {
|
|
2097
|
-
pushGenerated({ role: "tool", tool_call_id: tc.id, content: soleCallRejection });
|
|
2098
|
-
providerRuntime.appendToolOutput(tc.id, soleCallRejection);
|
|
2099
|
-
continue;
|
|
2100
|
-
}
|
|
2101
2096
|
const args = validatedCallArguments.get(tc);
|
|
2102
2097
|
const currentEffectFingerprint = effectFingerprint(tc.name, tc.arguments);
|
|
2103
|
-
if (forcingHistoricalEffect
|
|
2104
|
-
&& currentEffectFingerprint
|
|
2105
|
-
&& !unresolvedHistoricalEffects.some((effect) => effect.fingerprint === currentEffectFingerprint)) {
|
|
2106
|
-
const rejection = "rejected: this turn is retrying an unresolved historical effect, and these mutation arguments do not match it. Read current state or retry the exact failed effect.";
|
|
2107
|
-
callbacks.onToolStart(tc.name, args);
|
|
2108
|
-
callbacks.onToolEnd(tc.name, (0, tools_1.summarizeArgs)(tc.name, args), false);
|
|
2109
|
-
pushGenerated({ role: "tool", tool_call_id: tc.id, content: rejection });
|
|
2110
|
-
providerRuntime.appendToolOutput(tc.id, rejection);
|
|
2111
|
-
continue;
|
|
2112
|
-
}
|
|
2113
2098
|
if (tc.name === "send_message" && args.friendId === "self") {
|
|
2114
|
-
const latestUserText = latestUserMessageText(messages);
|
|
2115
|
-
if (!isPrivateRuntimeChannel && looksLikePrivateReturnRequest(latestUserText)) {
|
|
2116
|
-
const argSummary = (0, tools_1.summarizeArgs)(tc.name, args);
|
|
2117
|
-
const rejection = "private-return requests must use ponder, not send_message(friendId=self). Create a typed ponder packet with the marker/source request preserved, then only acknowledge that the private pass is queued.";
|
|
2118
|
-
callbacks.onToolStart(tc.name, args);
|
|
2119
|
-
callbacks.onToolEnd(tc.name, argSummary, false);
|
|
2120
|
-
pushGenerated({ role: "tool", tool_call_id: tc.id, content: rejection });
|
|
2121
|
-
providerRuntime.appendToolOutput(tc.id, rejection);
|
|
2122
|
-
continue;
|
|
2123
|
-
}
|
|
2124
2099
|
sawSendMessageSelf = true;
|
|
2125
2100
|
}
|
|
2126
2101
|
if (tc.name === "speak") {
|
|
@@ -2342,16 +2317,6 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
2342
2317
|
/* v8 ignore next -- flag tested via truth-check integration tests @preserve */
|
|
2343
2318
|
if (isExternalStateQuery(tc.name, args))
|
|
2344
2319
|
sawExternalStateQuery = true;
|
|
2345
|
-
const argSummary = (0, tools_1.summarizeArgs)(tc.name, args);
|
|
2346
|
-
const toolLoop = (0, tool_loop_1.detectToolLoop)(toolLoopState, tc.name, args);
|
|
2347
|
-
if (toolLoop.stuck) {
|
|
2348
|
-
const rejection = `loop guard: ${toolLoop.message}`;
|
|
2349
|
-
callbacks.onToolStart(tc.name, args);
|
|
2350
|
-
callbacks.onToolEnd(tc.name, argSummary, false);
|
|
2351
|
-
pushGenerated({ role: "tool", tool_call_id: tc.id, content: rejection });
|
|
2352
|
-
providerRuntime.appendToolOutput(tc.id, rejection);
|
|
2353
|
-
continue;
|
|
2354
|
-
}
|
|
2355
2320
|
callbacks.onToolStart(tc.name, args);
|
|
2356
2321
|
let toolResult;
|
|
2357
2322
|
let success;
|
|
@@ -2367,8 +2332,12 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
2367
2332
|
catch (e) {
|
|
2368
2333
|
toolResult = `error: ${e}`;
|
|
2369
2334
|
success = false;
|
|
2370
|
-
augmentedToolContext?.habitSession?.recordError?.(toolResult);
|
|
2371
2335
|
}
|
|
2336
|
+
const modelResult = (0, tool_friction_1.rewriteToolResultForModel)(tc.name, toolResult, toolFrictionLedger);
|
|
2337
|
+
pushGenerated({ role: "tool", tool_call_id: tc.id, content: modelResult });
|
|
2338
|
+
providerRuntime.appendToolOutput(tc.id, modelResult);
|
|
2339
|
+
if (!success)
|
|
2340
|
+
augmentedToolContext?.habitSession?.recordError?.(toolResult);
|
|
2372
2341
|
const validatedRequiredResult = success && requiredToolCallNames.includes(tc.name) && options?.requiredToolCalls?.requireSuccessfulResults
|
|
2373
2342
|
? requiredToolResultSucceeded(tc.name, toolResult, args, options.requiredToolCalls.validateRequiredToolResult)
|
|
2374
2343
|
: false;
|
|
@@ -2395,20 +2364,17 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
2395
2364
|
invoked: true,
|
|
2396
2365
|
sideEffect: success && toolRiskProfile?.mutates !== "none",
|
|
2397
2366
|
});
|
|
2398
|
-
|
|
2399
|
-
(0,
|
|
2400
|
-
callbacks.
|
|
2401
|
-
pushGenerated({ role: "tool", tool_call_id: tc.id, content: toolResult });
|
|
2402
|
-
providerRuntime.appendToolOutput(tc.id, toolResult);
|
|
2403
|
-
callbacks.onToolResult?.(messagesWithoutRequiredCorrections());
|
|
2367
|
+
(0, tool_loop_1.recordToolOutcome)(toolLoopState, tc.name, args, modelResult, success);
|
|
2368
|
+
callbacks.onToolEnd(tc.name, (0, tools_1.buildToolResultSummary)(tc.name, args, modelResult, success), success);
|
|
2369
|
+
callbacks.onToolResult?.(messages);
|
|
2404
2370
|
}
|
|
2405
2371
|
}
|
|
2406
2372
|
}
|
|
2407
2373
|
catch (e) {
|
|
2408
2374
|
// Abort is not an error — just stop cleanly
|
|
2409
2375
|
if (e instanceof provider_attempt_1.ProviderAttemptAbortError || signal?.aborted) {
|
|
2410
|
-
|
|
2411
|
-
|
|
2376
|
+
if (generatedMessages.length === 0)
|
|
2377
|
+
stripLastToolCalls(messages);
|
|
2412
2378
|
outcome = "aborted";
|
|
2413
2379
|
break;
|
|
2414
2380
|
}
|
|
@@ -2426,7 +2392,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
2426
2392
|
}
|
|
2427
2393
|
}
|
|
2428
2394
|
finally {
|
|
2429
|
-
|
|
2395
|
+
nextAttemptControls = [];
|
|
2396
|
+
rejectedAttempt = [];
|
|
2397
|
+
providerRuntime.resetTurnState(messages);
|
|
2430
2398
|
}
|
|
2431
2399
|
options?.captureGeneratedMessages?.(structuredClone(generatedMessages));
|
|
2432
2400
|
(0, runtime_1.emitNervesEvent)({
|