Visitar URL original
feat(script): 内容确认即转为正式脚本,提示词编写只填待编写分镜(Spec #2503 stage 1) by Pollo3470 · Pull Request #2531 · ArcReel/ArcReel · GitHub
Skip to content

feat(script): 内容确认即转为正式脚本,提示词编写只填待编写分镜(Spec #2503 stage 1) - #2531

Merged
Pollo3470 merged 7 commits into
mainfrom
afk/spec-2503-20260917-044955-c242db/stage-1
Sep 17, 2026
Merged

Pollo3470 merged 7 commits into
mainfrom
afk/spec-2503-20260917-044955-c242db/stage-1

Conversation

@Pollo3470

@Pollo3470 Pollo3470 commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

概要

Spec #2503 的 stage 1(expand 步):正式脚本成为提示词编写后该集唯一的内容真相,脚本规划在内容确认时一次性转为正式脚本并退出。本 stage 只新增机制、不删指纹与条目时效(contract 步归 stage 2:#2509 / #2510 / #2511)。

按 issue 的 commit(rebase merge 保留):

已知过渡态(stage 2 收口)

验证

每个 issue commit 由独立 local reviewer 审查,并在各自 worktree 跑完整后端闸门(ruff / format / basedpyright / lint-imports / deptry / pytest 全量)、audit_tests.py --check 与 frontend pnpm check。累计质量门与 AI review loop 见后续 integration-fix commit。

Closes #2504
Closes #2505
Closes #2506
Closes #2507
Closes #2508
Refs #2503

Summary by CodeRabbit

  • 新功能

    • 确认脚本规划后,将整集转换为正式脚本,并在确认后锁定为只读。
    • 已有正式脚本时,支持危险确认覆盖,并展示将移除的分镜及图片、视频数量。
    • 新增“待编写”状态,可自动补全待处理的分镜或视频单元。
    • 分镜详情新增旁白正文、对应原文只读展示及时间线入口。
  • 改进

    • 支持编辑旁白正文,并校验对应原文必须来自源文的逐字片段。
    • 提示词编写默认处理全部待编写条目,也支持指定条目重写。

@coderabbitai

coderabbitai Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Review Change StackReview Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

本次改动将内容确认改为整集物化正式脚本,并以正式脚本作为提示词编写输入。新增 pending_authoring 标记、覆盖确认、确认后只读、源文校验、旁白正文编辑和对应原文展示。

Changes

脚本确认与正式脚本物化

Layer / File(s) Summary
确认流程与覆盖保护
server/services/script_review.py, server/routers/script_review.py, server/remote_mcp.py, server/text_generation.py
确认时将整份脚本规划写入正式脚本。已有正式脚本时要求匹配的 overwrite_revision,否则返回覆盖清单和 409。
正式脚本状态模型
lib/script_models.py, lib/script_plan_entries.py, lib/script_review.py, lib/project_manager.py
新增 pending_authoring、source_text 和 ScriptOverwrite 数据。正式脚本覆盖时同步撤销被替换资源登记。
确认后只读保护
server/draft_workflow.py, server/routers/files.py, server/routers/_script_review_errors.py
已确认的脚本规划不能保存、补丁修改或晋升。重跑脚本规划后恢复可编辑。

提示词编写与待编写条目

Layer / File(s) Summary
正式脚本提示词编写
server/text_generation.py, server/tool_runtime.py, lib/workflow_state.py, lib/prompt_builders_ad.py
默认编写正式脚本中所有 pending_authoring 条目。entry_ids 用于显式重写。scope 被移除,传入后返回迁移错误。
多内容模式支持
lib/script_models.py, lib/prompt_templates/templates/text/*authoring.md, agent_runtime_profile/.claude/skills/generate-script/SKILL.md
narration、drama、ad 和 reference_video 使用对应视觉层或正文 schema。广告新增分镜和参考生视频提示词模板。
条目标记维护
lib/script_batch_edit.py, lib/script_editor.py, lib/data_validator.py
新增条目在视觉层不完整时置位标记。视觉层写完整后清除标记。直接 patch 不能修改标记。待编写广告分镜可暂时缺少视觉提示词。

前端确认与时间线编辑

Layer / File(s) Summary
确认面板与覆盖对话框
frontend/src/components/canvas/timeline/ScriptReviewGate.tsx, frontend/src/components/canvas/reference/ReferenceScriptPlanPreviewPanel.tsx, frontend/src/components/shared/ScriptOverwriteConfirmDialog.tsx
确认后的内容进入只读态。已有正式脚本时显示危险覆盖按钮、覆盖清单和素材数量。确认后提供时间线入口。
时间线内容字段
frontend/src/components/canvas/timeline/ShotDetail.tsx, frontend/src/components/shared/SourceTextReadonly.tsx, frontend/src/components/canvas/reference/ReferenceVideoCard.tsx
narration 支持编辑 novel_text。drama 和 reference_video 展示只读 source_text。待编写条目显示状态提示。
前端 API 与状态同步
frontend/src/api.ts, frontend/src/hooks/useScriptReviewDraft.ts, frontend/src/types/script.ts
确认 API 支持 overwriteRevision。客户端处理覆盖冲突和确认后只读状态。

验证覆盖

Layer / File(s) Summary
生成器与工作流测试
tests/integration/lib/test_script_generator*.py, tests/integration/lib/test_workflow_state.py
测试正式脚本转换、待编写条目选择、广告路径、参考生视频路径和工作流动作。
服务与路由测试
tests/integration/server/services/test_script_review.py, tests/integration/server/routers/test_script_review_router.py, tests/integration/server/agent_runtime/sdk_tools/*
测试整集物化、覆盖认可、确认后只读、草稿保护、源文校验和工具契约。
前端与模型测试
frontend/src/components/**/*.test.tsx, tests/unit/lib/*
测试覆盖对话框、只读控件、源文展示、旁白正文、待编写标记和 schema 约束。

Priority: ➖ Normal

Estimated code review effort: 5 (Critical) | ~120 minutes

Change: Feature

Merge Risk: 🟡 Moderate · up to 2b000

旧工作流或升级前草稿仍可能修改错误的脚本内容,非默认脚本绑定也可能绕过覆盖确认。建议修复这些路径后再合并。

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.68% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 142 functions across 50 files. (36 skippe… Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive 现有证据支持实现了 [#2504] 至 [#2508] 的主要编码目标:四类正式脚本条目使用 pending_authoring,插入时置位、视觉层写回时清除;内容确认执行整集物化并支持 overwrite_revision;确认后的脚本规划只读;提示词编写默认处理待编写条目并支持 entry_ids,拒绝 scope;时间线支持 narration 编辑、`source_text… 提供 scripts/audit_tests.py --check 成功的 CI 或其他可核验证据。若该检查失败,则修复其报告的问题后重新评估。
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 标题准确概括了主要变更:内容确认将脚本规划转换为正式脚本,提示词编写仅处理待编写条目。标题简洁、具体,符合 Conventional Commit 风格。
Description check ✅ Passed 描述完整说明了变更范围、关联 issue、验证结果及 stage 2 过渡事项。虽然未使用模板中的“变更”和“风险与遗留”标题,但相关信息已在“概要”和“已知过渡态”中提供,整体内容充分。
Out of Scope Changes check ✅ Passed 未发现与 [#2504] 至 [#2508] 无关的变更。模型、转换、提示词编写、工作流、时间线、Agent 工具、错误映射、测试、翻译、ADR、术语和 skill 文档均直接支持这些 issue 的契约。
Full details: Linked Issues check

Explanation

现有证据支持实现了 [#2504] 至 [#2508] 的主要编码目标:四类正式脚本条目使用 pending_authoring,插入时置位、视觉层写回时清除;内容确认执行整集物化并支持 overwrite_revision;确认后的脚本规划只读;提示词编写默认处理待编写条目并支持 entry_ids,拒绝 scope;时间线支持 narration 编辑、source_text 只读展示和源文子串校验。摘要还显示相关路由、工具、组件、翻译和测试已更新。聚焦检查确认当前支持语言为 en、vi、zh。未提供 scripts/audit_tests.py --check 的可核验执行结果,因此无法确认所有 issue 验收条件均已满足。

Full details: Docstring Coverage

Explanation

Docstring coverage is 62.68% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 142 functions across 50 files. (36 skipped: 9 unsupported, 27 over the file limit.)

  • Fix all pre-merge checks with AI

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the script pipeline so that content confirmation directly materializes the script plan into a formal script, with prompt authoring subsequently reading from and writing back to the formal script itself. This eliminates the redundant synchronization and fingerprinting between the plan and the formal script. It also introduces UI updates for read-only source text display, overwrite warning dialogs, and corresponding backend, SDK, and test updates. The review feedback correctly identifies a potential runtime TypeError in ReadOnlyUtterances when utterances is undefined, and a TypeScript compilation error in ReadOnlyText due to strict string typing on optional fields.

return <p className={`whitespace-pre-wrap text-[12.5px] leading-relaxed ${className}`}>{text}</p>;
}

function ReadOnlyUtterances({ utterances }: { utterances: Utterance[] }) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Since scene.utterances is optional (especially for legacy drama scripts that go through read-time migration), utterances can be undefined. Directly calling utterances.map will cause a runtime TypeError. We should default utterances to an empty array to prevent potential crashes.

Suggested change
function ReadOnlyUtterances({ utterances }: { utterances: Utterance[] }) {
function ReadOnlyUtterances({ utterances = [] }: { utterances?: Utterance[] }) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不修改。只读分支只在确认态且非待修复草稿时渲染(confirmed = status==="confirmed" && !quarantined),数据与同卡片的可编辑分支(UtteranceListEditor 同样直接 utterances.map)同源:frontend/src/types/script.ts 的 DramaSceneContent.utterances 是必填 Utterance[],web 保存经 DramaSceneContent.model_validate(...).model_dump() 落盘,utterances 总会以列表写出。旧口播双字段的读时迁移作用于正式脚本的 DramaScene,不作用于脚本规划的内容确认页。只在只读分支加默认值会和类型契约、编辑分支的处理不一致。

}

/** 只读正文:保留换行,空值不渲染。 */
function ReadOnlyText({ text, className = "" }: { text: string; className?: string }) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Both scene.source_text and segment.novel_text are optional (string | undefined) in the script models. Passing them to ReadOnlyText which expects a strict string type will cause a TypeScript compilation error. We should update the type signature of text to be optional.

Suggested change
function ReadOnlyText({ text, className = "" }: { text: string; className?: string }) {
function ReadOnlyText({ text, className = "" }: { text?: string; className?: string }) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不修改。DramaSceneContent.source_text 与 NarrationScriptPlanSegment.novel_text 在 frontend/src/types/script.ts 里都是必填 string,不是 string | undefined。当前 HEAD 上 pnpm check(typecheck)通过,不存在所说的编译错误。

@Pollo3470
Pollo3470 marked this pull request as ready for review September 17, 2026 08:16
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-17T10:17:12.549018Z 2b000ac New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 163e5fc07d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread server/draft_workflow.py
Comment on lines +1100 to +1101
if draft is not None and draft.meta.get(_FORMAL_EDIT_META_KEY) is not True:
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject legacy edit drafts after confirmation

When an episode was confirmed and its script plan was opened for editing before this upgrade, the persisted draft has source and base_fingerprint metadata but no new formal_edit flag. This early return therefore lets patch_draft and promote_draft mutate that confirmed plan after deployment, leaving the materialized formal script unchanged and violating the post-confirmation read-only requirement in docs/adr/0080-review-confirm-materializes-script-prompt-authoring-fills-pending.md:12. Treat legacy edit envelopes conservatively or distinguish generated rerun drafts with positive metadata.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不修改,作为存量过渡态记录。(1) 无 formal_edit 的编辑副本只可能是升级前打开、留到升级后的草稿:确认服务在草稿在场时拒绝确认,升级后新开的编辑副本一律带标记。(2) 这种草稿晋升后会改写脚本规划,指纹漂移使该集回到待确认;正式脚本不变,要确认(会覆盖,需显式认可 revision)后才会被替换。结果与「重跑脚本规划」一致,不会绕开确认改动正式脚本。(3) 按缺标记保守拒绝,会把重跑产出的待修复草稿(同样不带标记、需要修复后晋升)一并挡住,破坏「重跑脚本规划仍允许」。存量草稿的清理归后续的项目迁移处理。

kwargs["rewritten_entry_ids"].append("E1S01")
return scripts / "episode_1.json"

monkeypatch.setattr(mod, "ScriptGenerator", _FakeGenerator)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exercise the real generator in integration coverage

The new helper replaces the repository-internal ScriptGenerator, so the three integration tests using it only prove that preflight reaches a fabricated success; their {"episode": 1} formal script would not exercise the real pending-entry path, and regressions in that path can remain green. Per the repository's test-substitute rule, keep ScriptGenerator real and substitute only its outbound model boundary.

AGENTS.md reference: AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

@Pollo3470 Pollo3470 Sep 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已在 14485f4 修复。三个「脚本规划草稿不阻塞编写」用例不再替换 ScriptGenerator:在正式剧本里放一条带待编写标记的条目,以 dry_run 调编写工具,由真实生成器按正式剧本渲染编写 prompt,并断言 prompt 里有该条目。预检拦截时工具返回错误,用例会失败;草稿原样保留的断言不变。

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 14485f4742

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/script_batch_edit.py Outdated
Comment on lines +712 to +713
if removed.get(PENDING_AUTHORING_FIELD) is True and not authored:
item[PENDING_AUTHORING_FIELD] = True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark incomplete reinserted entries as pending

When patch_episode_script splits an authored entry and the anchor part omits or clears its visual prompts, the operation is projected as a remove followed by a same-ID insert. Because the removed entry was previously authored, it has no pending_authoring flag, so this condition leaves the replacement's empty visual layer unmarked. The workflow and default generate_episode_script both select only entries marked pending, causing this incomplete anchor to be reported as complete and skipped indefinitely unless its ID is explicitly supplied. Set the marker whenever the reinserted item is not authored, regardless of the removed item's prior marker.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已在 2e0118d 修复。同 id 重插(拆分锚点、先删后插)不再沿用被删条目的标记,和新增条目一样只看插入的条目是否带齐视觉层:缺视觉层就置待编写。用例覆盖了 authored 条目以缺视觉层重插后被置位,以及拆分锚点缺提示词时被置位。

@Pollo3470

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the script generation and content confirmation workflow. Key changes include materializing the script plan into a formal script upon confirmation, introducing explicit 'pending authoring' markers for visual layers, and requiring explicit acknowledgement when overwriting existing formal scripts. I have identified two critical issues: missing imports for hashlib and json in lib/script_review.py, and a missing import for ConfigDict in server/routers/script_review.py.

Comment thread lib/script_review.py
Comment on lines 26 to 30
import logging
from collections.abc import Callable, Generator, Sequence
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

在 formal_script_overwrite 函数中使用了 hashlib 和 json 模块(例如 hashlib.sha256 和 json.loads),但该文件头部并未导入这两个标准库模块。这会在解析非 JSON 或损坏的正式脚本文件时触发 NameError。

建议在文件头部添加 hashlib 和 json 的导入。

Suggested change
import logging
from collections.abc import Callable, Generator, Sequence
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
import hashlib
import json
import logging
from collections.abc import Callable, Generator, Sequence
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不修改。lib/script_review.py 文件头第 24、25 行已有 import hashlib 和 import json,两者在本 PR 之前就已被该模块使用。basedpyright 与全量测试在当前 HEAD 上均通过。

Comment on lines +125 to +126
class ConfirmScriptReviewRequest(BaseModel):
model_config = ConfigDict(extra="forbid")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

在 ConfirmScriptReviewRequest 类中使用了 ConfigDict,但该文件头部并未导入 ConfigDict,这会导致 NameError。

由于 Pydantic v2 同样支持使用字典来配置 model_config,建议直接将 model_config 声明为字典,以避免引入额外的导入依赖。

Suggested change
class ConfirmScriptReviewRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
class ConfirmScriptReviewRequest(BaseModel):
model_config = {"extra": "forbid"}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不修改。server/routers/script_review.py 第 11 行已有 from pydantic import BaseModel, ConfigDict, Field,同文件其他请求模型也用了 ConfigDict。路由集成测试在当前 HEAD 上通过。

@Pollo3470

Copy link
Copy Markdown
Contributor Author

@codex review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 4fc74e06-3ded-41e1-9386-1205db2815a0

📥 Commits

Reviewing files that changed from the base of the PR and between 9f41273 and 2e0118d.

📒 Files selected for processing (86)
  • CONTEXT.md
  • agent_runtime_profile/.claude/references/workflow-plan.md
  • agent_runtime_profile/.claude/skills/generate-script/SKILL.md
  • agent_runtime_profile/.claude/skills/video-workflow/SKILL.drama.md
  • agent_runtime_profile/.claude/skills/video-workflow/SKILL.narration.md
  • docs/adr/0041-script-pipeline-script-plan-prompt-authoring-review-gate.md
  • docs/adr/0080-review-confirm-materializes-script-prompt-authoring-fills-pending.md
  • frontend/src/api.test.ts
  • frontend/src/api.ts
  • frontend/src/components/canvas/grid/GridImageToVideoCanvas.tsx
  • frontend/src/components/canvas/reference/ReferenceScriptPlanPreviewPanel.test.tsx
  • frontend/src/components/canvas/reference/ReferenceScriptPlanPreviewPanel.tsx
  • frontend/src/components/canvas/reference/ReferenceVideoCanvas.test.tsx
  • frontend/src/components/canvas/reference/ReferenceVideoCanvas.tsx
  • frontend/src/components/canvas/reference/ReferenceVideoCard.test.tsx
  • frontend/src/components/canvas/reference/ReferenceVideoCard.tsx
  • frontend/src/components/canvas/timeline/ScriptReviewGate.test.tsx
  • frontend/src/components/canvas/timeline/ScriptReviewGate.tsx
  • frontend/src/components/canvas/timeline/ShotDetail.drama.test.tsx
  • frontend/src/components/canvas/timeline/ShotDetail.narration.test.tsx
  • frontend/src/components/canvas/timeline/ShotDetail.pending-prompt.test.tsx
  • frontend/src/components/canvas/timeline/ShotDetail.tsx
  • frontend/src/components/canvas/timeline/TimelineCanvas.test.tsx
  • frontend/src/components/canvas/timeline/TimelineCanvas.tsx
  • frontend/src/components/shared/ScriptOverwriteConfirmDialog.tsx
  • frontend/src/components/shared/SourceTextReadonly.tsx
  • frontend/src/hooks/useScriptEntryCurrency.test.tsx
  • frontend/src/hooks/useScriptReviewDraft.test.tsx
  • frontend/src/hooks/useScriptReviewDraft.ts
  • frontend/src/i18n/en/dashboard.ts
  • frontend/src/i18n/vi/dashboard.ts
  • frontend/src/i18n/zh/dashboard.ts
  • frontend/src/types/reference-video.ts
  • frontend/src/types/script.ts
  • lib/data_validator.py
  • lib/i18n/en/errors.py
  • lib/i18n/vi/errors.py
  • lib/i18n/zh/errors.py
  • lib/project_manager.py
  • lib/prompt_builders_ad.py
  • lib/prompt_builders_script.py
  • lib/prompt_templates/templates/text/ad_reference_video_prompt_authoring.md
  • lib/prompt_templates/templates/text/ad_storyboard_prompt_authoring.md
  • lib/reference_video/draft_validation.py
  • lib/script_batch_edit.py
  • lib/script_editor.py
  • lib/script_generator.py
  • lib/script_models.py
  • lib/script_plan_entries.py
  • lib/script_review.py
  • lib/workflow_state.py
  • server/agent_runtime/sdk_tools/patch_script.py
  • server/agent_runtime/sdk_tools/text_generation.py
  • server/draft_workflow.py
  • server/remote_mcp.py
  • server/routers/_script_review_errors.py
  • server/routers/files.py
  • server/routers/projects.py
  • server/routers/script_review.py
  • server/services/script_plan_conversion.py
  • server/services/script_review.py
  • server/text_generation.py
  • server/tool_runtime.py
  • tests/integration/lib/test_data_validator.py
  • tests/integration/lib/test_script_batch_edit.py
  • tests/integration/lib/test_script_generator.py
  • tests/integration/lib/test_script_generator_ad_prompt_authoring.py
  • tests/integration/lib/test_script_generator_incremental.py
  • tests/integration/lib/test_script_generator_reference_branch.py
  • tests/integration/lib/test_workflow_state.py
  • tests/integration/server/agent_runtime/sdk_tools/test_draft_tools.py
  • tests/integration/server/agent_runtime/sdk_tools/test_patch_tools.py
  • tests/integration/server/agent_runtime/sdk_tools/test_promote_draft.py
  • tests/integration/server/agent_runtime/sdk_tools/test_text_generation.py
  • tests/integration/server/routers/test_files_router.py
  • tests/integration/server/routers/test_projects_script_edit_endpoints.py
  • tests/integration/server/routers/test_reference_videos_router.py
  • tests/integration/server/routers/test_script_review_router.py
  • tests/integration/server/services/test_script_review.py
  • tests/integration/server/test_remote_mcp.py
  • tests/unit/lib/test_drama_pipeline_split.py
  • tests/unit/lib/test_script_editor.py
  • tests/unit/lib/test_script_models.py
  • tests/unit/lib/test_script_plan_entries.py
  • tests/unit/lib/test_workflow_action_types.py
  • tests/unit/server/test_tool_runtime.py

Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.

Comment thread agent_runtime_profile/.claude/skills/video-workflow/SKILL.drama.md
Comment thread CONTEXT.md Outdated
Comment thread frontend/src/components/shared/SourceTextReadonly.tsx Outdated
Comment thread lib/prompt_builders_ad.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c25a7efe38

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/script_generator.py Outdated
Comment on lines +917 to +920
if self._script_plan_input_claim is not None:
await asyncio.to_thread(
assert_current_artifact_input_claims_usable, self.project_path, (self._script_plan_input_claim,)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recheck the plan inside the materialization transaction

If a Web save, rerun, or draft promotion commits a new script plan after this claim check releases project_metadata_lock but before pm.save_script acquires its project lock, confirmation writes the previously loaded plan over the formal script and records its old fingerprint. The endpoint then merely reports the new plan as pending review, even though the prior formal script and asset registrations have already been destructively replaced instead of returning conversion_conflict; revalidate the plan from project_update while the formal write transaction is still rollback-capable.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已在 c3da019 修复。物化落盘改为在脚本规划的 per-path 锁内执行:先按当前文件复核规划指纹,与确认指纹不符就抛 ScriptPlanWriteConflict(服务层映射为 conversion_conflict),再做产物依据核对和 save_script(含确认记录)。Web 保存、重跑与晋升写规划时持同一把锁,加锁顺序同样是先规划锁、再项目锁,不会死锁。新增服务层用例:物化加载规划之后、落盘之前写入新规划,确认返回冲突,不写正式脚本,也不记录确认。修复前该用例失败。

Comment thread lib/script_review.py
读的是确认转换写出的那份 ``scripts/episode_N.json``。文件存在但读不成剧本(非法 JSON、
条目数组损坏)时照样算已有正式脚本、条目列表为空:覆盖它仍需认可。
"""
path = project_path / episode_script_relpath(episode)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve overwrite targets from the episode binding

When project.json binds the episode to a supported noncanonical filename such as scripts/custom-name.json, this hard-coded canonical path reports that no formal script exists. Confirmation therefore requires no overwrite revision, creates episode_1.json, and rebinds the episode to it without listing or explicitly accepting the existing script, its user edits, or its media; derive the overwrite target from the episode's script_file binding, as the existing review-state loader already does.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本 PR 不改,作为已知局限记录为后续迁移候选。当前产品里正式脚本的写出口(确认转换、提示词编写、ad 整份生成)都写 scripts/episode_N.json,自定义 script_file 只出现在存量集。main 上的机械转换同样忽略自定义绑定:写 canonical 文件并重新绑定,这里沿用了原有行为,不是本 PR 引入的回退。旧文件不会被删除,只是不再被该集引用。存量绑定的规范化、以及未正式化集的物化,归 Spec #2503 的迁移步(#2509)处理。

Comment thread lib/script_batch_edit.py
Comment on lines +660 to +662
# 手写视觉层等同编写完成:写入后视觉层齐备即清除待编写,默认编写不再覆盖它。
if roots & set(_VISUAL_LAYER_FIELDS[kind]) and _visual_layer_complete(kind, item):
item.pop(PENDING_AUTHORING_FIELD, None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore pending state when a visual layer is cleared

When an authored narration or drama item is updated through the batch API with image_prompt or video_prompt set to None, the schema accepts the pending form, but this branch only removes the marker when the layer is complete and never adds it when the layer becomes incomplete. Since default prompt authoring selects only entries whose pending_authoring is true, the cleared entry is subsequently treated as complete and skipped unless its ID is explicitly supplied.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不修改,这是已裁决的产品取舍。待编写标记只表示「这条的视觉层还没被编写或手写过」,清除规则是写齐视觉层或编写写回,清空提示词不会重新置位。原因:清空其中一侧后,如果重新置位,默认编写会把另一侧手写的内容一起覆盖,和「默认编写不覆盖手改」冲突。确实要重做时,显式传 entry_ids 重写,或把视觉层手写补齐。

Comment thread lib/script_batch_edit.py
text = ""
if text.strip():
return [text]
return [doc.text for doc in discover_sources(project_dir)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude other episodes from source-anchor fallback

If source/episode_N.txt is absent and the source directory contains only user-supplied split files, discover_sources() treats those episode files as primary sources. For example, editing episode 1 when only episode_2.txt exists allows a source_text copied from episode 2, despite the contract requiring a verbatim fragment from the current episode; the fallback should distinguish unsplit project originals from explicitly named sources belonging to another episode.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不修改。对应原文校验以本集源文为准,本集源文缺失时回落到项目源文,这是本批次已裁决的口径。所说的场景是:episode_N.txt 缺失,项目里只有其他集的拆分文件。此时没有属于本集的源文可以比对,把其他集文件排除在外,只会让回落列表变空、校验被跳过,同样拦不住跨集片段;如果改为直接拒绝,又会让这类项目完全写不了锚点。拆分工具在本集源文缺失时本身也无法生成锚点。

@@ -255,15 +255,47 @@ async def generate(self, **kwargs) -> Path:
return project_path / "scripts" / "episode_1.json"

monkeypatch.setattr(mod, "ScriptGenerator", _FakeGenerator)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exercise the real generator in handler integration tests

Unlike the already-fixed promote-draft tests, these handler integration cases still replace the repository-internal ScriptGenerator and fabricate rewritten IDs and no-op behavior, so they remain green if pending-entry selection or the ad formal-script dispatch breaks between the handler and generator. Keep the real generator and substitute only its outbound model boundary, as required by the repository's test-substitute standard.

AGENTS.md reference: AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

部分修复(c3da0194)。本 PR 新增的「脚本规划重跑未确认不阻塞编写」用例改为真实生成器,以 dry_run 按正式脚本里的待编写条目渲染 prompt。「无待编写条目的回执」用例保留生成器替身:非 dry-run 路径的 ScriptGenerator.create 要经配置解析器创建文本后端,这套 handler 测试夹具没有可替换的文本后端边界;待编写条目选择与无待编写时不调模型,已由真实生成器用例覆盖(tests/integration/lib/test_script_generator_incremental.py 的 TestPromptAuthoring)。文件里其余替身用例在 main 上就已存在,不在本 PR 范围内。

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c3da0194a0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread server/tool_runtime.py
Comment on lines 1964 to 1967
source=payload.get("source"),
instructions=payload.get("instructions"),
dry_run=bool(payload.get("dry_run")),
scope=payload.get("scope") or SCOPE_STALE,
entry_ids=tuple(payload.get("entry_ids") or ()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject legacy scope values in queued text tasks

When a text_episode_script task was persisted before deployment with scope="all" and is claimed afterward, request reconstruction silently drops that field and supplies empty entry_ids, whose new meaning is “author pending entries only.” For an already-authored script this can report a successful no-op instead of performing the requested rewrite; either preserve the legacy semantics during queue draining or fail legacy payloads explicitly.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不在本 PR 修改,记为非阻塞的后续候选。只影响部署前入队、部署后才被领取、且带 scope="all" 的编写任务。这类任务按「只填待编写」执行:已编写完的剧本会得到空操作回执,回执写明「没有待编写的条目,未调用文本模型,正式脚本未改动;要重写指定条目请传 entry_ids」,Agent 可以按提示重新发起,不会写坏或丢失数据。文本任务是短时任务,跨部署残留的窗口很小;另外本 Spec 的 contract 步(#2509)会整体清理 expand 期的过渡兼容。

Comment on lines +462 to +464
onConfirm={async () => {
await handleConfirm({ overwriteRevision: overwrite.revision });
setOverwriteOpen(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the overwrite dialog open after confirmation errors

When overwrite confirmation fails—for example because another writer changed the formal script revision—handleConfirm catches the error and resolves normally after updating the overwrite details, so this callback unconditionally closes the dialog. The user loses the refreshed consequences and must reopen the dialog before retrying; close it only when confirmation actually succeeds. The same unconditional close exists in ReferenceScriptPlanPreviewPanel.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不在本 PR 修改,记为非阻塞的体验改进候选。确认失败时,hook 已把最新的覆盖清单(新的 revision 与移除条目)写回页面状态,危险态按钮和覆盖提示随之刷新,并有错误提示。用户再次点击会看到更新后的清单。正式脚本不会被未认可的内容覆盖:服务端只接受与当前正式脚本一致的 overwrite_revision。失败后保持对话框打开属于交互细节,ScriptReviewGate 与 ReferenceScriptPlanPreviewPanel 可以一起处理。

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · 🎯 Functional Correctness · prompt_builders_ad.py:237-245

lib/prompt_builders_ad.py:237-245
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

在写入 <units> 前中和单元正文中的标签。 build_ad_reference_prompt_authoring_prompt() 将 render_ad_units_for_prompt_authoring() 的结果作为 units_content 插入 <units> 块。模板引擎关闭了自动转义,因此 unit["text"] 中的 </units> 会提前关闭该块并改变 prompt context。请在追加正文前调用 _neutralize_tags:

body = _neutralize_tags(str(unit.get("text") or "").strip())

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 776c4886-41ed-4773-a132-2a8991687cd1

📥 Commits

Reviewing files that changed from the base of the PR and between c25a7ef and c3da019.

📒 Files selected for processing (3)
  • lib/script_generator.py
  • tests/integration/server/agent_runtime/sdk_tools/test_text_generation.py
  • tests/integration/server/services/test_script_review.py

Included review availability: Your plan provides up to 8 included reviews per hour; 4 remain after this review.

@Pollo3470

Copy link
Copy Markdown
Contributor Author

@coderabbitai 关于 review 中 outside-diff 的 lib/prompt_builders_ad.py:237-245(中和 <units> 正文标签):处置同 inline 讨论 #discussion_r4035058209,不修改。单元正文既是这份 prompt 的上下文,也是模型改写并写回正式脚本的基底,中和尖括号会把全角字符带进用户正文;参考生视频路线 render_reference_units_for_prompt_authoring 同样不中和正文。

@coderabbitai

coderabbitai Bot commented Sep 17, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@Pollo3470,确认。该正文必须保持原样,以避免模型将中和后的字符写回正式脚本。render_reference_units_for_prompt_authoring 也应保持相同规则。此处不需要修改。


🧠 Learnings used
Learnt from: Pollo3470
Repo: ArcReel/ArcReel PR: 2531
File: lib/prompt_builders_ad.py:245-245
Timestamp: 2026-09-17T09:30:07.268Z
Learning: 在 `lib/prompt_builders_ad.py` 的 `render_ad_units_for_prompt_authoring` 中,参考生视频单元的 `text` 是模型改写后会原样写回正式脚本、并作为后续视频生成提示词的内容基底。因此不得对该正文使用 `_neutralize_tags`,以免将 `<`、`>` 等用户原有字符改写并持久化。`render_reference_units_for_prompt_authoring` 也遵循相同的正文保真规则。分镜的口播和视觉提示词摘要仅作上下文且不会写回,可以中和标签字符。

You are interacting with an AI system.

@Pollo3470
Pollo3470 force-pushed the afk/spec-2503-20260917-044955-c242db/stage-1 branch from c3da019 to 34e6d5e Compare September 17, 2026 09:53
新增 ADR 0080,记录正式脚本成为该集唯一内容真相、脚本规划在内容确认时退出的决策;
ADR 0041 顶部加指向说明其「规划为持久基底、按条目增量、指纹」一段已被取代;
词汇表更新「脚本规划」「提示词编写」「内容确认」释义并新增「待编写」词条。

Refs #2503
- 四种条目模型新增对 LLM 隐藏的 pending_authoring,patch 拒改
- 批量编辑 insert_after 为新增条目置位,同 id 重插沿用被删条目状态;
  Agent insert / split 新 part 与参考生视频新增单元随之置位
- 提示词编写写回的条目清除标记;机械转换为新增的提示词条目置位,采用新内容保持原状态
- 工作流 author_prompts 改读标记,不再以提示词为空隐式判断
- 时间线分镜详情对待编写条目显示提示(zh/en/vi)

指纹退役 expand–contract 的 expand 步,不删除现有机制。

Refs #2504
- 确认服务在记录确认的同一次正式剧本写入里,把脚本规划整份机械转为正式剧本;
  drama / narration / reference_video 全部条目带待编写标记,视觉层为空,条目指纹与整集指纹照常盖上
- 该集已有正式脚本时,确认须带覆盖清单的 revision;缺失或与当前正式脚本不符时拒绝并返回当前
  将移除的分镜与产物摘要,不写正式脚本与确认记录;认可后旧分镜(含同 id 条目)的产物登记经剧本
  写入的撤登记出口撤销
- GET script-review 增加 script_overwrite(含 revision);确认路由接受 overwrite_revision,缺失或过期时 409 带诊断
- Agent confirm_script_review(SDK 与 remote MCP)增加同名参数,缺失或过期时返回 script_overwrite_required 与相同清单
- 内容确认页(drama / narration 与参考生视频)在已有正式脚本时以 danger 按钮与确认框列出后果(zh/en/vi)
- 视频模型解析不到时长档位时确认以 422 script_review_video_model_unresolved 拒绝,提示去配置视频模型
- 确认转换不按机器口径预判参考单元正文(单元待编写,由提示词编写改写并校验)
- 工作流对参考生视频待编写单元同样给出 author_prompts
- 手动预拆分自愈补建集条目后补登记该集脚本规划产物

指纹、条目时效、合并装配、convert_script_plan 工具与转换预演路由保留,退役归后续 ticket。

Refs #2505
- 内容确认服务保存(PUT script-review/content)与通用草稿端点对已确认的脚本规划拒绝,
  409 script_review_script_plan_confirmed,诊断 {"code": "script_plan_confirmed"},文件不变(zh/en/vi)
- 判据 formal_script_plan_confirmed 与 review_status 同口径但不看待修复草稿:重跑留下草稿时仍只读,
  重跑写出新内容后回到待确认、恢复可编辑
- Agent open_draft 不再为已确认的脚本规划取回编辑副本;编辑副本带 formal_edit 标记,
  确认后 patch_draft / promote_draft 对其返回 script_plan_confirmed 与指引;重跑产出的草稿与提示词编写草稿不受影响
- ScriptReviewGate 与 ReferenceScriptPlanPreviewPanel 确认态不渲染编辑控件,显示只读指引与「去时间线修改」入口;
  保存被拒或刷新到确认态时采用服务端内容

Refs #2507
- 旁白分镜 PATCH 接受 novel_text;时间线分镜详情把旁白正文纳入草稿,保存经分镜更新路由落盘
- drama 分镜与参考生视频单元的对应原文只读展示,手动新增条目显示空态;web PATCH 不接受 source_text
- 参考生视频正式单元增加 source_text,脚本规划投影带上它,内容确认转换时从规划透传,对 LLM 隐藏
- 脚本批量编辑对新增或改动的非空 source_text 校验:须是本集派生源文 source/episode_N.txt 的逐字子串(与拆分工具同源同口径);本集源文缺失时回落到项目源文,项目无源文时不校验;不通过以 source_text_not_verbatim 拒绝并给出 operation_index / unit_id / 字段位置

Refs #2508
- generate_episode_script 的输入改为正式脚本自身:不传 entry_ids 编写全部带待编写标记的
  条目,entry_ids 为显式重写;只写视觉层(参考生视频改写单元正文),其余条目逐字节不变,
  无目标条目时不调用模型、不写盘。不再读脚本规划、不受内容确认门禁与规划草稿阻塞。
- 取消 scope:SDK 工具与远程 MCP 均拒绝并返回迁移说明;队列回放同步去掉。
- ad:尚无正式脚本时整份生成不变;已有正式脚本时按同一动作填充待编写分镜 / 单元(新增两份
  提示词编写模板与 AdVisualScript),待编写分镜允许提示词为空。
- 参考生视频待修复草稿记录 unit_ids,晋升按它从正式脚本取回同一批单元,不再受确认门禁阻塞。
- 工作流四种骨架都按待编写标记给出 author_prompts;转换为新增参考单元也置待编写。
- generate-script skill 按新契约重写。

Refs #2506
- 内容确认转换把 drama 的视觉改编描述透传进正式分镜,提示词编写读正式脚本时仍以它为视觉基底
- 批量编辑(Agent patch、时间线与参考画布路由)写齐视觉层即清除待编写;新增、拆分与同 id 重插的条目按是否带齐视觉层判定待编写,默认编写不再覆盖手写提示词;时间线待编写提示恢复手填指引
- 内容确认物化持脚本规划锁复核指纹,加载后被保存、重跑或晋升改写的规划按冲突拒绝,不以旧内容覆盖正式脚本
- 对应原文按逐字原样展示;脚本规划词条与转换注释对齐待编写口径
- 草稿与内容确认不阻塞编写的用例改用真实生成器预演
@Pollo3470
Pollo3470 force-pushed the afk/spec-2503-20260917-044955-c242db/stage-1 branch from 34e6d5e to 2b000ac Compare September 17, 2026 10:10

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b000acd69

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/script_generator.py
script_data = self._add_metadata(script_data, episode)
script_data["metadata"]["generator"] = SCRIPT_PLAN_CONVERSION_GENERATOR
for item in script_data[items_key]:
item[PENDING_AUTHORING_FIELD] = True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Block media generation for pending reference units

For reference-video plans, confirmation preserves each unit's nonempty text and only adds pending_authoring, but neither ReferenceVideoCanvas.isUnitGenerationBlocked nor the single/batch admission paths (_require_unit_ready and reference_unit_task_spec) inspect this marker. Immediately after confirmation, the enabled generation actions can therefore enqueue paid video directly from the un-authored script-plan text, bypassing the authoring step required by docs/adr/0080-review-confirm-materializes-script-prompt-authoring-fills-pending.md:10-11; reject these units in media admission and disable their generation controls until authoring clears the marker.

Useful? React with 👍 / 👎.

Comment thread lib/script_generator.py
content = {**content, "image_prompt": None, "video_prompt": None}
items.append(content)
items_key = plan_variant(plan_kind).skeleton_kind
episode_title = self._episode_entry(episode).get("title")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-read the episode title inside the commit

For narration/reference plans, or a drama plan without a title, this fallback comes from the ScriptGenerator's pre-transaction project.json snapshot. If a user renames the episode after that snapshot is loaded but before _commit, pm.save_script later acquires the project lock and _apply_episode_sync copies this stale title back into the latest project, silently undoing the concurrent rename; the plan and formal-script fingerprint checks do not protect project metadata. Resolve or validate the fallback title while holding the project transaction.

Useful? React with 👍 / 👎.

@Pollo3470
Pollo3470 merged commit d0351e7 into main Sep 17, 2026
22 of 23 checks passed
@Pollo3470
Pollo3470 deleted the afk/spec-2503-20260917-044955-c242db/stage-1 branch September 17, 2026 10:18

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 517e4472-47c7-49a3-bbf3-e0fbf6710957

📥 Commits

Reviewing files that changed from the base of the PR and between 34e6d5e and 2b000ac.

📒 Files selected for processing (86)
  • CONTEXT.md
  • agent_runtime_profile/.claude/references/workflow-plan.md
  • agent_runtime_profile/.claude/skills/generate-script/SKILL.md
  • agent_runtime_profile/.claude/skills/video-workflow/SKILL.drama.md
  • agent_runtime_profile/.claude/skills/video-workflow/SKILL.narration.md
  • docs/adr/0041-script-pipeline-script-plan-prompt-authoring-review-gate.md
  • docs/adr/0080-review-confirm-materializes-script-prompt-authoring-fills-pending.md
  • frontend/src/api.test.ts
  • frontend/src/api.ts
  • frontend/src/components/canvas/grid/GridImageToVideoCanvas.tsx
  • frontend/src/components/canvas/reference/ReferenceScriptPlanPreviewPanel.test.tsx
  • frontend/src/components/canvas/reference/ReferenceScriptPlanPreviewPanel.tsx
  • frontend/src/components/canvas/reference/ReferenceVideoCanvas.test.tsx
  • frontend/src/components/canvas/reference/ReferenceVideoCanvas.tsx
  • frontend/src/components/canvas/reference/ReferenceVideoCard.test.tsx
  • frontend/src/components/canvas/reference/ReferenceVideoCard.tsx
  • frontend/src/components/canvas/timeline/ScriptReviewGate.test.tsx
  • frontend/src/components/canvas/timeline/ScriptReviewGate.tsx
  • frontend/src/components/canvas/timeline/ShotDetail.drama.test.tsx
  • frontend/src/components/canvas/timeline/ShotDetail.narration.test.tsx
  • frontend/src/components/canvas/timeline/ShotDetail.pending-prompt.test.tsx
  • frontend/src/components/canvas/timeline/ShotDetail.tsx
  • frontend/src/components/canvas/timeline/TimelineCanvas.test.tsx
  • frontend/src/components/canvas/timeline/TimelineCanvas.tsx
  • frontend/src/components/shared/ScriptOverwriteConfirmDialog.tsx
  • frontend/src/components/shared/SourceTextReadonly.tsx
  • frontend/src/hooks/useScriptEntryCurrency.test.tsx
  • frontend/src/hooks/useScriptReviewDraft.test.tsx
  • frontend/src/hooks/useScriptReviewDraft.ts
  • frontend/src/i18n/en/dashboard.ts
  • frontend/src/i18n/vi/dashboard.ts
  • frontend/src/i18n/zh/dashboard.ts
  • frontend/src/types/reference-video.ts
  • frontend/src/types/script.ts
  • lib/data_validator.py
  • lib/i18n/en/errors.py
  • lib/i18n/vi/errors.py
  • lib/i18n/zh/errors.py
  • lib/project_manager.py
  • lib/prompt_builders_ad.py
  • lib/prompt_builders_script.py
  • lib/prompt_templates/templates/text/ad_reference_video_prompt_authoring.md
  • lib/prompt_templates/templates/text/ad_storyboard_prompt_authoring.md
  • lib/reference_video/draft_validation.py
  • lib/script_batch_edit.py
  • lib/script_editor.py
  • lib/script_generator.py
  • lib/script_models.py
  • lib/script_plan_entries.py
  • lib/script_review.py
  • lib/workflow_state.py
  • server/agent_runtime/sdk_tools/patch_script.py
  • server/agent_runtime/sdk_tools/text_generation.py
  • server/draft_workflow.py
  • server/remote_mcp.py
  • server/routers/_script_review_errors.py
  • server/routers/files.py
  • server/routers/projects.py
  • server/routers/script_review.py
  • server/services/script_plan_conversion.py
  • server/services/script_review.py
  • server/text_generation.py
  • server/tool_runtime.py
  • tests/integration/lib/test_data_validator.py
  • tests/integration/lib/test_script_batch_edit.py
  • tests/integration/lib/test_script_generator.py
  • tests/integration/lib/test_script_generator_ad_prompt_authoring.py
  • tests/integration/lib/test_script_generator_incremental.py
  • tests/integration/lib/test_script_generator_reference_branch.py
  • tests/integration/lib/test_workflow_state.py
  • tests/integration/server/agent_runtime/sdk_tools/test_draft_tools.py
  • tests/integration/server/agent_runtime/sdk_tools/test_patch_tools.py
  • tests/integration/server/agent_runtime/sdk_tools/test_promote_draft.py
  • tests/integration/server/agent_runtime/sdk_tools/test_text_generation.py
  • tests/integration/server/routers/test_files_router.py
  • tests/integration/server/routers/test_projects_script_edit_endpoints.py
  • tests/integration/server/routers/test_reference_videos_router.py
  • tests/integration/server/routers/test_script_review_router.py
  • tests/integration/server/services/test_script_review.py
  • tests/integration/server/test_remote_mcp.py
  • tests/unit/lib/test_drama_pipeline_split.py
  • tests/unit/lib/test_script_editor.py
  • tests/unit/lib/test_script_models.py
  • tests/unit/lib/test_script_plan_entries.py
  • tests/unit/lib/test_workflow_action_types.py
  • tests/unit/server/test_tool_runtime.py
🚧 Files skipped from review as they are similar to previous changes (12)
  • tests/unit/server/test_tool_runtime.py
  • frontend/src/types/reference-video.ts
  • frontend/src/components/canvas/grid/GridImageToVideoCanvas.tsx
  • frontend/src/hooks/useScriptEntryCurrency.test.tsx
  • docs/adr/0041-script-pipeline-script-plan-prompt-authoring-review-gate.md
  • frontend/src/hooks/useScriptReviewDraft.test.tsx
  • lib/i18n/vi/errors.py
  • server/services/script_plan_conversion.py
  • frontend/src/i18n/zh/dashboard.ts
  • server/agent_runtime/sdk_tools/patch_script.py
  • frontend/src/i18n/vi/dashboard.ts
  • agent_runtime_profile/.claude/skills/video-workflow/SKILL.drama.md

Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.

`supported_durations` / `max_duration` / `max_reference_images` / `default_duration` / `episode_target_duration` 等数据。)

**中间文件变更必重生失效条目**:`prepare_script_plan` 的中间文件被修改或重拆后(无论哪种生成模式、无论首次还是重做),即使 `scripts/episode_{N}.json` 已存在,也必须重新执行 `generate_script`——剧本 JSON 不会自动跟随中间文件更新,跳过会留下"新中间文件 + 旧 JSON"的陈旧组合。`generate_script` 默认只重写内容变化与新增的条目(工作流状态在 `artifacts.script.stale_entry_ids` 列出它们),未变条目的提示词、备注、尾帧与已生成产物原样保留;要整集重写传 `scope: "all"`,只重做指定几条传 `entry_ids`。
**中间文件变更必重生失效条目**:`prepare_script_plan` 的中间文件被修改或重拆后(无论哪种生成模式、无论首次还是重做),即使 `scripts/episode_{N}.json` 已存在,也必须重新执行 `generate_script`——剧本 JSON 不会自动跟随中间文件更新,跳过会留下"新中间文件 + 旧 JSON"的陈旧组合。`generate_script` 默认只重写内容变化与新增的条目(工作流状态在 `artifacts.script.stale_entry_ids` 列出它们),未变条目的提示词、备注、尾帧与已生成产物原样保留;只重做指定几条传 `entry_ids`。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

先重新确认脚本规划,再执行提示词编写。

脚本规划重跑后,正式脚本不会自动更新。此时直接执行 generate_script 会继续编写旧正式脚本。

删除 stale_entry_ids 增量重写说明。改为先执行 confirm_script_plan。确认会整集覆盖正式脚本并设置待编写标记。随后执行 author_prompts,且不传 entry_ids。

建议修改
-**中间文件变更必重生失效条目**:`prepare_script_plan` 的中间文件被修改或重拆后(无论哪种生成模式、无论首次还是重做),即使 `scripts/episode_{N}.json` 已存在,也必须重新执行 `generate_script`——剧本 JSON 不会自动跟随中间文件更新,跳过会留下"新中间文件 + 旧 JSON"的陈旧组合。`generate_script` 默认只重写内容变化与新增的条目(工作流状态在 `artifacts.script.stale_entry_ids` 列出它们),未变条目的提示词、备注、尾帧与已生成产物原样保留;只重做指定几条传 `entry_ids`。
+**脚本规划重跑后必须重新确认**:刷新计划后执行 `confirm_script_plan`。确认会整集覆盖并物化正式脚本,全部条目进入待编写状态。随后刷新计划并执行 `author_prompts`;不传 `entry_ids`,一次编写全部待编写条目。

基于 PR 目标:确认即物化正式脚本,提示词编写默认处理全部待编写条目。

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
**中间文件变更必重生失效条目**:`prepare_script_plan` 的中间文件被修改或重拆后(无论哪种生成模式、无论首次还是重做),即使 `scripts/episode_{N}.json` 已存在,也必须重新执行 `generate_script`——剧本 JSON 不会自动跟随中间文件更新,跳过会留下"新中间文件 + 旧 JSON"的陈旧组合。`generate_script` 默认只重写内容变化与新增的条目(工作流状态在 `artifacts.script.stale_entry_ids` 列出它们),未变条目的提示词、备注、尾帧与已生成产物原样保留;只重做指定几条传 `entry_ids`。
**脚本规划重跑后必须重新确认**:刷新计划后执行 `confirm_script_plan`。确认会整集覆盖并物化正式脚本,全部条目进入待编写状态。随后刷新计划并执行 `author_prompts`;不传 `entry_ids`,一次编写全部待编写条目。

- **待编写是条目级显式标记**,segments / scenes / shots / video_units 四种条目形态统一,物化与手动新增置位,编写完成清除;drama / narration 不再靠「提示词为空」隐式判断,参考生视频以它判定哪些单元的正文尚待改写。
- **确认之后的脚本规划只读。** web 保存与 Agent `patch_draft` 对已确认的脚本规划一律拒绝,提示到时间线上修改,要整集重做就重跑脚本规划。
- **编写完成后的内容修改在正式脚本上做。** 时间线开放 `utterances` / `novel_text` / 参考单元 `text` 的编辑;对应原文只读展示,参考单元把脚本规划的 `source_text` 透传进正式单元;Agent 经 `patch_episode_script` 可改对应原文,项目有源文时服务端校验它是源文的逐字子串,与参考生视频拆分工具的规则一致。
- **退役**:条目指纹 `script_plan_entry_revision` 与整集指纹 `script_plan_revision`、条目时效比对与合并装配、存量回填、`GET script-review` 的 `script_entry_currency`、时间线「内容已更新,提示词可能不符」横幅与「采用新内容」、转换预演、工作流状态里正式脚本的 stale / removed 条目。由一步项目 schema 迁移删除存量指纹字段,对「已确认但尚无正式脚本」的集执行物化,对已有正式脚本的集把视觉层为空的分镜盖上待编写标记。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

把退役操作明确标为 stage 2。

当前文字声明项目迁移会删除指纹、条目时效和转换预演。当前 stage 1 实现仍保留这些机制,CONTEXT.md 也仍定义相关术语。

请说明这些操作是后续收口计划,而不是本 ADR 在当前提交中已经完成的结果。否则 ADR 会错误描述当前持久化和 API 契约。

基于 PR 目标:指纹、条目时效及相关过渡机制保留到 stage 2。

Also applies to: 29-29

🧰 Tools
🪛 LanguageTool

[uncategorized] ~14-~14: 您的意思是“对"于"”吗?
Context: ...ion与整集指纹script_plan_revision、条目时效比对与合并装配、存量回填、GET script-review的script_...

(YU7_YU8)

Comment thread lib/script_review.py
读的是确认转换写出的那份 ``scripts/episode_N.json``。文件存在但读不成剧本(非法 JSON、
条目数组损坏)时照样算已有正式脚本、条目列表为空:覆盖它仍需认可。
"""
path = project_path / episode_script_relpath(episode)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '455,540p' lib/script_review.py
sed -n '540,625p' server/services/script_review.py
sed -n '850,990p' lib/script_generator.py
rg -n 'script_file|formal_script_overwrite|materialize_script_plan' server/routers lib tests/integration/server/routers/test_projects_episodes_patch.py tests/integration/server/services/test_script_review.py | head -n 260

Repository: ArcReel/ArcReel

Length of output: 39053


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- project PATCH models and endpoint ---'
sed -n '220,285p' server/routers/projects.py
sed -n '500,620p' server/routers/projects.py
printf '%s\n' '--- project manager save and episode synchronization ---'
sed -n '1180,1285p' lib/project_manager.py
printf '%s\n' '--- generator save/materialization helpers ---'
sed -n '390,575p' lib/script_generator.py
printf '%s\n' '--- confirmation flow and relevant tests ---'
rg -n -C 5 'confirm\(|overwrite_required|script_file|episode_script_filename|episode_script_relpath' tests/integration/server/services/test_script_review.py tests/integration/server/routers/test_projects_episodes_patch.py lib/script_review.py server/services/script_review.py lib/script_generator.py | sed -n '1,360p'

Repository: ArcReel/ArcReel

Length of output: 50371


🤖 get_repo_knowledge executed:

get_repo_knowledge ArcReel/ArcReel /tmp/coderabbit-repo-knowledge/arcreel-arcreel-21a849c5/architecture /tmp/coderabbit-repo-knowledge/arcreel-arcreel-21a849c5/learnings

Length of output: 47662


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- exact PATCH route ---'
rg -n -C 18 'EpisodePatch|episodes.*script_file|update_project.*episodes|def .*project|`@router.patch`' server/routers/projects.py | sed -n '1,280p'
printf '%s\n' '--- save_script definition and call path ---'
rg -n -C 12 '^    def save_script|^    async def save_script|def _write_script|_apply_episode_sync|project_update=' lib/project_manager.py lib/script_generator.py | sed -n '1,320p'
printf '%s\n' '--- confirmation route/service symbols ---'
rg -n -C 16 'confirm_script_plan|def confirm\(|@.*confirm|overwrite_revision' server/routers/script_review.py server/services/script_review.py tests/integration/server/services/test_script_review.py | sed -n '1,300p'

Repository: ArcReel/ArcReel

Length of output: 48052


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- project PATCH implementation ---'
rg -n 'async def (update|patch).*project|def (update|patch).*project|`@router.patch`|req\.episodes|EpisodePatch' server/routers/projects.py
sed -n '880,1045p' server/routers/projects.py
printf '%s\n' '--- exact save transaction and synchronization ---'
sed -n '709,902p' lib/project_manager.py
sed -n '1233,1268p' lib/project_manager.py
printf '%s\n' '--- alternate binding contract/tests/docs ---'
rg -n -i -C 3 'non-default|alternate|重新绑定|重命名.*正式|script_file.*episode_|episode_N|episode_\{N\}|confirm.*script_file|script_file.*confirm' docs lib server tests | head -n 320

Repository: ArcReel/ArcReel

Length of output: 41961


统一正式脚本绑定的确认路径。

PATCH /projects/{name} 接受 episodes[].script_file 的非默认路径。确认流程中的 formal_script_overwrite() 却固定读取 scripts/episode_N.json,materialize_script_plan() 也固定写入该路径。随后 ProjectManager._apply_episode_sync() 会把 episodes[].script_file 重置为实际写入的默认路径。

因此,当绑定文件已存在且默认文件不存在时,覆盖检查返回 None,确认不会要求 overwrite_revision,并会丢失原绑定。默认文件存在时,确认的指纹也对应错误的脚本。

请在确认开始时解析当前 script_file,让覆盖检查、确认基线和物化写入共用该路径,并在提交时重新检查绑定未发生变化。

Comment thread server/draft_workflow.py
Comment on lines +1100 to +1101
if draft is not None and draft.meta.get(_FORMAL_EDIT_META_KEY) is not True:
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🔎 Supported by static analysis

🏁 Script executed:

sed -n '260,325p' server/draft_workflow.py
sed -n '1075,1120p' server/draft_workflow.py
sed -n '1200,1255p' server/draft_workflow.py
rg -n -C 5 'formal_edit|schema_version|read_quarantine|script_plan_confirmed' server lib/project_migrations lib/draft_quarantine.py tests/integration/server/agent_runtime/sdk_tools

Repository: ArcReel/ArcReel

Length of output: 50372


🏁 Script executed:

set -e
printf '%s\n' '--- commit context ---'
git rev-parse HEAD
git log -1 --oneline
git diff --stat HEAD^ HEAD -- server/draft_workflow.py lib/draft_quarantine.py lib/project_migrations tests/integration/server/agent_runtime/sdk_tools/test_draft_tools.py
printf '%s\n' '--- formal_edit references ---'
rg -n -C 3 'formal_edit|_FORMAL_EDIT_META_KEY' server/draft_workflow.py lib server tests --glob '*.py'
printf '%s\n' '--- migration draft references ---'
rg -n -C 4 'draft|quarantine|DRAFT_FILE_RENAMES|apply_script_plan_draft_renames' lib/project_migrations --glob '*.py'
printf '%s\n' '--- v9 migration focused source ---'
sed -n '120,180p' lib/project_migrations/v9_to_v10_script_plan_naming.py
sed -n '205,280p' lib/project_migrations/v9_to_v10_script_plan_naming.py
printf '%s\n' '--- migration runner ---'
rg -n -C 4 'v9_to_v10|migrate_v|CURRENT_SCHEMA_VERSION|MIGRATIONS|migrations' lib/project_migrations --glob '*.py'

Repository: ArcReel/ArcReel

Length of output: 50371


🏁 Script executed:

set -e
printf '%s\n' '--- parent draft creation ---'
git show HEAD^:server/draft_workflow.py | sed -n '250,315p'
printf '%s\n' '--- parent confirmed-plan handling ---'
git show HEAD^:server/draft_workflow.py | rg -n -C 8 'def _reject_confirmed_script_plan_edit|def _patch_locked|formal_script_plan_confirmed|def promote'
printf '%s\n' '--- focused diff ---'
git diff HEAD^ HEAD -- server/draft_workflow.py
printf '%s\n' '--- repository-wide marker and migration references ---'
rg -n -C 2 'formal_edit|FORMAL_EDIT' . --glob '*.py' --glob '*.md' --glob '*.json'

Repository: ArcReel/ArcReel

Length of output: 5945


🏁 Script executed:

set -e
printf '%s\n' '--- marker introduction ---'
git log --oneline -S'_FORMAL_EDIT_META_KEY' -- server/draft_workflow.py
printf '%s\n' '--- marker history ---'
git log --oneline -S'"formal_edit"' -- server/draft_workflow.py

Repository: ArcReel/ArcReel

Length of output: 322


🏁 Script executed:

set -e
printf '%s\n' '--- introduction diff ---'
git diff 551121a4c^ 551121a4c -- server/draft_workflow.py lib/draft_quarantine.py lib/project_migrations
printf '%s\n' '--- pre-introduction draft creation ---'
git show 551121a4c^:server/draft_workflow.py | rg -n -C 12 'def _open_script_plan_draft|write_quarantine\('
printf '%s\n' '--- pre-introduction draft operations ---'
git show 551121a4c^:server/draft_workflow.py | rg -n -C 8 'async def patch|async def promote|def _patch_locked|read_quarantine'

Repository: ArcReel/ArcReel

Length of output: 10825


为旧正式编辑草稿补齐 formal_edit 标记。 formal_edit 引入前创建的脚本规划编辑草稿没有该标记。现有迁移只重命名草稿文件,不会补写该元数据。

确认脚本规划后,当前条件会对缺少该键的草稿直接返回。因此,旧正式编辑草稿仍可通过 patch 或 promote 修改并晋升。

请在升级流程中识别正式编辑副本并补写 meta.formal_edit = true。如果无法可靠识别旧草稿,则拒绝缺少该字段的草稿,不能将其按重跑产物放行。

Comment thread server/remote_mcp.py
Comment on lines +530 to +538
scope: SkipJsonSchema[object] = None,
) -> CallToolResult:
"""Generate an episode script, or return its prompt when dry_run is true.
"""Author prompts for an episode script, or return the prompt when dry_run is true.

Existing scripts are rewritten incrementally: only entries whose script_plan content
changed (``scope="stale"``, the default) or the entries named by ``entry_ids`` are
re-authored; every other entry keeps its prompts, note, end frame and generated assets.
By default only the entries marked pending authoring in the formal script are authored;
``entry_ids`` explicitly re-authors the named entries. Content fields, note, end frame and
generated assets are kept. An ad project without a formal script generates the whole script.
"""
if scope is not None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '510,555p' server/remote_mcp.py
rg -n -C 5 'scope.*null|scope.*None|SCOPE_REMOVED_MESSAGE|remote_generate_episode_script' tests server/remote_mcp.py server/agent_runtime/sdk_tools/text_generation.py server/text_generation.py

Repository: ArcReel/ArcReel

Length of output: 50371


🏁 Script executed:

set -e
printf '%s\n' '--- imports and local declarations ---'
sed -n '1,90p' server/remote_mcp.py
rg -n -C 8 'remote_generate_episode_script|generate_episode_script|scope.*SCOPE_REMOVED_MESSAGE|SkipJsonSchema' tests server pyproject.toml uv.lock requirements*.txt 2>/dev/null | head -n 500

printf '%s\n' '--- dependency declarations ---'
rg -n -C 3 'fastmcp|mcp' pyproject.toml uv.lock requirements*.txt 2>/dev/null | head -n 120

printf '%s\n' '--- installed FastMCP binding/source (if available) ---'
python3 - <<'PY'
import importlib.util
from pathlib import Path
spec = importlib.util.find_spec("fastmcp")
print("fastmcp_spec=", spec)
if spec and spec.origin:
    root = Path(spec.origin).parent
    print("fastmcp_root=", root)
    for p in sorted(root.rglob("*.py")):
        text = p.read_text(errors="replace")
        if "SkipJsonSchema" in text or "include_in_schema" in text or "ignore_unknown" in text:
            print(p)
            for i, line in enumerate(text.splitlines(), 1):
                if "SkipJsonSchema" in line or "include_in_schema" in line or "ignore_unknown" in line:
                    print(f"{i}:{line}")
PY

Repository: ArcReel/ArcReel

Length of output: 43334


🏁 Script executed:

set -e
printf '%s\n' '--- local cached mcp sources ---'
find /root/.cache /tmp -type f \( -name 'fastmcp.py' -o -path '*/mcp/server/fastmcp/*' -o -name 'mcp-1.29.0*.whl' -o -name 'mcp-1.29.0*.tar.gz' \) 2>/dev/null | head -n 100

printf '%s\n' '--- repository remote MCP tests and tool registration ---'
rg -n -C 12 'build_remote_mcp_server|list_tools|call_tool|generate_episode_script|SkipJsonSchema|scope' tests/integration/server tests/unit/server tests 2>/dev/null | rg -n -C 6 'remote_mcp|generate_episode_script|SkipJsonSchema|scope|call_tool|list_tools' | head -n 700

printf '%s\n' '--- lock metadata for exact dependency ---'
sed -n '1850,1875p' uv.lock

Repository: ArcReel/ArcReel

Length of output: 50371


🌐 Web query:

mcp 1.29.0 FastMCP SkipJsonSchema tool unknown arguments function binding source

💡 Result:

<search_synthesis>
In FastMCP (specifically in versions around 1.29.0), the handling of "unknown arguments" in tool calls is a known behavior where unrecognized arguments are silently ignored rather than causing a validation error [1][2]. This behavior occurs because the FastMCP tool-call binding mechanism prioritizes mapping known parameters from the function&#39;s signature and disables the strict JSON Schema additionalProperties: false check for backwards compatibility [2]. Consequently, if an LLM provides an argument not defined in the tool&#39;s schema, it is dropped during processing instead of triggering a failure [2]. Regarding the technical components mentioned: - SkipJsonSchema: This is a Pydantic 2 utility (pydantic.json_schema.SkipJsonSchema) used within the FastMCP codebase to prevent internal callable fields (such as fn, forwarding_fn, or serializer) from being included in the generated JSON schema for a tool [3][4][5]. It is not related to the runtime handling of tool arguments themselves, but rather to the internal Pydantic model serialization [3]. - Function Binding: FastMCP automatically binds Python function parameters to MCP tool arguments by inspecting type annotations [6][7][8]. Because it relies on these signatures, it does not natively support *args or kwargs [7][8]. For users requiring stricter validation (rejecting unknown arguments), community solutions involve creating subclasses that explicitly diff the raw arguments against the tool&#39;s published schema and raising an error if a mismatch occurs [2]. Note that in newer versions (e.g., mcp 2.0.0+), FastMCP was renamed to MCPServer, and users should refer to official migration documentation when upgrading [9][6].
</search_synthesis>

<source_evidence>

<title>e10f0f5 Reject unknown tool arguments instead of silently dropping them (`#132`)</title> https://github.com/yutori-ai/yutori-mcp/commit/e10f0f58b1107e50fc9851a35f55065c0d018acd # e10f0f5 Reject unknown tool arguments instead of silently dropping them (`#132`) - SHA: e10f0f58b1107e50fc9851a35f55065c0d018acd - Repository: yutori-ai/yutori-mcp - Author: dhruvbatra - Date: 2026-07-05T22:33:04Z - +56 -9 in 3 files - Verified: yes --- Reject unknown tool arguments instead of silently dropping them (`#132`) FastMCP&`#39`;s tool-call binding disables the low-level Server&`#39`;s jsonschema additionalProperties:false check "for backwards compatibility" (see mcp.server.fastmcp.server.FastMCP._setup_handlers) and only binds parameter names it recognizes from the function signature, silently dropping anything else. This defeats the extra="forbid" intent documented on schemas.ToolInput for the MCP transport path specifically: a misspelled or hallucinated argument name from an LLM caller was silently ignored instead of failing loudly. Add _StrictArgsFastMCP, a thin FastMCP subclass that diffs the raw argument dict against the tool&`#39`;s published parameter schema before FastMCP&`#39`;s own binding drops anything unrecognized, raising ToolError on a mismatch. Falls back to skipping validation (with a warning) if the undocumented `_tool_manager`/`tool.parameters` internals it reads are ever renamed by a future `mcp` release, so a dependency upgrade degrades gracefully instead of crashing the server. Flips tests/test_server.py::test_unknown_argument_silently_accepted to pin the new rejection behavior (renamed to test_unknown_argument_rejected) and adds a sibling test confirming known arguments are unaffected. Updates TOOLS.md, which had been deliberately changed to document the old silently-ignored behavior. Claude-Session: https://claude.ai/code/session_019pL81nTEpijvLYc56HYWyX Co-authored-by: Claude <noreply@anthropic.com> ## Changed Files | File | Status | + | - | | --- | --- | --- | --- | | TOOLS.md | modified | 1 | 1 | | src/yutori_mcp/server.py | modified | 39 | 1 | | tests/test_server.py | modified | 16 | 7 | <title>Cannot produce `json_schema` from FastMCP Tool Type</title> GitHub issue 3046 in jlowin/fastmcp (link omitted to avoid creating a cross-reference) Callables on BaseModels break the ability to generate Json Schemas from them using Pydantic. ... We should annotate callables on models like tools (resources, prompts, etc) such that pydantic doesnt include them in the json schema ... ``` from pydantic.json_schema import SkipJsonSchema ... class Tool(BaseModel): # ... other fields ... serializer: SkipJsonSchema[Callable[[Any], str] | None] = None auth: SkipJsonSchema[Callable[[Any], bool] | list[Callable[[Any], bool]] | None] = None ``` ... > ### Investigation Complete > > I&`#39`;ve thoroughly investigated this issue. Here&`#39`;s my analysis: > > ### Recommendation > > **Apply the proposed fix using `SkipJsonSchema` annotation** from `pydantic.json_schema` to exclude callable fields from JSON schema generation. This is the correct Pydantic 2 approach for fields that shouldn&`#39`;t appear in generated schemas. > > The issue affects 9 BaseModel classes with callable fields. Some already use `exclude=True` for `auth` fields, but other callable fields (`serializer`, `fn`, `forwarding_fn`, `parent_tool`) are missing this protection. > > ### Findings > > > Affected Classes and Fields > > | Class | File | Callable Fields | Status | > |-------|------|----------------|--------| > | `Tool` | src/fastmcp/tools/tool.py | `serializer` (line 142), `auth` (line 148) | `auth` already excluded ✓ | > | `FunctionTool` | src/fastmcp/tools/function_tool.py | `fn` (line 84) | **Needs fix** | > | `TransformedTool` | src/fastmcp/tools/tool_transform.py | `fn` (line 257), `forwarding_fn` (line 258), `parent_tool` (line 256) | **Needs fix** | ... > | `Resource` | src/fastmcp/resources/resource.py | `auth` (line 228) | Already excluded ✓ | > | `FunctionResource` | src/fastmcp/resources/function_resource.py | `fn` (line 76) | **Needs fix** | > | `ResourceTemplate` | src/fastmcp/resources/template.py | `auth` (line 119) | Already excluded ✓ | > | `FunctionResourceTemplate` | src/fastmcp/resources/template.py | `fn` (line 330) | **Needs fix** | ... > | `Prompt` | src/fastmcp/prompts/prompt.py | `auth` (line 197) | Already excluded ✓ | > | `FunctionPrompt` | src/fastmcp/prompts/function_prompt.py | `fn` (line 76) | **Needs fix** | > > **Note:** `auth` fields already use `Field(..., exclude=True)`. The problem is with the other callable fields. > > > > Why This Matters > > When users try to generate JSON schemas from these BaseModel classes (e.g., for OpenAPI specs, validation, or type introspection), Pydantic fails because callable types cannot be serialized to JSON Schema. This breaks legitimate use cases like: > > - Generating OpenAPI documentation > - Creating TypeScript types from Python models > - Using `model_json_schema()` for introspection > - Third-party tools that expect valid JSON schemas > > > > ### Detailed Action Plan ... > > > Implementation Steps > > 1. **Add the import** to each affected file: ... > ```python > from pydantic.json_schema import SkipJsonSchema > ``` > > 2. **Update field annotations** to wrap the callable type with `SkipJsonSchema[]`: > > **For Tool (src/fastmcp/tools/tool.py:142-147):** ... > ```python > serializer: Annotated[ > SkipJsonSchema[ToolResultSerializerType | None], > Field(description="Deprecated. Return ToolResult from your tools for full control over serialization."), > ] = None > ``` > > **For FunctionTool (src/fastmcp/tools/function_tool.py:84):** > ```python > fn: SkipJsonSchema[Callable[..., Any]] > ``` > > **For TransformedTool (src/fastmcp/tools/tool_transform.py:256-258):** ... > ```python > parent_tool: SkipJsonSchema[Tool] > fn: SkipJsonSchema[Callable[..., Any]] > forwarding_fn: SkipJsonSchema[Callable[..., Any]] > ``` > > **For FunctionResource (src/fastmcp/resources/function_resource.py:76):** > ```python …[truncated] <title>src/fastmcp/tools/tool_transform.py at 5e0211dd · PrefectHQ/fastmcp</title> https://github.com/jlowin/fastmcp/blob/5e0211dd/src/fastmcp/tools/tool_transform.py from pydantic ... class TransformedTool(Tool): """A tool that is transformed from another tool. This class represents a tool that has been created by transforming another tool. It supports argument renaming, schema modification, custom function injection, structured output control, and provides context for the forward() and forward_raw() functions. ... schema-based ( ... renaming, dropping, ... forward() to ... but can be ... Attributes: parent_tool ... The original tool that this tool was transformed from. fn: The function to execute when this tool is called (either the forwarding function for pure transformations or a custom user function). forwarding_fn: Internal function that handles argument transformation and validation when forward() is called from custom functions. """ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True) parent_tool: SkipJsonSchema[Tool] fn: SkipJsonSchema[Callable[..., Any]] forwarding_fn: SkipJsonSchema[ Callable[..., Any] ] # Always present, handles arg transformation transform_args: dict[str, ArgTransform] async def run(self, arguments: dict[str, Any]) -> ToolResult: """Run the tool with context set for forward() functions. This method executes the tool&`#39`;s function while setting up the context that allows forward() and forward_raw() to work correctly within custom functions. Args: arguments: Dictionary of arguments to pass to the tool&`#39`;s function. Returns: ToolResult object containing content and optional structured ... . """ # Fill in missing arguments with schema defaults to ensure # ArgTransform defaults take precedence over function defaults arguments = arguments.copy() properties = self.parameters.get("properties", {}) ... for param_name, param_schema in properties.items(): if param_name not in arguments and "default" in param_schema: # Check if this parameter has a default_factory from transform_args # We need to call the factory for each run, not use the cached schema value has_factory_default = False if self.transform_args: # Find the original parameter name that maps to this param_name for orig_name, transform in self.transform_args.items(): transform_name = ( transform.name if transform.name is not NotSet else orig_name ) if ( transform_name == param_name and transform.default_factory is not NotSet ): # Type check to ensure default_factory is callable if callable(transform.default_factory): arguments[param_name] = transform.default_factory() has_factory_default = True break if not has_factory_default: arguments[param_name] = param_schema["default"] ... Deprecated ... , ) ... if transform_fn is not None: parsed_fn = ParsedFunction.from_function(transform_fn, validate=False) else: parsed_fn = None # Validate transform_args parent_params = set(tool.parameters.get("properties", {}).keys()) unknown_args = set(transform_args.keys()) - parent_params if unknown_args: raise ValueError( f"Unknown arguments in transform_args: {&`#39`;, &`#39`;.join(sorted(unknown_args))}. " f"Parent tool `{tool.name}` has: {&`#39`;, &`#39`;.join(sorted(parent_params))}" ) # Always create the forwarding transform schema, forwarding_fn = cls._create_forwarding_transform(tool, transform_args) ... # Handle output schema if output_schema is NotSet: # Use smart fallback: try custom function, then parent if transform_fn is not None: # parsed fn is not none here final_output_schema = cast(ParsedFunction, parsed_fn).output_schema ... if final_output_schema is None: # Check if function returns ToolResult - if so, don&`#39`;t fall back to parent return_annotation = inspect.signature( transform_fn ).return_annotation if return_annotation is ToolResult: final_output_schema = None else: final_output_schema = tool.output_schema else: final_output_schema = tool.output_schema else: final_output_schema = cast(dict | None, output_schema) ... if transform_fn is None: # User wants…[truncated] <title>src/fastmcp/tools/tool.py</title> https://github.com/jlowin/fastmcp/blob/5e0211dd/src/fastmcp/tools/tool.py from mcp.types import ... from pydantic import BaseModel, Field, model_validator from pydantic.json_schema import SkipJsonSchema ... class Tool(FastMCPComponent): """Internal tool registration info.""" KEY_PREFIX: ClassVar[str] = "tool" parameters: Annotated[ dict[str, Any], Field(description="JSON schema for tool parameters") ] output_schema: Annotated[ dict[str, Any] | None, Field(description="JSON schema for tool output") ] = None annotations: Annotated[ ToolAnnotations | None, Field(description="Additional annotations about the tool"), ] = None execution: Annotated[ ToolExecution | None, Field(description="Task execution configuration (SEP-1686)"), ] = None serializer: Annotated[ SkipJsonSchema[ToolResultSerializerType | None], Field( description="Deprecated. Return ToolResult from your tools for full control over serialization." ), ] = None auth: Annotated[ SkipJsonSchema[AuthCheckCallable | list[AuthCheckCallable] | None], Field(description="Authorization checks for this tool", exclude=True), ] = None timeout: Annotated[ float | None, Field( description="Execution timeout in seconds. If None, no timeout is applied." ), ] = None `@model_validator`(mode="after") def _validate_tool_name(self) -> Tool: """Validate tool name according to MCP specification (SEP-986).""" validate_and_warn_tool_name(self.name) return self def to_mcp_tool( ... Convert the FastMCP tool to an MCP tool ... elif self.annotations and self.annotations.title: ... title = self.annotations.title return ... =overrides.get("name", self.name), ... title=overrides.get("title", title), ... =overrides. ... ("description", ... .description), ... ("icons", self.icons ... get("annotations", self.annotations), ... ("execution", self.execution ... ] # _meta is Pydantic alias for meta field ... "_meta", self.get_meta() ... `@classmethod` def from_function( cls, fn: Callable[..., Any], *, name: str | None = None, version: str | int | None = None, title: str | None = None, description: str | None = None, icons: list[Icon] | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, output_schema: dict[str, Any] | NotSetT | None = NotSet, serializer: ToolResultSerializerType | None = None, # Deprecated meta: dict[str, Any] | None = None, task: bool | TaskConfig | None = None, timeout: float | None = None, auth: AuthCheckCallable | list[AuthCheckCallable] | None = None, ) -> FunctionTool: """Create a Tool from a function.""" from fastmcp.tools.function_tool import FunctionTool return FunctionTool.from_function( fn=fn, name=name, version=version, title=title, description=description, icons=icons, tags=tags, annotations=annotations, exclude_args=exclude_args, output_schema=output_schema, serializer=serializer, meta=meta, task=task, timeout=timeout, auth=auth, ) async def run(self, arguments: dict[str, Any]) -> ToolResult: """ Run the tool with arguments. This method is not implemented in the base Tool class and must be implemented by subclasses. `run()` can EITHER return a list of ContentBlocks, or a tuple of (list of ContentBlocks, dict of structured output). """ raise NotImplementedError("Subclasses must implement run()") def convert_result(self, raw_value: Any) -> ToolResult: """Convert a raw result to ToolResult. Handles ToolResult passthrough and converts raw values using the tool&`#39`;s attributes (serializer, output_schema) for proper conversion. """ if isinstance(raw_value, ToolResult): return raw_value content = _convert_to_content(raw_value, serializer=self.serializer) # Skip structured content for ContentBlock types only if no output_schema # (if output_schema exi…[truncated]

Citations:


🌐 Web query:

GitHub modelcontextprotocol python-sdk v1.29.0 FunctionTool SkipJsonSchema parsed function run arguments

💡 Result:

<search_synthesis>
In the context of the Model Context Protocol (MCP) Python SDK, the SkipJsonSchema annotation and FunctionTool are not part of the official modelcontextprotocol/python-sdk codebase [1][2][3]. Instead, these components belong to the fastmcp library [4][5][6], a separate project often used in conjunction with or alongside MCP-related tooling [7][4]. The SkipJsonSchema annotation, imported from pydantic.json_schema, is used in fastmcp to exclude callable fields (such as fn) from JSON schema generation [4][5][6]. This prevents Pydantic from attempting to serialize non-serializable callables into the tool&#39;s JSON schema [4]. Regarding argument parsing in the official modelcontextprotocol/python-sdk: 1. The SDK automatically inspects function signatures and type hints to generate JSON schemas [8][9][10]. 2. Incoming JSON-RPC arguments are validated against a dynamically generated Pydantic model (arg_model) [8][10]. 3. The func_metadata utility handles pre-parsing (e.g., json.loads() for string values) before validating the arguments [8][10]. 4. Validated arguments are then passed to the user-defined function via model_dump_one_level() [8]. If you are encountering issues with argument parsing in the official SDK, ensure your type hints are correctly defined, as they serve as the contract for argument validation [9]. The official SDK maintains v1.x as a maintenance line, while v2.x is the current stable release [7][3].
</search_synthesis>

<source_evidence>

<title>v1.29.0</title> https://github.com/modelcontextprotocol/python-sdk/releases/tag/v1.29.0 # v1.29.0 - Tag: v1.29.0 - Repository: modelcontextprotocol/python-sdk - Published: 2026-07-28T13:37:06Z - Author: maxisbey --- ## What&`#39`;s Changed * [v1.x] Route Context.report_progress() to the originating request stream by `@maxisbey` in https://github.com/modelcontextprotocol/python-sdk/pull/2994 * [v1.x] docs: publish llms.txt and markdown renditions of the docs by `@maxisbey` in https://github.com/modelcontextprotocol/python-sdk/pull/3029 * [v1.x] docs: pin mkdocs<2 by `@maxisbey` in https://github.com/modelcontextprotocol/python-sdk/pull/3074 * [v1.x] Add Streamable HTTP request body limits by `@Kludex` in https://github.com/modelcontextprotocol/python-sdk/pull/3101 * [v1.x] fix: reject trailing newline in tool-name validation by `@maxisbey` in https://github.com/modelcontextprotocol/python-sdk/pull/3086 * [v1.x] ci: pick the docs toolchain per worktree in build-docs.sh by `@maxisbey` in https://github.com/modelcontextprotocol/python-sdk/pull/3082 * [v1.x] Move the v1.x docs to /v1/ and mark v1.x as the maintenance line by `@maxisbey` in https://github.com/modelcontextprotocol/python-sdk/pull/3177 **Full Changelog**: https://github.com/modelcontextprotocol/python-sdk/compare/v1.28.1...v1.29.0 <title>v1.28.1...v1.29.0</title> https://github.com/modelcontextprotocol/python-sdk/compare/v1.28.1...v1.29.0 # v1.28.1...v1.29.0 - Repository: modelcontextprotocol/python-sdk - Status: ahead - Ahead by: 7 - Behind by: 0 - Total commits: 7 - Files changed: 21 ## Commits - 9678a3b [v1.x] Route Context.report_progress() to the originating request stream (`#2994`) - 0e3a604 [v1.x] docs: publish llms.txt and markdown renditions of the docs (`#3029`) - ba33472 [v1.x] docs: pin mkdocs<2 (`#3074`) - 5f0b6af [v1.x] Add Streamable HTTP request body limits (`#3101`) - e828374 [v1.x] fix: reject trailing newline in tool-name validation (`#3086`) - c0c5a9d [v1.x] ci: pick the docs toolchain per worktree in build-docs.sh (`#3082`) - 98b7159 [v1.x] Move the v1.x docs to /v1/ and mark v1.x as the maintenance line (`#3177`) ## Changed Files | File | Status | + | - | | --- | --- | --- | --- | | .github/workflows/deploy-docs.yml | removed | 0 | 57 | | .github/workflows/shared.yml | modified | 17 | 0 | | README.md | modified | 8 | 10 | | docs/hooks/llms_txt.py | added | 178 | 0 | | docs/index.md | modified | 11 | 3 | | docs/installation.md | modified | 5 | 3 | | docs/server.md | modified | 9 | 0 | | mkdocs.yml | modified | 10 | 4 | | pyproject.toml | modified | 4 | 2 | | scripts/build-docs.sh | removed | 0 | 54 | | scripts/docs/build.sh | added | 19 | 0 | | src/mcp/server/fastmcp/resources/templates.py | modified | 1 | 1 | | src/mcp/server/fastmcp/server.py | modified | 6 | 1 | | src/mcp/server/streamable_http_manager.py | modified | 82 | 2 | | src/mcp/shared/tool_name_validation.py | modified | 1 | 1 | | tests/issues/test_176_progress_token.py | modified | 9 | 3 | | tests/server/fastmcp/resources/test_resource_template.py | modified | 15 | 0 | | tests/server/fastmcp/test_server.py | modified | 9 | 0 | | tests/server/test_streamable_http_manager.py | modified | 133 | 8 | | tests/shared/test_tool_name_validation.py | modified | 5 | 0 | | uv.lock | modified | 1 | 1 | <title>mcp v1.29.0</title> https://pypi.org/project/mcp/1.29.0/ # mcp v1.29.0 ... - Documentation: https://py.sdk.modelcontextprotocol.io/v1/ - Homepage: https://modelcontextprotocol.io - Issues: https://github.com/modelcontextprotocol/python-sdk/issues - Repository: https://github.com/modelcontextprotocol/python-sdk ... -badge]][ ... -url] [![ ... ][mit-badge]][mit-url] [![ ... Version][python ... badge]][python-url] [![Documentation][docs-badge]][docs-url] [![Protocol][protocol-badge]][protocol-url] [![Specification][spec-badge]][spec-url] > **This documents v1.x, the maintenance line of the MCP Python SDK.** v2 is the current stable release: `pip install mcp` now installs 2.x. See the v2 documentation and the migration guide for what changed and how to upgrade. > > **Staying on v1.x?** Keep a `<2` upper bound on your requirement (for example `mcp>=1.28,<2`) so an ... pinned resolve stays on ... x line. ... 1.x remains supported for existing deployments and continues to receive critical bug fixes and security patches; its documentation is ... . ## Table of Contents ... The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of ... context from the actual ... M interaction. This Python SDK implements the full MCP specification, making it ... - Build MCP clients that can connect to any MCP server - Create MCP servers that expose resources, prompts and tools - Use standard transports like stdio, SSE, and Streamable HTTP - Handle all MCP protocol messages and lifecycle events ... =True) ... # Add an addition tool `@mcp.tool`() def add(a: int, b: int) -> int: """Add two numbers""" return a + b ... # Run with streamable HTTP transport if __name__ == "__main__": mcp.run(transport="streamable-http") ``` ... The Model Context Protocol (MCP) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can: ... - Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM&`#39`;s context) - Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect) - Define interaction patterns through **Prompts** (reusable templates for LLM interactions) - And more! ... - Building Servers -- tools, resources, prompts, logging, completions, sampling, elicitation, transports, ASGI mounting - Writing Clients -- connecting to servers, using tools/resources/prompts, display utilities - Authorization -- OAuth 2.1, token verification, client authentication - Low-Level Server -- direct handler registration for advanced use cases - Protocol Features -- MCP primitives, server capabilities - Testing -- in-memory transport testing with pytest - API Reference - Experimental Features (Tasks) - Model Context Protocol documentation - Model Context Protocol specification - Officially supported servers <title>Cannot produce `json_schema` from FastMCP Tool Type</title> GitHub issue 3046 in PrefectHQ/fastmcp (link omitted to avoid creating a cross-reference) to generate Json Schemas from ... using Pydantic. ... ``` from pydantic.json_schema import SkipJsonSchema ... class Tool(BaseModel): # ... other fields ... serializer: SkipJsonSchema[Callable[[Any], str] | None] = None auth: SkipJsonSchema[Callable[[Any], bool] | list[Callable[[Any], bool]] | None] = None ``` ... > ### Investigation Complete > > I&`#39`;ve thoroughly investigated this issue. Here&`#39`;s my analysis: > > ### Recommendation > > **Apply the proposed fix using `SkipJsonSchema` annotation** from `pydantic.json_schema` to exclude callable fields from JSON schema generation. This is the correct Pydantic 2 approach for fields that shouldn&`#39`;t appear in generated schemas. > > The issue affects 9 BaseModel classes with callable fields. Some already use `exclude=True` for `auth` fields, but other callable fields (`serializer`, `fn`, `forwarding_fn`, `parent_tool`) are missing this protection. > > ### Findings > > > Affected Classes and Fields > > | Class | File | Callable Fields | Status | > |-------|------|----------------|--------| > | `Tool` | src/fastmcp/tools/tool.py | `serializer` (line 142), `auth` (line 148) | `auth` already excluded ✓ | > | `FunctionTool` | src/fastmcp/tools/function_tool.py | `fn` (line 84) | **Needs fix** | > | `TransformedTool` | src/fastmcp/tools/tool_transform.py | `fn` (line 257), `forwarding_fn` (line 258), `parent_tool` (line 256) | **Needs fix** | ... > | `Resource` | src/fastmcp/resources/resource.py | `auth` (line 228) | Already excluded ✓ | > | `FunctionResource` | src/fastmcp/resources/function_resource.py | `fn` (line 76) | **Needs fix** | ... > | `ResourceTemplate` | src/fastmcp/resources/template.py | `auth` (line 119) | Already excluded ✓ | ... > | `FunctionResourceTemplate` | src/fastmcp/resources/template.py | `fn` (line 330) | **Needs fix** | ... > | `Prompt` | src/fastmcp/prompts/prompt.py | `auth` (line 197) | Already excluded ✓ | > | `FunctionPrompt` | src/fastmcp/prompts/function_prompt.py | `fn` (line 76) | **Needs fix** | > > **Note:** `auth` fields already use `Field(..., exclude=True)`. The problem is with the other callable fields. > > > > Why This Matters > > When users try to generate JSON schemas from these BaseModel classes (e.g., for OpenAPI specs, validation, or type introspection), Pydantic fails because callable types cannot be serialized to JSON Schema. This breaks legitimate use cases like: > > - Generating OpenAPI documentation > - Creating TypeScript types from Python models > - Using `model_json_schema()` for introspection > - Third-party tools that expect valid JSON schemas > > > > ### Detailed Action Plan > > > Implementation Steps > > 1. **Add the import** to each affected file: ... > ```python > from pydantic.json_schema import SkipJsonSchema > ``` > > 2. **Update field annotations** to wrap the callable type with `SkipJsonSchema[]`: > > **For Tool (src/fastmcp/tools/tool.py:142-147):** ... > ```python > serializer: Annotated[ > SkipJsonSchema[ToolResultSerializerType | None], > Field(description="Deprecated. Return ToolResult from your tools for full control over serialization."), > ] = None > ``` > > **For FunctionTool (src/fastmcp/tools/function_tool.py:84):** > ```python > fn: SkipJsonSchema[Callable[..., Any]] > ``` > > **For TransformedTool (src/fastmcp/tools/tool_transform.py:256-258):** ... > ```python > parent_tool: SkipJsonSchema[Tool] > fn: SkipJsonSchema[Callable[..., Any]] > forwarding_fn: SkipJsonSchema[Callable[..., Any]] > ``` > > **For FunctionResource (src/fastmcp/resources/function_resource.py:76):** ... > ```python > fn: SkipJsonSchema[Callable[..., Any]] > ``` ... > > **For FunctionResourceTemplate (src/fastmcp/resources/template.py:330):** ... > ```python > fn: SkipJ…[truncated] <title>src/fastmcp/tools/function_tool.py</title> https://github.com/jlowin/fastmcp/blob/e87ede07/src/fastmcp/tools/function_tool.py from pydantic.json_schema import ... class FunctionTool(Tool): fn: SkipJsonSchema[Callable[..., Any]] def to_mcp_tool( self, **overrides: Any, ) -> mcp.types.Tool: """Convert the FastMCP tool to an MCP tool. Extends the base implementation to add task execution mode if enabled. """ # Get base MCP tool from parent mcp_tool = super().to_mcp_tool(**overrides) # Add task execution mode per SEP-1686 # Only set execution if not overridden and task execution is supported if self.task_config.supports_tasks() and "execution" not in overrides: mcp_tool.execution = ToolExecution(taskSupport=self.task_config.mode) return mcp_tool `@classmethod` def from_function( cls, fn: Callable[..., Any], *, metadata: ToolMeta | None = None, # Keep individual params for backwards compat name: str | None = None, version: str | int | None = None, title: str | None = None, description: str | None = None, icons: list[Icon] | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, output_schema: dict[str, Any] | NotSetT | None = NotSet, serializer: ToolResultSerializerType | None = None, meta: dict[str, Any] | None = None, task: bool | TaskConfig | None = None, timeout: float | None = None, auth: AuthCheck | list[AuthCheck] | None = None, ) -> FunctionTool: """Create a FunctionTool from a function. Args: fn: The function to wrap metadata: ToolMeta object with all configuration. If provided, individual parameters must not be passed. name, title, etc.: Individual parameters for backwards compatibility. Cannot be used together with metadata parameter. """ # Check mutual exclusion individual_params_provided = ( any( x is not None and x is not NotSet for x in [ name, version, title, description, icons, tags, annotations, meta, task, serializer, timeout, auth, ] ) or output_schema is not NotSet or exclude_args is not None ) if metadata is not None and individual_params_provided: raise TypeError( "Cannot pass both &`#39`;metadata&`#39`; and individual parameters to from_function(). " "Use metadata alone or individual parameters alone." ) # Build metadata from kwargs if not provided if metadata is None: metadata = ToolMeta( name=name, version=version, title=title, description=description, icons=icons, tags=tags, output_schema=output_schema, annotations=annotations, meta=meta, task=task, exclude_args=exclude_args, serializer=serializer, timeout=timeout, auth=auth, ) if metadata.serializer is not None and fastmcp.settings.deprecation_warnings: warnings.warn( "The `serializer` parameter is deprecated. " "Return ToolResult from your tools for full control over serialization. " "See https://gofastmcp.com/servers/tools#custom-serialization for migration examples.", DeprecationWarning, stacklevel=2, ) if metadata.exclude_args and fastmcp.settings.deprecation_warnings: warnings.warn( "The `exclude_args` parameter is deprecated as of FastMCP 2.14. " "Use dependency injection with `Depends()` instead for better lifecycle management. " "See https://gofastmcp.com/servers/dependency-injection#using-depends for examples.", DeprecationWarning, stacklevel=2, ) parsed_fn = ParsedFunction.from_function(fn, exclude_args=metadata.exclude_args) func_name = metadata.name or parsed_fn.name if func_name == " ": raise ValueError("You must provide a name for lambda functions") # Normalize task to TaskConfig task_value = metadata.task if task_value is None: task_config = TaskConfig(mode="forbidden") elif isinstance(task_value, bool): task_config = TaskConfig.from_bool(task_value) else: task_config = task_value task_config.validate_function(fn, func_name) # Handle output_schema if isinstance(metadata.output_schema, NotSetT): final_output_schema = parsed_fn.output_schema else: final_output_schema = metadata.output_schema if final_output…[truncated]

Citations:


🏁 Script executed:

set -e
base='https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/v1.29.0/src/mcp/server/fastmcp'
for file in tools/function_tool.py tools/tool.py server.py; do
  url="$base/$file"
  echo "--- $url ---"
  curl -fsSL "$url" | rg -n -C 12 'class FunctionTool|def run|def from_function|SkipJsonSchema|arguments|additionalProperties|unknown' | head -n 260
done

Repository: ArcReel/ArcReel

Length of output: 7632


🏁 Script executed:

set -e
api='https://api.github.com/repos/modelcontextprotocol/python-sdk/git/trees/v1.29.0?recursive=1'
curl -fsSL "$api" | python3 -c '
import json,sys
d=json.load(sys.stdin)
for x in d.get("tree",[]):
    p=x.get("path","")
    if "fastmcp" in p or "func_metadata" in p or p.endswith("/tools.py"):
        print(p)
'
printf '%s\n' '--- fastmcp server registration and call path ---'
curl -fsSL 'https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/v1.29.0/src/mcp/server/fastmcp/server.py' |
  rg -n -C 18 'def call_tool|def tool|ToolManager|call_tool|arguments|func_metadata|additionalProperties' | head -n 500
printf '%s\n' '--- metadata implementation ---'
for path in src/mcp/server/fastmcp/utilities/func_metadata.py src/mcp/server/fastmcp/utilities/func_metadata.py src/mcp/server/fastmcp/tools/base.py src/mcp/server/fastmcp/tools/tool.py src/mcp/server/fastmcp/tools/function_tool.py; do
  url="https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/v1.29.0/$path"
  if curl -fsSL "$url" > /tmp/mcp-source.py; then
    echo "--- $path ---"
    rg -n -C 18 'SkipJsonSchema|ParsedFunction|arg_model|model_dump|def run|def call|arguments|extra' /tmp/mcp-source.py | head -n 500
  fi
done

Repository: ArcReel/ArcReel

Length of output: 50373


区分省略的 scope 与显式 null。

FastMCP 会按函数签名绑定 scope。SkipJsonSchema 只会将其从 MCP schema 中隐藏。当前省略参数和传入 scope: null 都会绑定为 None,因此显式 null 会绕过迁移错误并继续使用默认的待编写范围。

嵌入式工具契约要求任何已提供的 scope 值(包括 null)返回 SCOPE_REMOVED_MESSAGE。请使用哨兵值区分两种情况。

建议修改
+_SCOPE_OMITTED = object()
+
 async def remote_generate_episode_script(
     ...
-    scope: SkipJsonSchema[object] = None,
+    scope: SkipJsonSchema[object] = _SCOPE_OMITTED,
 ) -> CallToolResult:
-    if scope is not None:
+    if scope is not _SCOPE_OMITTED:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant