symfonic.core.prompt.blocks.sources.file¶
file ¶
FileBlockSource -- a prompt block read from a file on disk.
An IDENTITY.md committed next to the application is the strongest
form a block's content can take: it is version-controlled, it is
reviewable in a pull request, and it is still there when the datastore
is down. This adapter serves exactly that -- one instance backs one
file -- and nothing else. It touches no database, no network and no
cache, on any path.
The revision is the content hash, never the mtime¶
:attr:BlockRevision.revision is sha256:<hex> over the block's
content. The obvious alternative -- the file's modification time -- is
wrong in both directions, and both directions are damaging:
- mtime changes when content does not.
git checkout,rsync -aonto a fresh host and a container rebuild all restamp mtime on byte-identical files. A revision derived from it would change on every redeploy, invalidating the prompt cache for content that did not move and re-billing the full prefix on the next turn. - mtime can fail to change when content does. Coarse filesystem timestamp granularity, a restored backup, or a write that preserves times leave the stamp untouched. A revision derived from it would go on advertising a block that has already changed underneath it, and a cache keyed on that revision would serve the stale text.
A content hash has neither failure mode: it is a pure function of the bytes that will actually reach the prompt. Two hosts that checked the same commit out in different weeks compute the same revision.
created_at is therefore left None rather than set to the mtime.
The same untrustworthiness that disqualifies mtime as a revision
disqualifies it as a recorded fact, and
:class:~symfonic.core.prompt.blocks.types.BlockRevision exists to keep
recorded facts distinguishable from framework filler.
What this source deliberately cannot do¶
- No history. A plain file has one state -- the one on disk. The
adapter therefore does not present
list_revisions/load_revision, soisinstance(src, HistoryCapableBlockSource)isFalseand :func:~symfonic.core.prompt.blocks.validation.check_operator_editablerejectsoperator_editable=Trueagainst it at construction. Git may well hold that file's history; this adapter does not read git, and claiming a capability it has not implemented is exactly what the structural Protocols exist to prevent. - Not scope-aware. One file is one value for the whole deployment,
so
scope_awareisFalseandscopeis accepted and ignored. Pairing it with a non-deploymentblock scope is a construction-time error in :func:~symfonic.core.prompt.blocks.validation.check_scope_pairing; it is not silently served as per-tenant content. - Single block.
block_idis accepted (the Protocol has no overload that omits it) and ignored, because the path -- not the id -- selects the content. It is still carried into error messages so an unreadable file names the block it was serving.
Failures are typed, not raw OSError¶
A missing or unreadable file raises
:class:BlockFileNotFoundError / :class:BlockFileUnavailableError,
both of which derive from
:class:~symfonic.core.protocols.StorageError. The resolver catches the
source-failure family and applies the block's
on_source_failure policy -- fail_closed for the authored tiers,
omit for the learned ones. A bare FileNotFoundError escaping to
the caller would bypass that policy entirely and turn a missing optional
block into a failed turn.
MAX_BLOCK_FILE_BYTES
module-attribute
¶
The largest file this adapter will read into a prompt -- 1 MiB.
A prompt block is operator-authored text, not a general-purpose file
store: an authored block that size is already implausible, and reading
past it converts a config mistake (a deploy that swapped a small
IDENTITY.md for a generated or log file) into unbounded memory growth on
every single resolve, with the failure surfacing downstream as memory
pressure or a provider context error rather than through the block's
on_source_failure policy. Checked against the file's stat size before
any read, so an oversized file never reaches read_text.
REVISION_ALGORITHM
module-attribute
¶
The digest naming the revision. Recorded in the revision id itself.
BlockFileNotFoundError ¶
Bases: BlockFileUnavailableError, NotFoundError
The backing file does not exist.
Derives from both :class:BlockFileUnavailableError (so a resolver
catching the source-failure family catches it) and
:class:~symfonic.core.protocols.NotFoundError (so "absent" stays
distinguishable from "present but unreadable" -- an operator who
mistyped a path and one whose deploy dropped read permissions need
different fixes).
BlockFileUnavailableError ¶
Bases: StorageError
The backing file exists but could not be read as block content.
A permission denial, a path that is a directory, or content that is
not decodable in the declared encoding. Typed as a
:class:~symfonic.core.protocols.StorageError so the resolver routes
it through the block's on_source_failure policy instead of
letting a raw :class:OSError escape past it.
FileBlockSource ¶
Serves one prompt block from one file on disk.
Satisfies :class:~symfonic.core.prompt.blocks.protocol.BlockSource
structurally and stops there -- see the module docstring for why
history is not presented.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
The file backing this block. Resolved once at construction
so a later |
required |
encoding
|
str
|
Text encoding of the file. Content is always hashed as UTF-8, so this affects decoding only, never the revision. |
'utf-8'
|
Attributes:
| Name | Type | Description |
|---|---|---|
offline_safe |
bool
|
Always |
scope_aware |
bool
|
Always |
Source code in src/symfonic/core/prompt/blocks/sources/file.py
current_revision
async
¶
Return the current revision id without building a revision.
Not part of :class:BlockSource; offered so a cache-validity
check has a cheap path that stays as offline as
:meth:load -- the check reads the same local file and hashes
it, and reaches no datastore either.
Source code in src/symfonic/core/prompt/blocks/sources/file.py
load
async
¶
Return the current revision of the backing file.
scope and block_id are accepted because the Protocol has
no overload that omits them, and ignored because one instance
backs one file: the path selects the content. block_id is
still used in failure messages so an unreadable file names the
block it was serving.
Raises:
| Type | Description |
|---|---|
BlockFileNotFoundError
|
The file does not exist. |
BlockFileUnavailableError
|
The file exists but cannot be read
as text in :attr: |
Source code in src/symfonic/core/prompt/blocks/sources/file.py
content_revision ¶
Return the revision id for content -- sha256:<hex>.
A pure function of the text, with no filesystem access at all, so the same content produces the same revision on every host and at every mtime. Exposed rather than inlined so a cache-invalidation check can compute the expected revision for content it already holds, without a second read.
The text is hashed as UTF-8 regardless of the encoding it was read in: the revision identifies the block's content, so re-saving the same words in a different on-disk encoding must not present itself as an edit.