Skip to content

Content chunking

A chunk is one unit of semantic recall. When you query, Jennah ranks your agent's chunks by cosine distance and returns the nearest matches. How content is split into chunks determines recall precision.

Retrieval precision depends on two factors: 1. Embedding token limits: Content exceeding the model input limit is truncated during vector generation. 2. Chunk specificity: Well-focused chunks improve semantic similarity scoring when ranking top matches.

The embedding input limit

When you commit a chunk with rawContent and no embedding, Jennah generates the vector for you with Vertex AI gemini-embedding-001, which accepts about 2048 tokens of input.

If your content is longer, the model embeds a prefix and stops. Your text is not lost; rawContent is stored in full and returned in full on a match, but the vector only represents the beginning. Content beyond the token limit is not represented in the vector index and cannot match semantic search queries.

Truncation reporting

Jennah reports truncation on commit receipts. Inspect commit receipts to verify that long inputs fit within embedding token limits.

Reading the truncation report

Every memory:commit receipt names the chunks whose embedding was truncated:

{
  "commitTimestamp": "2026-07-30T09:12:44.113Z",
  "vectorRows": "1",
  "truncatedChunkIds": ["daily-2026-07-30-slack"]
}

An empty or absent truncatedChunkIds list means every chunk in that commit was embedded in full. Truncation is reported without failing the commit.

To fail the commit rather than storing a truncated embedding, set rejectOnTruncation on the request:

{
  "vectors": [{ "chunkId": "doc-42", "rawContent": "...long..." }],
  "rejectOnTruncation": true
}

The entire commit is then rejected with FAILED_PRECONDITION and nothing is written.

To audit chunks that are already stored, use memory:inspect (see Reading a workspace back for paging a listing to the end):

{
  "vectors": {
    "chunks": [{
      "chunkId": "daily-2026-07-30-slack",
      "rawContent": "...",
      "truncated": true,
      "tokenCount": "9001"
    }]
  }
}

Unmeasured chunk truncation

Chunks written before truncation tracking was enabled omit the truncated field. An absent field indicates unmeasured truncation state rather than false. Re-committing the chunkId evaluates and reports truncation status.

Sizing a split

tokenCount is the token count of submitted content:

pieces needed ≈ tokenCount / 2048

A chunk reporting tokenCount: 9001 needs roughly five pieces.

Don't estimate from character counts

Characters are a bad proxy for tokens, and how bad depends on the language. English runs about 4 characters per token, so ~8000 characters is near the limit. Japanese, Chinese, and Korean run closer to 1 token per character, so a 2500-character Japanese chunk can already be over it. Read tokenCount - don't guess from len(text).

Chunk granularity and retrieval strategy

Staying under token limits is required, but smaller chunks also improve retrieval precision:

  • Split on meaning, not size. One chunk per section, topic, or exchange. A fixed-width window may split semantic context across chunks.
  • Distill before storing. Summarize lengthy conversations or logs before committing to prevent low-value chunks from occupying query result slots.
  • Keep pointers to large sources. Store concise summaries with external IDs or URIs to fetch raw content as needed.
  • Retire chunks that stop being true. A stale chunk competes for the same result slots as the one that replaced it. See Temporal memory.

Structuring metadata vs. raw content

Including inline provenance tags (such as [#eng-billing 2026-07-30]) in rawContent dilutes embedding similarity and prevents exact metadata filtering.

Use metadata instead. It is stored with the chunk and returned with every hit, but it is not embedded - only rawContent is:

{
  "vectors": [{
    "chunkId": "daily-2026-07-30-slack",
    "rawContent": "Alice fixed duplicate invoice lines in billing.",
    "metadata": {
      "employee": "alice",
      "day": "2026-07-30",
      "source": "slack"
    }
  }]
}

Then scope recall to a subset with filters:

{
  "semantic": {
    "queryText": "what did alice ship recently?",
    "limit": 8,
    "filters": [
      { "key": "employee", "value": "alice" },
      { "key": "source", "value": "slack" }
    ]
  }
}

Filters combine with AND and are applied before ranking, so you get the nearest limit chunks from among those that match - not a filter applied to an already-ranked result set.

For complete metadata specifications and filter rules, see Metadata.

Note that metadata tags on chunks are not embedded into vector space; only rawContent is embedded.

A note on field names

Responses use lowerCamelCase (truncatedChunkIds, tokenCount, rawContent), which is what the examples above show. Requests accept either casing. The API Reference lists fields in snake_case, so truncated_chunk_ids there is the same field as truncatedChunkIds on the wire.

Note: 64-bit integers are represented as JSON strings (such as "tokenCount": "9001" and "vectorRows": "1").