mirror of
https://github.com/Darkatse/TauriTavern.git
synced 2026-10-19 15:45:12 +00:00
fix(agent): fail fast /trigger Agent routing errors
This commit is contained in:
parent
2e03515b97
commit
ea3f5122ce
5 changed files with 70 additions and 33 deletions
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
本文档是 Agent Host ABI 当前参考。它描述前端/扩展可见的稳定入口,不等同于 Rust 内部 service/repository。
|
||||
|
||||
状态:当前已实现 canonical model IR、provider native metadata 保真、provider_state continuation、上下文只读工具、Skill tools、workspace 读改工具循环与前端 dryRun adapter。Agent System 扩展开关开启时,普通发送、regenerate 与 overswipe 新候选生成会通过 Legacy Generate 兼容桥启动 Agent;普通切换已有 swipe 候选不启动 Agent。本文以当前已落地 Host ABI 为准,并在后续章节保留 readDiff/rollback/approval/listRuns 等未来设计。
|
||||
状态:当前已实现 canonical model IR、provider native metadata 保真、provider_state continuation、上下文只读工具、Skill tools、workspace 读改工具循环与前端 dryRun adapter。Agent System 扩展开关开启时,普通发送、`/trigger`、regenerate 与 overswipe 新候选生成会通过 Legacy Generate 兼容桥启动 Agent;普通切换已有 swipe 候选不启动 Agent。本文以当前已落地 Host ABI 为准,并在后续章节保留 readDiff/rollback/approval/listRuns 等未来设计。
|
||||
|
||||
`provider_state` 是 Rust 后端内部 continuation contract,不是 Host ABI。前端/扩展不应读写 `_tauritavern_provider_state`;需要诊断时通过 run events、`modelResponsePath` 与 LLM API log 观察。
|
||||
模型回合详情必须通过 `readModelTurn()` 读取后端投影 DTO;前端不解析 `model-responses/` raw JSON。
|
||||
|
|
@ -76,6 +76,7 @@ type AgentStartRunFromLegacyGenerateInput = {
|
|||
- `worldInfoActivation` 必须来自本次 dryRun 的最终 `WORLDINFO_SCAN_DONE`,不能读取全局 last activation 当作 run 真相。
|
||||
- `stream` 必须为 `false` 或省略;`presentation` 可显式覆盖 profile 默认前台/后台语义。
|
||||
- dryRun 没有产出 messages、已有 tool turns、已有 external tools 都必须 reject,不回退 Legacy Generate。
|
||||
- 入口路由到 Agent 后,profile、provider、group chat、context policy 或 Host API 错误必须 reject 并显式呈现;不得静默降级为 Legacy Generate。`/trigger` 作为生成入口遵守同一规则。
|
||||
|
||||
注意:`Generate(..., dryRun = true)` 不返回 payload。它只 emit `GENERATE_AFTER_DATA`,然后 resolve `undefined`。调用方不应写 `const payload = await Generate(..., true)`;捕获逻辑由 `startRunFromLegacyGenerate()` 内部 adapter 负责。
|
||||
|
||||
|
|
|
|||
|
|
@ -230,6 +230,7 @@ run_failed
|
|||
|
||||
- 恢复成功 → run 继续,不会发 `run_rollback_targets`,也不会写 `run_failed`
|
||||
- 恢复失败(模型再次返回 0 tool_calls)→ 若没有成功 chat commit,写 `run_failed`(`userRetryable=true`);若已有成功 chat commit,写 `run_partial_success` 并保留输出
|
||||
- 上述 rollback / partial-success 语义与前端入口无关;普通发送、`/trigger`、regenerate 与 overswipe 只要进入 Agent run,都必须遵守同一 journal 与 host commit 契约。
|
||||
|
||||
### 4.2 Workspace
|
||||
|
||||
|
|
|
|||
|
|
@ -359,7 +359,9 @@ Workspace path 必须是相对路径。绝对路径、Windows drive prefix、NUL
|
|||
|
||||
## 当前手动测试入口
|
||||
|
||||
Agent System 扩展已在输入栏提供 Agent Mode toggle。开启后,普通发送、regenerate 菜单与右划 overswipe 生成新候选会走 Agent;普通切换已有 swipe 候选仍保持 Legacy swipe 行为。
|
||||
Agent System 扩展已在输入栏提供 Agent Mode toggle。开启后,普通发送、`/trigger`、regenerate 菜单与右划 overswipe 生成新候选会走 Agent;普通切换已有 swipe 候选仍保持 Legacy swipe 行为。
|
||||
|
||||
`/trigger` 仍保持 SillyTavern 的 `normal` generation 语义,不新增 generation type;但 Agent 路由错误必须 fail-fast,不得回退 Legacy Generate。
|
||||
|
||||
前端控制台入口:
|
||||
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashComma
|
|||
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
|
||||
import { getActiveIosPolicyCapabilities } from './tauritavern/ios-policy.js';
|
||||
import { getAgentGenerationOptions } from './tauritavern/agent/agent-generation-router.js';
|
||||
import { agentErrorMessage } from './tauritavern/agent/agent-error-presenter.js';
|
||||
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
|
||||
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
|
||||
|
|
@ -4480,45 +4481,60 @@ async function addGroupMemberCallback(_, name) {
|
|||
|
||||
async function triggerGenerationCallback(args, value) {
|
||||
const shouldAwait = isTrueBoolean(args?.await);
|
||||
const outerPromise = new Promise((outerResolve) => setTimeout(async () => {
|
||||
try {
|
||||
await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
|
||||
} catch {
|
||||
console.warn('Timeout waiting for generation unlock');
|
||||
toastr.warning(t`Cannot run /trigger command while the reply is being generated.`);
|
||||
outerResolve(Promise.resolve(''));
|
||||
return '';
|
||||
const generationPromise = runTriggeredGeneration(value);
|
||||
|
||||
if (shouldAwait) {
|
||||
await generationPromise;
|
||||
} else {
|
||||
void generationPromise.catch((error) => {
|
||||
console.error('Error running /trigger command', error);
|
||||
queueMicrotask(() => {
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
async function runTriggeredGeneration(value) {
|
||||
await delay(1);
|
||||
try {
|
||||
await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
|
||||
} catch {
|
||||
console.warn('Timeout waiting for generation unlock');
|
||||
toastr.warning(t`Cannot run /trigger command while the reply is being generated.`);
|
||||
return '';
|
||||
}
|
||||
|
||||
// Prevent generate recursion
|
||||
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
|
||||
|
||||
let chid = undefined;
|
||||
|
||||
if (selected_group && value) {
|
||||
chid = findGroupMemberId(value);
|
||||
|
||||
if (chid === undefined) {
|
||||
console.warn(`WARN: No group member found for argument ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent generate recursion
|
||||
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
|
||||
|
||||
let chid = undefined;
|
||||
|
||||
if (selected_group && value) {
|
||||
chid = findGroupMemberId(value);
|
||||
|
||||
if (chid === undefined) {
|
||||
console.warn(`WARN: No group member found for argument ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
const agentOptions = await getAgentGenerationOptions({
|
||||
let agentOptions;
|
||||
try {
|
||||
agentOptions = await getAgentGenerationOptions({
|
||||
generationType: 'normal',
|
||||
isSlashCommand: false,
|
||||
mainApi: main_api,
|
||||
selectedGroup: selected_group,
|
||||
}).catch(() => ({}));
|
||||
|
||||
outerResolve(new Promise(innerResolve => setTimeout(() => innerResolve(Generate('normal', { force_chid: chid, ...agentOptions })), 100)));
|
||||
}, 1));
|
||||
|
||||
if (shouldAwait) {
|
||||
const innerPromise = await outerPromise;
|
||||
await innerPromise;
|
||||
});
|
||||
} catch (error) {
|
||||
toastr.error(agentErrorMessage(error), t`Agent Mode`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return '';
|
||||
await delay(100);
|
||||
return Generate('normal', { force_chid: chid, ...agentOptions });
|
||||
}
|
||||
/**
|
||||
* Find persona by name.
|
||||
|
|
|
|||
|
|
@ -201,6 +201,23 @@ test('Agent generation router uses the global toggle for normal regenerate and s
|
|||
);
|
||||
});
|
||||
|
||||
test('/trigger routes Agent generation fail-fast without Legacy fallback', async () => {
|
||||
const source = await readFile(path.join(REPO_ROOT, 'src/scripts/slash-commands.js'), 'utf8');
|
||||
const start = source.indexOf('async function triggerGenerationCallback');
|
||||
const end = source.indexOf('/**\n * Find persona by name.', start);
|
||||
assert.ok(start >= 0 && end > start, 'triggerGenerationCallback section must be present');
|
||||
|
||||
const section = source.slice(start, end);
|
||||
assert.match(section, /runTriggeredGeneration/);
|
||||
assert.match(section, /getAgentGenerationOptions\(\{\s*generationType: 'normal',\s*isSlashCommand: false,\s*mainApi: main_api,\s*selectedGroup: selected_group,\s*\}\)/s);
|
||||
assert.match(section, /toastr\.error\(agentErrorMessage\(error\), t`Agent Mode`\)/);
|
||||
assert.match(section, /return Generate\('normal', \{ force_chid: chid, \.\.\.agentOptions \}\)/);
|
||||
assert.doesNotMatch(section, /\.catch\(\(\) => \(\{\}\)\)/);
|
||||
|
||||
const routeCall = section.slice(section.indexOf('getAgentGenerationOptions'));
|
||||
assert.doesNotMatch(routeCall, /getAgentGenerationOptions[\s\S]*?\.catch\s*\(/);
|
||||
});
|
||||
|
||||
test('Agent System confirmations use SillyTavern Popup instead of window.confirm', async () => {
|
||||
const calls = [];
|
||||
installWindow({});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue