fix(teacher): 合并续接工具活动并修正暂停状态
This commit is contained in:
@@ -12,16 +12,32 @@
|
||||
|
||||
## Scope
|
||||
|
||||
- 2026-09-27 follow-up also covers the confirmed duplicate tool-activity rows and false unfinished states across native interrupted/resumed Runs. Keep local reading, student charging and quota unchanged.
|
||||
|
||||
- Resumed 2026-09-27 for the continuing expired-read screenshot. First verify the actual installed runtime, request time and rollout boundary; make no additional product changes without a new reproduced defect.
|
||||
|
||||
- Diagnose the reported read-request-expired failure after 48 tool activities in installed Makelore 1.6.8. Establish the actual trigger and a red regression before changing the responsible boundary.
|
||||
|
||||
## Intent And Constraints
|
||||
|
||||
- Project Context Loaded (same-task continuation): feature ownership/branch/worktree/base unchanged and official touch succeeded. Entry, active record, integrated teacher architecture/domain and evidence, current AGENTS and source contracts reused; peer packaging 1.6.9 is complete, integration and historical peers stay read-only. Product goal remains source-grounded readonly cloud consultation; relevant decision is native checkpoint continuation with six read batches. Main cloud-runner/cloud-activity and focused tests own this correction. No conflicting peer semantic decision identified; production worker revision is unknown. Planning Gate: Passed. Plan: replay typed interruption/resume events, assert one row per logical tool call and actual local outcome, fix identity/error projection, then targeted tests/typecheck/build.
|
||||
|
||||
- Same-task resume through official check/start/status passed. Required memory context is reused with refreshed own/peer records; 1.6.9 packaging is complete but explicitly excludes installation. No new subagent consent, installation, restart or production deployment is implied by this diagnostic step.
|
||||
|
||||
- Concurrent and Planning Gates Passed through official check/start/status in this isolated feature worktree. Source base matches current main; entry, own record, AGENTS, relevant teacher decisions/domain/architecture/evidence and recent peer scopes were loaded, with unchanged earlier context reused. Packaging 1.6.8 is a separate completed scope. Historical placeholder peers have no concrete dependency here.
|
||||
- Installed 1.6.8 contains the prior run_busy retry. Its expired-read guard currently conflates context mismatch, malformed/oversized calls and the six-round limit. Trigger still needs runtime evidence; do not presume expiry from the UI count alone.
|
||||
- One fresh read-only reviewer explicitly authorized by the user. No additional subagents, primary/peer writes, credentials in evidence, live paid calls, deployment, unapproved cleanup or silent change to the accepted six-batch contract. Plan: inspect bounded local request metadata, construct a red test, repair the demonstrated cause, run targeted validation and document remaining rollout limits.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Follow-up correction: activity IDs now use this question plus logical tool_call_id, so native continuation Runs share one row. Raw tool-error does not establish failure because LangGraph emits it for interrupts; actual local results, finished tool results and failed/cancelled question termination own the visible state. Pending rows close as unfinished if the question stops. File execution, read quota, billing and answer projection are unchanged. Existing saved history is not rewritten.
|
||||
|
||||
- 08:52 +08:00: user independently upgraded to installed/running 1.6.9. The new failed request contains 36 rows but 19 distinct tool_call_id values: 17 pairs of failed/completed, plus two uncompleted seventh-batch calls. Current client update is no longer a blocker; the following 08:15 observations describe the previous installation. No tool arguments/results are retained locally, so identical path/range repetition cannot be concluded.
|
||||
|
||||
- 2026-09-27 recurrence: the actual running executable and installed app.asar remain version 1.6.8, written 2026-09-26 16:43 +08:00, with processes started 17:19. Direct read of the installed bundled guard confirms context/call validation and ++rounds > 6 still share teacher_context_expired; teacher_read_limit is absent.
|
||||
- The latest local project request was created 2026-09-27 08:15:13 +08:00 and failed with the reported message. Its 52 activities span seven Run groups of 1/4/7/12/13/8/7, including repeated call activity. This is a new request on the old installed runtime, not evidence that the repaired installed runtime failed.
|
||||
- Packaging task 20260926-package-169-e5b8 built 1.6.9 from integrated main 5d24a219aad6b2f2dd40d6d9c1ad17403124cf31. The 208666838-byte installer exists and its unpacked archive contains teacher_read_limit. Packaging explicitly did not install. No new product defect or source patch is established by this recurrence; the previous source was merged into main after its original handoff.
|
||||
|
||||
- Installed 1.6.8 includes the previous run_busy retry. Read-only history inspection found 48 activities across seven cloud Runs but 25 unique tool-call IDs; prior batches reappear in continuation Runs. The first six batches have later completed results and a seventh batch of new calls appears before the local error. Local history does not retain the cloud model request body, so the exact production provider/deployment cause remains unverified.
|
||||
- A deterministic valid-context seventh-batch replay produced the exact reported expired-context error. Main now retains the six-batch protection and cancellation but distinguishes teacher_read_limit, teacher_context_expired and teacher_protocol_invalid; six-batch completion returns the actual answer. README documents this behavior.
|
||||
- The paired Yuxi task 20260926-teacher-read-round-yx-8be372c1 adds explicit tool_choice none for final-answer calls after consuming the sixth batch. Its real HTTP/PG/Redis/worker/SDK test reproduces a seventh interrupt when the fixture provider continues historical tool names without explicit none, and completes after the fix. A cooperative-provider six-batch control passed before the fix; the round counter itself is not proven broken.
|
||||
@@ -29,6 +45,11 @@
|
||||
|
||||
## Verification
|
||||
|
||||
- Follow-up red: pnpm exec vitest run tests/unit/coding-teacher-cloud.test.ts -t "one .* local read|pending tool as failed" --maxWorkers=1 failed all three cases before the fix. A successful local read produced before:file failed and after:file completed, reproducing the screenshot.
|
||||
- Follow-up green: coding-teacher-cloud.test.ts and teacher-cloud-activity.test.ts: 47 passed. Success and genuine local read failure each retain one call; bare tool errors await the question outcome; terminal failure still closes pending tools. pnpm run typecheck and pnpm run build:vite passed (existing Browserslist/chunk/import warnings). No new E2E fixture was added: existing layout fixture replaces Host API and cannot reach Main cloud-runner continuation; the regression exercises the actual runner and file service with deterministic cloud events. No installed acceptance, live provider call or source deployment performed.
|
||||
|
||||
- Recurrence read-only verification: executable version/timestamps and live process path; exact installed archive guard; latest request status/time and aggregate tool-activity metadata; new installer existence and packaged read-limit marker. No new hashes, secrets, raw user messages, tool arguments or production payloads recorded.
|
||||
|
||||
- Red: pnpm exec vitest run tests/unit/coding-teacher-cloud.test.ts -t 'exhausted read rounds' --maxWorkers=1: failed with teacher_context_expired on a valid seventh batch before the guard split.
|
||||
- Green: pnpm exec vitest run tests/unit/coding-teacher-cloud.test.ts tests/unit/coding-teacher-read-tools.test.ts tests/unit/coding-teacher.test.ts --maxWorkers=1: 130 passed. Corrected table rows to wrap empty/oversized arrays as objects; focused malformed-batch replay: 3 passed.
|
||||
- pnpm run typecheck and pnpm run build:vite: passed. Build warnings concern existing chunk sizes, mixed imports and Browserslist age.
|
||||
@@ -36,9 +57,14 @@
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- Deploy the paired Yuxi worker fix and update the client error projection before live acceptance. Existing failed questions are not automatically replayed or billed.
|
||||
- Tool activity presentation currently counts the same call per Run and can retain interruption-related failed status before a continuation completes; report this observed presentation issue separately. It was not changed in this bounded completion repair.
|
||||
- Client follow-up patch is ready for integration; it is not merged, packaged or installed. Current installed 1.6.9 predates this activity fix. Existing failed history is retained rather than rewritten.
|
||||
- Paired Yuxi completion diagnosis remains blocked on actual API/worker revision or production request evidence. Public health returns only package version 0.7.3, not commit or worker revision; local Kubernetes has no current-context. The prior question asking whether 3a278ff was deployed remains unanswered. Do not attribute the new failure to an outdated client: user independently installed 1.6.9 before the 08:52 request.
|
||||
- WS read-only task 20260927-teacher-read-gateway-41acb598 confirms model-session returns One API base URL and short authorization; Yuxi sends the model body directly to that relay. No WS model-body rewriting is present on this path. Actual relay/provider payload remains unobserved.
|
||||
- At 08:52, 17 distinct first-six-batch calls have completed continuation event records; two additional calls were stopped by the client quota. Local history lacks parameters and actual result content: a successful cloud ToolMessage can wrap a local read error, so neither repeated paths/ranges nor successful file reads can be inferred from those labels alone. No blind quota increase or duplicate-read caching added.
|
||||
- No new subagents were created. Prior Yuxi reviewer approval was already consumed; Yuxi product code has not changed in this follow-up.
|
||||
|
||||
## Promotion Candidates
|
||||
|
||||
- Target: consultation activity contract in integrated state/system overview. Proposal: one question plus tool_call_id identifies a logical tool through checkpoint continuation; raw tool-error is provisional, not proof of failure. Evidence: 36 stored rows = 19 call IDs (17 failed/completed pairs) and red/green runner regression. Impact: tool counts represent calls and successful reads no longer appear unfinished. No semantic conflict; no new product-direction approval.
|
||||
|
||||
- Target: integrated consultation runtime boundary. Proposal: six valid local batches still complete normally, while protocol-invalid batches, stale context and exhausted read quota have separate error semantics. Evidence: deterministic red/green and paired worker test. Impact: future diagnosis must not infer context expiry from quota exhaustion or UI tool count. No semantic conflict or additional product-direction approval needed.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Distinguish logical tool calls from Run events
|
||||
|
||||
A teacher question showed 36 tool rows, but local history contained 19 unique tool_call_id values. Seventeen resumed calls each had an interruption row marked failed and a completed row under the continuation Run. Counting UI rows as executions overstates work and obscures the real seventh-batch failure.
|
||||
|
||||
When diagnosing a durable interrupt/resume chain, compare request, Run and tool-call identities separately. A raw tool-error may be a checkpoint control signal. Verify actual result and Run/question termination before reporting failure. Tests that only emit tool starts and finishes within one Run miss this defect; replay the interrupted parent and resumed child while asserting one logical row and the real local result.
|
||||
|
||||
The client regression now covers that sequence. This explains the duplicate display; it does not establish why the production model requests a seventh batch. That still requires deployed-worker/request evidence.
|
||||
@@ -174,7 +174,7 @@ Pi 正式包必须继续运行 `pnpm run verify:artifact:pi`、`pnpm run smoke:p
|
||||
- 智能体讨论采用“上方固定整理内容、下方独立滚动对话、底部原有输入框”的布局。普通回答可带直接发送的引导问题;想法板、结构图、流程/条件图和逐项对照由同一次模型回复提供结构化数据。先邀请学生“用这个一起想”,进入后程序锁定信息结构,智能体随讨论更新同一份内容;解释问题可仅回复文字。节点点击只选择讨论焦点,对照里的“聊聊这一点”直接发问,都保留输入草稿。
|
||||
- 想法板区分已留下、智能体建议和暂放内容,可由学生采纳、暂放、选择先想哪项。“把想法理一理”明确转换为智能体归纳的结构图,并可“回去补充想法”。“只聊天”暂停整理,“先这些”暂时结束并保留未确定内容,“接着改”恢复原工具;这些操作只影响智能体讨论,不创建项目分支、不执行作品修改。状态、版本和历史快照按当前账号/项目/话题保存。
|
||||
- Main 校验结构、引用和内容长度,拒绝未经学生选择的类型变更与旧版本更新。结构化回复在完整校验后一次应用;生成中显示简短处理状态,停止、失败或无效组件保留上一份内容。普通 Markdown 链接、JSON 对象/数组和代码示例保持正文,仅讨论协议的顶层字段或专用围栏进入组件解析。解析失败仍保留完整的可读 reply,并将本次原始回答保存为默认折叠、字面显示的“查看收到的原始内容”,不把它再次加入模型上下文;既有丢失原文的历史不能恢复。
|
||||
- 咨询正文支持 Markdown 标题、列表、表格、代码围栏、HTTP(S) 链接/图片和数学公式,长代码与表格在栏内横向滚动,不执行 HTML。云端正文维持 string 合同,未知对象不会猜测转成回答。工具活动只从云端主线程的类型化工具事件及 Main 本地读取过程获得,按运行/调用身份合并名称和状态,单独折叠显示;参数、结果原文和内部错误不混入回复,思考内容不作为正文。客户端仅保存自己实际收到的工具状态,不能补回旧历史或断线期间已经过期的事件。
|
||||
- 咨询正文支持 Markdown 标题、列表、表格、代码围栏、HTTP(S) 链接/图片和数学公式,长代码与表格在栏内横向滚动,不执行 HTML。云端正文维持 string 合同,未知对象不会猜测转成回答。工具活动只从云端主线程的类型化工具事件及 Main 本地读取过程获得,按本次问题/工具调用身份合并名称和状态,跨云端暂停、续接仍只计一次;暂停读取不算失败,状态以实际读取结果或问题终态为准,单独折叠显示;参数、结果原文和内部错误不混入回复,思考内容不作为正文。客户端仅保存自己实际收到的工具状态,不能补回旧历史或断线期间已经过期的事件。
|
||||
- 云端咨询最多执行六批本地读取。配套 Yuxi 在最后一批结果返回后以禁止工具调用的模型请求生成答案;Main 仍拒绝第七批读取,并分别提示读取达到上限、上下文失效或读取请求格式无效。
|
||||
- 智能体人设、职责、提示词和 Skills 由发布配置决定。Main 不追加固定教学基线、不生成“小麦”、不清空所选智能体的 Skills,只添加真实工具能力、上下文边界和本轮界面协议。智能体名称叫“老师”“朋友”或“代码顾问”不改变调用路径或权限。
|
||||
- 每轮格式由 Main 的对应意图协议决定;`discussion.ts` 为支持组件的请求注入唯一 `{reply, quickReplies, tool}` 协议。运营教学补充不另写字段协议或要求始终纯文字。工具内讨论保留类型、稳定 ID、未修改内容和采纳状态;暂停/未进入时 `tool:null`,没有实质变化时也可保留原内容。结构图、流程和对照目前没有独立的采纳/来源字段,待定、建议与预测只能在展示文字中明确,不能据此推导已确认共识。
|
||||
|
||||
@@ -11,22 +11,24 @@ function object(value: unknown): Record<string, unknown> | undefined {
|
||||
}
|
||||
|
||||
/** Project Yuxi's typed tool events; arguments, outputs and internal errors never enter the UI. */
|
||||
export function cloudToolActivity(chunk: Record<string, unknown>, runId: string): ToolActivityUpdate | undefined {
|
||||
export function cloudToolActivity(chunk: Record<string, unknown>, requestId: string): ToolActivityUpdate | undefined {
|
||||
const event = object(chunk.stream_event);
|
||||
if (event?.type === 'tool_call' || event?.type === 'tool_call_delta') {
|
||||
if (typeof event.tool_call_id !== 'string' || !event.tool_call_id || typeof event.name !== 'string' || !event.name) return;
|
||||
return { id: runId + ':' + event.tool_call_id, name: event.name, status: 'running' };
|
||||
return { id: requestId + ':' + event.tool_call_id, name: event.name, status: 'running' };
|
||||
}
|
||||
const custom = object(chunk.event);
|
||||
if (chunk.status !== 'stream_event' || custom?.method !== 'tools') return;
|
||||
const data = object(custom.data);
|
||||
if (!data || typeof data.tool_call_id !== 'string' || !data.tool_call_id) return;
|
||||
if (data.event !== 'tool-started' && data.event !== 'tool-finished' && data.event !== 'tool-error') return;
|
||||
// A raw tool-error can be a checkpoint interrupt. Only a tool result or the
|
||||
// question's terminal state establishes failure, as in Yuxi's tool audit.
|
||||
if (data.event !== 'tool-started' && data.event !== 'tool-finished') return;
|
||||
const output = object(data.output);
|
||||
return {
|
||||
id: runId + ':' + data.tool_call_id,
|
||||
id: requestId + ':' + data.tool_call_id,
|
||||
...(typeof data.tool_name === 'string' && data.tool_name ? { name: data.tool_name } : {}),
|
||||
status: data.event === 'tool-started' ? 'running'
|
||||
: data.event === 'tool-error' || data.error || output?.status === 'error' || output?.status === 'failed' ? 'failed' : 'completed',
|
||||
: data.error || output?.status === 'error' || output?.status === 'failed' ? 'failed' : 'completed',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -320,13 +320,13 @@ export function prepareCloudTeacher(
|
||||
const call = object(raw);
|
||||
if (typeof call.tool_call_id !== 'string' || typeof call.name !== 'string')
|
||||
throw new TeacherError(502, 'teacher_protocol_invalid', '智能体读取请求无效。');
|
||||
reportActivity({ id: runId + ':' + call.tool_call_id, name: call.name, status: 'running' });
|
||||
reportActivity({ id: requestId + ':' + call.tool_call_id, name: call.name, status: 'running' });
|
||||
const result = await tools.executeResult(call.name, JSON.stringify(call.arguments), bounded);
|
||||
results.push({
|
||||
tool_call_id: call.tool_call_id,
|
||||
...result,
|
||||
});
|
||||
reportActivity({ id: runId + ':' + call.tool_call_id, name: call.name, status: result.status === 'success' ? 'completed' : 'failed' });
|
||||
reportActivity({ id: requestId + ':' + call.tool_call_id, name: call.name, status: result.status === 'success' ? 'completed' : 'failed' });
|
||||
}
|
||||
// POST 重试使用完全相同的结果,文件变化也不会导致重复续接或不同输入。
|
||||
const resumed = await transport.json(
|
||||
@@ -383,7 +383,8 @@ export function prepareCloudTeacher(
|
||||
? [payload.chunk]
|
||||
: []) {
|
||||
const chunk = object(item);
|
||||
const activity = cloudToolActivity(chunk, runId);
|
||||
// A checkpoint continuation changes Run, not the logical tool call.
|
||||
const activity = cloudToolActivity(chunk, requestId);
|
||||
if (activity) reportActivity(activity);
|
||||
const event = chunk.stream_event ? object(chunk.stream_event) : {};
|
||||
if (event.type === 'message_delta' && typeof event.content === 'string') {
|
||||
@@ -409,6 +410,9 @@ export function prepareCloudTeacher(
|
||||
throw new TeacherError(408, 'teacher_question_expired', '本次智能体提问已超时,请重新提问。');
|
||||
} finally {
|
||||
if (!completed) {
|
||||
for (const activity of activities.values()) {
|
||||
if (activity.status === 'running') reportActivity({ ...activity, status: 'failed' });
|
||||
}
|
||||
await transport
|
||||
.json(
|
||||
'/questions/' + encodeURIComponent(requestId) + '/cancel',
|
||||
|
||||
@@ -246,12 +246,94 @@ it('merges typed tool activity across replay without adding tool data or child e
|
||||
await prepareCloudTeacher(f.account, f.topic, requestId, f.access, f.progress, f.saveRequest, transport, activity)
|
||||
.run([{ role: 'user', content: '问题' }], new AbortController().signal, text);
|
||||
expect(activity.mock.calls).toEqual([
|
||||
[{ id: 'run:call', name: 'search', status: 'running' }],
|
||||
[{ id: 'run:call', name: 'search', status: 'completed' }],
|
||||
[{ id: requestId + ':call', name: 'search', status: 'running' }],
|
||||
[{ id: requestId + ':call', name: 'search', status: 'completed' }],
|
||||
]);
|
||||
expect(text.mock.calls).toEqual([['最终正文']]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ path: 'src/game.ts', status: 'completed', result: 'success' },
|
||||
{ path: 'src/missing.ts', status: 'failed', result: 'error' },
|
||||
])('keeps one $status local read across interrupted and resumed runs', async scenario => {
|
||||
const f = await fixture();
|
||||
const activity = vi.fn(), text = vi.fn();
|
||||
let before = 0, after = 0;
|
||||
const transport: TeacherCloudTransport = {
|
||||
json: vi.fn(async url => {
|
||||
if (url === '/questions') return { request_id: 'cloud-question', run_id: 'before' };
|
||||
if (url === '/runs/before') return ++before === 1
|
||||
? { status: 'running', thread_id: 'main' }
|
||||
: { status: 'interrupted', interrupt: { source: 'client_read_tools', context_id: requestId,
|
||||
calls: [{ tool_call_id: 'file', name: 'read_project_file', arguments: { path: scenario.path } }],
|
||||
} };
|
||||
if (url === '/runs/before/tool-results') return { run_id: 'after' };
|
||||
if (url === '/runs/after') return ++after === 1
|
||||
? { status: 'running', thread_id: 'main' }
|
||||
: { status: 'completed', output: '基于读取结果回答。' };
|
||||
throw new Error('Unexpected request: ' + url);
|
||||
}),
|
||||
events: vi.fn(async (url, _signal, accept) => {
|
||||
const data = url.includes('/before/')
|
||||
? { event: 'tool-error', tool_call_id: 'file', message: 'Interrupt' }
|
||||
: { event: 'tool-finished', tool_call_id: 'file', output: {
|
||||
type: 'tool', status: 'success', content: JSON.stringify({ status: scenario.result }),
|
||||
} };
|
||||
accept('messages', { thread_id: 'main', payload: { chunk: {
|
||||
stream_event: { type: 'tool_call', tool_call_id: 'file', name: 'read_project_file' },
|
||||
} } }, '1-0');
|
||||
accept('custom', { thread_id: 'main', payload: { chunk: {
|
||||
status: 'stream_event', event: { method: 'tools', data },
|
||||
} } }, '2-0');
|
||||
}),
|
||||
};
|
||||
await prepareCloudTeacher(f.account, f.topic, requestId, f.access, f.progress, f.saveRequest, transport, activity)
|
||||
.run([{ role: 'user', content: '检查项目' }], new AbortController().signal, text);
|
||||
expect(activity.mock.calls).toEqual([
|
||||
[{ id: requestId + ':file', name: 'read_project_file', status: 'running' }],
|
||||
[{ id: requestId + ':file', name: 'read_project_file', status: scenario.status }],
|
||||
]);
|
||||
expect(vi.mocked(transport.json).mock.calls.filter(([url]) => url.endsWith('/tool-results'))).toHaveLength(1);
|
||||
expect(transport.json).toHaveBeenCalledWith('/runs/before/tool-results', {
|
||||
context_id: requestId,
|
||||
results: [expect.objectContaining({ tool_call_id: 'file', status: scenario.result })],
|
||||
}, expect.anything());
|
||||
expect(text.mock.calls).toEqual([['基于读取结果回答。']]);
|
||||
});
|
||||
|
||||
it('closes a pending tool as failed when its question fails after a raw tool error', async () => {
|
||||
const f = await fixture();
|
||||
const activity = vi.fn();
|
||||
let activityBeforeFailure: unknown;
|
||||
let reads = 0;
|
||||
const transport: TeacherCloudTransport = {
|
||||
json: vi.fn(async url => {
|
||||
if (url === '/questions') return { request_id: 'cloud-question', run_id: 'run' };
|
||||
if (url.endsWith('/cancel')) return { status: 'cancelled' };
|
||||
return ++reads === 1 ? { status: 'running', thread_id: 'main' }
|
||||
: { status: 'failed', error: { message: '工具执行失败' } };
|
||||
}),
|
||||
events: vi.fn(async (_url, _signal, accept) => {
|
||||
for (const data of [
|
||||
{ event: 'tool-started', tool_call_id: 'search', tool_name: 'search' },
|
||||
{ event: 'tool-error', tool_call_id: 'search', message: 'provider unavailable' },
|
||||
]) accept('custom', { thread_id: 'main', payload: { chunk: {
|
||||
status: 'stream_event', event: { method: 'tools', data },
|
||||
} } }, '1-0');
|
||||
activityBeforeFailure = structuredClone(activity.mock.calls);
|
||||
}),
|
||||
};
|
||||
await expect(prepareCloudTeacher(f.account, f.topic, requestId, f.access, f.progress, f.saveRequest, transport, activity)
|
||||
.run([{ role: 'user', content: '搜索' }], new AbortController().signal, vi.fn()))
|
||||
.rejects.toMatchObject({ code: 'teacher_run_failed' });
|
||||
expect(activityBeforeFailure).toEqual([
|
||||
[{ id: requestId + ':search', name: 'search', status: 'running' }],
|
||||
]);
|
||||
expect(activity.mock.calls.at(-1)).toEqual([
|
||||
{ id: requestId + ':search', name: 'search', status: 'failed' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('submits the active discussion protocol, current tool content and selected focus', async () => {
|
||||
const f = await fixture();
|
||||
f.topic.definition.limits.max_input_tokens = 16000;
|
||||
@@ -677,8 +759,8 @@ it.each(['suggestions', 'discussion-v1'] as const)(
|
||||
).run([{ role: 'user', content: '一起讨论' }], new AbortController().signal, text);
|
||||
expect(text.mock.calls).toEqual([[finalOutput]]);
|
||||
expect(activity.mock.calls).toEqual([
|
||||
[{ id: 'before-read:file', name: 'read_project_file', status: 'running' }],
|
||||
[{ id: 'before-read:file', name: 'read_project_file', status: 'completed' }],
|
||||
[{ id: requestId + ':file', name: 'read_project_file', status: 'running' }],
|
||||
[{ id: requestId + ':file', name: 'read_project_file', status: 'completed' }],
|
||||
]);
|
||||
const response = text.mock.calls.map(([delta]) => delta).join('');
|
||||
expect(format === 'suggestions'
|
||||
|
||||
@@ -29,10 +29,14 @@ describe('teacher cloud tool activity projection', () => {
|
||||
});
|
||||
it.each([
|
||||
['tool-finished', { type: 'tool', content: 'tool result', status: 'success' }, undefined, 'completed'],
|
||||
['tool-error', undefined, 'private error', 'failed'],
|
||||
])('accepts real %s events that carry only the call id', (event, output, error, status) => {
|
||||
expect(cloudToolActivity({ status: 'stream_event', event: { method: 'tools', data: {
|
||||
event, tool_call_id: 'call', output, error,
|
||||
} } }, 'run')).toEqual({ id: 'run:call', status });
|
||||
});
|
||||
it('waits for the outcome instead of treating a raw tool error as failure', () => {
|
||||
expect(cloudToolActivity({ status: 'stream_event', event: { method: 'tools', data: {
|
||||
event: 'tool-error', tool_call_id: 'call', message: 'Interrupt',
|
||||
} } }, 'question')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user