Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 29 additions & 8 deletions docs/en/development/processing-failures.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ in its processing path.
|---|---|---|---|---|---|---|
| Memory / Atomic Memory | Partial | Covered | Partial | Covered | Partial | Missing |
| Topic Memory | Partial | Covered | Partial | Covered | Partial | Missing |
| Experience | Partial | N/A | Partial | Missing | Missing | Missing |
| Profile | Partial | N/A | Partial | Missing | Missing | Missing |
| Skill | Partial | N/A | Partial | Missing | Missing | Missing |
| Handoff | Partial | N/A | Partial | Missing | Missing | Missing |
| Experience | Partial | N/A | Partial | Partial | Missing | Missing |
| Profile | Partial | N/A | Partial | Partial | Missing | Missing |
| Skill | Partial | N/A | Partial | Partial | Missing | Missing |
| Handoff | Partial | N/A | Partial | Partial | Missing | Missing |
| Prompt | Partial | N/A | Partial | Missing | Missing | Missing |

Memory / Atomic Memory and Topic Memory are tracked at operation granularity while they are the active slices:
Expand Down Expand Up @@ -86,10 +86,31 @@ Topic Memory operation-level coverage:
| Publish / persistence | Persistence | Partial | Projection content and vector-completeness rules are business validation; storage-invariant failures map to `persistence` / `storage_failure`; unsupported capabilities report `configuration`; publisher conflicts keep their domain outcomes, and other storage failures keep unknown. |
| Usage accounting | Usage accounting | Missing | The window provider budget is reported as `generation` / `usage_limit`; accounting failures are not classified. |

The first two rollout slices cover Memory / Atomic Memory and Topic Memory: provider failures,
budget exhaustion, and extraction/reconciliation/domain validation are classified, and safe
accounting fields travel from the child Worker to the supervisor log. Other Families keep their
current behavior until their own slice lands; the matrix is updated in the same change as each slice.
Experience, Profile, Skill, and Handoff operation-level coverage:

| Operation | Stage | Status | Notes |
|---|---|---|---|
| `experience.incubate` | Business validation | Partial | Bounded input and out-of-window evidence carry explicit diagnostics; structural limits do not establish context overflow. |
| `profile.generate` | Business validation | Partial | Invalid Markdown carries the input count and original validation exception type without exposing the content. |
| Experience / Skill typed generation | Output parsing | Partial | Invalid typed results use the shared schema mapping; unclassified provider details remain unknown. |
| Dream (`refine_experience`, `revise_profile`, `derive_skill`, `revise_skill`) | Generation / business validation | Partial | Caught attempt failures emit `dream.attempt.failed` using the existing worker/request identity. Evidence limits, unavailable evidence, and rejected request targets carry `business_validation` and their failure location. |
| `handoff.generate` | Business validation | Partial | Objective, citation, rollover, and byte-limit violations are invalid output, not model resource exhaustion. |
| Publication / persistence | Persistence | Missing | These slices do not yet classify all storage entry points. |
| Usage accounting | Usage accounting | Missing | Accounting failures remain separate from processing outcomes. |

The matrices describe classification coverage, not completion of the whole issue. Dream failure
records contain bounded diagnostic fields only, never exception text or evidence content. Its
persisted run outcome and retry policy remain authoritative; the failure record is an observation,
not a new durable failure history. Durable-block classification currently covers Topic Memory only;
Dream uses its existing Run outcomes and attempt logs, not a durable scheduler block.

A timeout from Dream's own execution deadline produces the terminal Run code `budget_exceeded`.
Its attempt log reports `generation` / `usage_limit` with `failure_location=budget_exceeded` and
`exception_type=TimeoutError`. The stage identifies the owner of the execution budget. A bare
`TimeoutError` raised before that deadline expires follows the terminal `generation_failed` path
and logs `timeout` without inferring a stage.
Provider `InferenceTimeoutError` keeps its operation-specific stage and `timeout` reason and remains
retryable within the existing Run budget.

## Rollout

Expand Down
32 changes: 25 additions & 7 deletions docs/zh/development/processing-failures.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ Worker/supervisor 值(`worker_process`、`work_admission`、`supervisor`、`le
|---|---|---|---|---|---|---|
| Memory / Atomic Memory | Partial | Covered | Partial | Covered | Partial | Missing |
| Topic Memory | Partial | Covered | Partial | Covered | Partial | Missing |
| Experience | Partial | N/A | Partial | Missing | Missing | Missing |
| Profile | Partial | N/A | Partial | Missing | Missing | Missing |
| Skill | Partial | N/A | Partial | Missing | Missing | Missing |
| Handoff | Partial | N/A | Partial | Missing | Missing | Missing |
| Experience | Partial | N/A | Partial | Partial | Missing | Missing |
| Profile | Partial | N/A | Partial | Partial | Missing | Missing |
| Skill | Partial | N/A | Partial | Partial | Missing | Missing |
| Handoff | Partial | N/A | Partial | Partial | Missing | Missing |
| Prompt | Partial | N/A | Partial | Missing | Missing | Missing |

Memory / Atomic Memory 与 Topic Memory 作为当前切片,按 operation 粒度跟踪:
Expand Down Expand Up @@ -80,9 +80,27 @@ Topic Memory 的 operation 级覆盖:
| Publish / persistence | Persistence | Partial | 投影内容及向量完整性规则归业务校验;存储不变量失败归 `persistence` / `storage_failure`;不支持的能力归 `configuration`;publisher 冲突保留既有领域结果,其他存储失败保持 unknown。 |
| Usage accounting | Usage accounting | Missing | window provider budget 归为 `generation` / `usage_limit`;账务失败未分类。 |

前两个落地切片覆盖 Memory / Atomic Memory 与 Topic Memory:provider 失败、预算耗尽、提取/对账/领域校验
均有分类,安全计数字段可从子 Worker 传播到 supervisor 日志。其他 Family 在各自切片落地前保持现有行为;
每次切片在同一变更中更新本矩阵。
Experience、Profile、Skill 与 Handoff 的 operation 级覆盖:

| Operation | Stage | 状态 | 说明 |
|---|---|---|---|
| `experience.incubate` | Business validation | Partial | 有界输入和窗口外证据携带明确诊断;结构上限不证明模型上下文溢出。 |
| `profile.generate` | Business validation | Partial | 无效 Markdown 携带输入数量与原始校验异常类型,不暴露内容。 |
| Experience / Skill typed generation | Output parsing | Partial | 无效 typed 结果使用共享 schema 映射;无法分类的 provider 细节保持 unknown。 |
| Dream(`refine_experience`、`revise_profile`、`derive_skill`、`revise_skill`) | Generation / business validation | Partial | 捕获的 attempt 失败通过 `dream.attempt.failed` 记录,复用 worker/request 身份。证据超过上限、证据不可用或请求目标不受支持时,记录 `business_validation` 和失败位置。 |
| `handoff.generate` | Business validation | Partial | objective、引用、rollover、字节上限违规均为无效输出,不是模型资源耗尽。 |
| Publication / persistence | Persistence | Missing | 这些切片尚未分类全部存储入口。 |
| Usage accounting | Usage accounting | Missing | 账务失败仍与处理结果分离。 |

矩阵描述分类覆盖情况,不表示整个 issue 已完成。Dream 失败记录只包含有界诊断字段,不含异常文本或证据正文。
持久化 run 结果和重试规则仍是权威状态;日志属于观测,不新增持久化失败历史。当前 durable block 分类只覆盖
Topic Memory;Dream 使用既有 Run 结果和 attempt 日志,不产生持久化调度阻塞。

Dream 自身的执行期限耗尽时,Run 以 `budget_exceeded` 终止。对应 attempt 日志记录
`generation` / `usage_limit` 和 `failure_location=budget_exceeded`,与执行预算结果一致,同时保留
`exception_type=TimeoutError`。这里的 stage 表示执行预算的归属。期限尚未耗尽时抛出的裸
`TimeoutError` 走 `generation_failed` 终止路径,日志记录 `timeout`,省略未知阶段。
Provider 的 `InferenceTimeoutError` 保留操作对应的 stage 和 `timeout` 原因,仍可在既有 Run 预算内重试。

## 落地顺序

Expand Down
33 changes: 27 additions & 6 deletions src/powercontext/builtin/artifacts/experience/incubation.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@

from powercontext.builtin.artifacts.experience.models import ExperienceContent
from powercontext.builtin.inference import GenerationResult, InvalidInferenceOutputError, StructuredGenerator
from powercontext.builtin.processing_diagnostics import (
ProcessingDiagnostics,
ProcessingFailureReason,
ProcessingStage,
attach_diagnostics,
)
from powercontext.builtin.sources import CONTENT_SOURCE_NAME, ContentSource
from powercontext.sources import Source, SourceRef

Expand Down Expand Up @@ -136,9 +142,17 @@ def _incubation_input(
try:
return ExperienceIncubationInput(evidence=tuple(projected)), evidence
except ValidationError as error:
raise InvalidInferenceOutputError(
"experience-incubate",
"eligible Task Outcome evidence exceeded the bounded input contract",
raise attach_diagnostics(
InvalidInferenceOutputError(
"experience-incubate",
"eligible Task Outcome evidence exceeded the bounded input contract",
),
ProcessingDiagnostics(
stage=ProcessingStage.BUSINESS_VALIDATION,
input_item_count=len(projected),
failure_location="experience-incubate-input",
cause_exception_type=type(error).__name__,
),
) from error


Expand All @@ -161,9 +175,16 @@ def _selected_sources(values: tuple[str, ...], evidence: dict[str, SourceRef]) -
try:
source = evidence[evidence_id]
except KeyError:
raise InvalidInferenceOutputError(
"experience-incubate",
"candidate cited evidence outside the current Task Outcome window",
raise attach_diagnostics(
InvalidInferenceOutputError(
"experience-incubate",
"candidate cited evidence outside the current Task Outcome window",
),
ProcessingDiagnostics(
stage=ProcessingStage.BUSINESS_VALIDATION,
reason=ProcessingFailureReason.INVALID_OUTPUT,
failure_location="experience-incubate-evidence",
),
) from None
key = (source.source_type, source.source_id)
if key not in seen:
Expand Down
38 changes: 31 additions & 7 deletions src/powercontext/builtin/artifacts/handoff/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@
HandoffStatement,
)
from powercontext.builtin.inference import GenerationResult, InvalidInferenceOutputError, StructuredGenerator
from powercontext.builtin.processing_diagnostics import (
ProcessingDiagnostics,
ProcessingFailureReason,
ProcessingStage,
attach_diagnostics,
)
from powercontext.sources import Source

HandoffGenerationEvidenceType: TypeAlias = Literal["source", "artifact"]
Expand Down Expand Up @@ -146,9 +152,17 @@ async def generate(self, request: HandoffGenerationRequest, /) -> HandoffDraft:
omissions=tuple(_omission(value, citations) for value in output.omissions),
)
except ValidationError as error:
raise InvalidInferenceOutputError(
"handoff-generate",
"generated content violates the Handoff Draft contract",
raise attach_diagnostics(
InvalidInferenceOutputError(
"handoff-generate",
"generated content violates the Handoff Draft contract",
),
ProcessingDiagnostics(
stage=ProcessingStage.BUSINESS_VALIDATION,
reason=ProcessingFailureReason.INVALID_OUTPUT,
failure_location="handoff-generate-contract",
cause_exception_type=type(error).__name__,
),
) from error


Expand Down Expand Up @@ -230,7 +244,7 @@ def _statement(
citations: Mapping[str, HandoffCitation],
) -> HandoffStatement:
if not value.evidence_ids:
raise InvalidInferenceOutputError("handoff-generate", "statement does not cite evidence")
raise _invalid_handoff_output("statement does not cite evidence", "handoff-generation-citation") # noqa: TRY003
return HandoffStatement(
text=value.text,
citations=tuple(_citation(evidence_id, citations) for evidence_id in dict.fromkeys(value.evidence_ids)),
Expand All @@ -251,12 +265,22 @@ def _citation(evidence_id: str, citations: Mapping[str, HandoffCitation]) -> Han
try:
return citations[evidence_id]
except KeyError:
raise InvalidInferenceOutputError(
"handoff-generate",
"generated content cites evidence outside the request",
raise _invalid_handoff_output( # noqa: TRY003
"generated content cites evidence outside the request", "handoff-generation-citation"
) from None


def _invalid_handoff_output(detail: str, location: str) -> InvalidInferenceOutputError:
return attach_diagnostics(
InvalidInferenceOutputError("handoff-generate", detail),
ProcessingDiagnostics(
stage=ProcessingStage.BUSINESS_VALIDATION,
reason=ProcessingFailureReason.INVALID_OUTPUT,
failure_location=location,
),
)


def _validated_json(value: object) -> JsonValue:
try:
return _JSON_VALUE.validate_python(value)
Expand Down
17 changes: 16 additions & 1 deletion src/powercontext/builtin/artifacts/profile/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@
from powercontext.builtin.persistence.profile import ProfilePolicyRepository
from powercontext.builtin.persistence.sources import SourceRepository
from powercontext.builtin.persistence.tables import PROFILE_POLICIES_TABLE
from powercontext.builtin.processing_diagnostics import (
ProcessingDiagnostics,
ProcessingFailureReason,
ProcessingStage,
attach_diagnostics,
)
from powercontext.builtin.records import BaseValueConflictError, BaseValueNotFoundError
from powercontext.builtin.scope.errors import ScopeNotFoundError
from powercontext.builtin.scope.repository import ScopeRepository
Expand Down Expand Up @@ -266,7 +272,16 @@ async def _flush( # noqa: C901
except ValueError as error:
from powercontext.builtin.inference import InvalidInferenceOutputError

raise InvalidInferenceOutputError("profile-generate", "invalid Markdown") from error
raise attach_diagnostics(
InvalidInferenceOutputError("profile-generate", "invalid Markdown"),
ProcessingDiagnostics(
stage=ProcessingStage.BUSINESS_VALIDATION,
reason=ProcessingFailureReason.INVALID_OUTPUT,
input_item_count=len(evidence),
failure_location="profile-generate-markdown",
cause_exception_type=type(error).__name__,
),
) from error
if current is not None and current.content.content == markdown:
markdown = None
try:
Expand Down
Loading
Loading