Skip to content

Commit 61aafc6

Browse files
authored
feat: logic_tracking option to control scope of logic change tracking (#1732)
1 parent 2e89ce3 commit 61aafc6

20 files changed

Lines changed: 745 additions & 47 deletions

docs/docs/advanced_topics/memoization_keys.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ The following types are handled automatically (no custom key needed):
2929

3030
The key fragments are combined into a deterministic fingerprint. If the fingerprint matches a cached entry, the cached result is reused — unless **memo states** indicate it's stale (see [Memo state validation](#memo-state-validation) below).
3131

32-
In addition to input fingerprints, CocoIndex also validates against **function code fingerprints** and **[tracked context value](../programming_guide/context.md#tracked-context-keys) fingerprints**. If the function's code or any tracked context value it consumed has changed, the cached result is invalidated regardless of input matching.
32+
In addition to input fingerprints, CocoIndex also validates against **function code fingerprints** and **[tracked context value](../programming_guide/context.md#tracked-context-keys) fingerprints**. If the function's code or any tracked context value it consumed has changed, the cached result is invalidated regardless of input matching. You can control the scope of code change tracking with the [`logic_tracking` parameter](../programming_guide/function.md#logic_tracking).
3333

3434
## Customizing the memoization key
3535

docs/docs/programming_guide/function.md

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,19 +64,68 @@ See [Memoization Keys & States](../advanced_topics/memoization_keys.md) for deta
6464

6565
### Change tracking
6666

67-
The logic of a function decorated with `@coco.fn` is tracked based on the content of the function. When a function's implementation changes, CocoIndex detects this and re-executes the places where it's called.
67+
Every `@coco.fn` function has its code fingerprinted. These fingerprints propagate up the call chain: when a function's code changes, all memoized callers and components that transitively depend on it are invalidated. Two parameters let you customize this:
6868

69-
In addition to code changes, [tracked context values](./context.md#tracked-context-keys) consumed via `coco.use_context()` are also tracked. When a tracked context value changes between runs, functions that used it are re-executed — even if the function code and inputs are unchanged.
69+
- **`logic_tracking`** — controls the *scope* of automatic code change tracking
70+
- **`version`** — provides explicit manual control over when dependent memos are invalidated
7071

71-
You can also explicitly control the behavior version with a `version` option:
72+
#### `logic_tracking`
73+
74+
The `logic_tracking` parameter controls whether and how function code changes are detected:
75+
76+
- **`"full"` (default):** Track this function's code AND all transitively called `@coco.fn` functions' code. A change anywhere in the call chain invalidates dependent memos.
77+
- **`"self"`:** Track only this function's own code. Changes in called functions do not propagate through this function.
78+
- **`None`:** Don't track this function's code at all. Code changes to this function are invisible to the change tracking system.
79+
80+
#### `version`
81+
82+
The `version` parameter lets you explicitly invalidate dependent memos by bumping an integer:
7283

7384
```python
74-
@coco.fn(memo=True, version=2)
85+
@coco.fn(version=2)
7586
def process_chunk(chunk: Chunk) -> Embedding:
76-
# Bumping version forces re-execution even if code looks the same
87+
# Bumping version invalidates all memoized callers, even if code looks the same
7788
return embed(chunk.text)
7889
```
7990

91+
#### Common patterns
92+
93+
These parameters can be set on any `@coco.fn` function — not just memoized ones. A non-memoized function's fingerprint still propagates to its memoized callers and components.
94+
95+
**Fully automatic (default)** — use `logic_tracking="full"` (or omit it) without setting `version`. Any code change in the function or its callees invalidates dependent memos. This always just works.
96+
97+
```python
98+
@coco.fn
99+
async def process_file(file: FileLike) -> list[Chunk]:
100+
# Any change here or in called @coco.fn functions invalidates dependent memos
101+
text = await file.read_text()
102+
return split_and_embed(text)
103+
```
104+
105+
**Manual, precise control** — use `logic_tracking="self"` with `version`. You decide what counts as a behavior change by bumping `version`, without being affected by implementation detail changes (performance optimizations, logging tweaks, refactoring, etc.).
106+
107+
```python
108+
@coco.fn(logic_tracking="self", version=3)
109+
async def process(data: str) -> str:
110+
# Bump version when behavior changes (e.g., new output format).
111+
# Internal refactors or logging changes won't trigger reprocessing.
112+
return await transform(data)
113+
```
114+
115+
**Opt out of tracking** — use `logic_tracking=None` for functions with a stable contract (where code changes don't affect output), or functions whose changes don't affect behavior (e.g., logging, performance hints). This prevents unnecessary reprocessing when only internals change.
116+
117+
```python
118+
@coco.fn(logic_tracking=None)
119+
def embed(text: str) -> list[float]:
120+
# Contract is stable: same input always produces the same embedding.
121+
# Internal changes (e.g., switching backends) are handled by version bumps.
122+
return model.encode(text)
123+
```
124+
125+
:::note
126+
[Tracked context values](./context.md#tracked-context-keys) consumed via `coco.use_context()` are always tracked regardless of the `logic_tracking` setting. Even with `logic_tracking=None`, a change in a tracked context value still invalidates dependent memos.
127+
:::
128+
80129
### Async adapter
81130

82131
Use `@coco.fn.as_async` when you need an **async** interface for a function that has a sync underlying implementation. This is useful for compute-intensive leaf functions, and is required for features like [batching](#batching) and [runner](#runner).

python/cocoindex/_internal/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from .stable_path import StableKey
2626
from .function import (
2727
AnyCallable,
28+
LogicTracking,
2829
create_core_component_processor,
2930
fn,
3031
)
@@ -417,6 +418,7 @@ def runtime() -> _DualModeRuntime:
417418
"AppConfig",
418419
# .function
419420
"fn",
421+
"LogicTracking",
420422
# .context_keys
421423
"ContextKey",
422424
"ContextProvider",

python/cocoindex/_internal/component_ctx.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ def app_main() -> None:
221221
value = ctx._env.context_provider.use(key)
222222
if key.tracked:
223223
fp = ctx._env.context_provider.get_tracked_fingerprint(key)
224-
ctx._core_fn_call_ctx.add_logic_dep(fp)
224+
ctx._core_fn_call_ctx.add_context_tracked_dep(fp)
225225
return value
226226

227227

python/cocoindex/_internal/core.pyi

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,13 @@ class ComponentProcessorContext:
9090

9191
# --- FnCallContext ---
9292
class FnCallContext:
93-
def __new__(cls) -> FnCallContext: ...
93+
def __new__(
94+
cls, *, propagate_children_fn_logic: bool = True
95+
) -> FnCallContext: ...
9496
def join_child(self, child_fn_ctx: FnCallContext) -> None: ...
9597
def join_child_memo(self, memo_fp: Fingerprint) -> None: ...
96-
def add_logic_dep(self, fp: Fingerprint) -> None: ...
98+
def add_fn_logic_dep(self, fp: Fingerprint) -> None: ...
99+
def add_context_tracked_dep(self, fp: Fingerprint) -> None: ...
97100

98101
# --- FnCallMemoGuard ---
99102
class FnCallMemoGuard:

0 commit comments

Comments
 (0)