Skip to content

feishu pack: document text is unreachable — the content actions are single-object, row_path requires an array #197

Description

@Userneima

The feishu pack can enumerate documents but cannot read them. The gateway has the actions; the pack layer cannot express their shape. Evidence below is pinned to SkardiLabs/skardi@2291b19 and oomol-lab/open-connector@bf94ce8.

The gap

The feishu pack (merged 2026-08-06, 2291b19) exposes six tables: chats,
messages, chat_members, tasks, wiki_spaces, wiki_nodes. That covers
chat history well. It does not cover document text at all: wiki_nodes yields
node_token / obj_token / obj_type / title / url — the keys you need to
open a document, but no column carrying what the document says.

So today an agent can answer "what was discussed in this chat" but not
"what does this document say", even though the second is the request users
keep making.

This is not an upstream gap. The gateway has the actions. Verified against
oomol-lab/open-connector at bf94ce8, and the feishu provider registers every
action group (src/providers/feishu/actions.ts:201-240), so all of these exist
on a current gateway:

Action Response shape Mappable as a table today?
feishu.get_document_content single object {documentId, content} — full plain text (actions.ts:94) No
feishu.fetch_document single object {document: {…}}, with scope / detail / keyword / block-range reads (shared/docs-actions.ts:91, schema at :28) No
feishu.list_document_blocks array, same envelope as the six mapped tables (actions.ts:112) Yes, but see below
feishu.search_documents array {results, hasMore, pageToken} (shared/docs-actions.ts:192) Yes — metadata only, no text

Why the two content actions cannot be mapped

A pack table locates its rows with row_path, and that path must resolve to an
array:

  • RowPath::parse rejects $ outright — "no segments"
    (crates/skardi/src/sources/providers/open_connector/row_path.rs:31).
  • RowPath::rows fails with RowPathNotArray on a non-array target
    (row_path.rs:98).

Both content actions return one object at the root. There is no nested array to
point at, so no row_path can express them.

Why list_document_blocks does not close the gap either

It is array-shaped and needs no engine change — but a Feishu docx block carries
its payload under a key named by the block's own type (text, heading1
heading9, bullet, ordered, code, quote, …). A fixed relational mapping
cannot address a key whose name varies per row.

This is exactly the wall the Notion pack already hit, and the note there records
the decision taken at the time:

The type-specific payload (paragraph/heading_1/...) lives under a key named BY
type, which a fixed relational mapping cannot address; content extraction
belongs to a rendered-markdown table (notion.retrieve_page_markdown) as a
follow-up.
packs/notion.yaml:110

Mapping list_document_blocks is still possible and still an improvement —
structure, hierarchy, per-block edit times, and the raw block object as a json
column that SQL can dig into. But "dig the text out with JSON functions" is not
the same product as a content column, and the Notion note suggests the project
already judged this route insufficient for content.

What would close it

An optional, per-table declaration that the response is one row — everything
else unchanged. Sketch:

  document_content:
    action: feishu.get_document_content
    row_shape: object          # new; default `array` = today's behaviour
    row_path: "$"              # only legal when row_shape is object
    pagination: { strategy: single_page }
    resources:
      required: [documentId]
    columns:
      - { name: document_id, path: documentId, type: utf8, nullable: false }
      - { name: content,     path: content,    type: utf8, nullable: true }

Surface of the change, measured:

  • TableDoc gains one #[serde(default)] field
    (packs/loader.rs, struct TableDoc).
  • RowPath needs to accept a root path when the shape is object
    (row_path.rs:31 is the only rule in the way).
  • One production call site consumes rows —
    exec.rs:509 (let rows = self.row_path.rows(&envelope, page)?;). Every other
    .rows( occurrence in the tree is inside pack test modules.
  • PaginationStrategy::SinglePage already exists (pagination.rs:83), so the
    no-pagination half needs nothing new.
  • Default is today's behaviour, so the four existing packs are untouched — no
    yaml change, no behaviour change.

Why this may be worth more than one table

The Notion follow-up quoted above hits the same wall: a rendered-content action
returns one document, not a page of rows. If the row model learns single-object
rows once, both providers can map content, and any future provider whose
"read one thing" action matters gets it for free.

What I am NOT proposing

  • Not proposing to write this myself and hand you a fait accompli. The Notion
    note tells me this wall was met and deliberately deferred once already, which
    suggests you may have a different mechanism in mind (a rendered-content
    concept? a deliberate choice to keep the model scan-only?). That decision is
    yours; I would rather ask than push engine code through review.
  • Not proposing anything about writes. All four actions above are reads.

Open questions for you

  1. Is a single-row table acceptable in a scan-oriented model, or is that the
    reason content extraction was deferred on Notion?
  2. If yes — is row_shape: object the shape you want, or would you rather the
    gateway grow a list-shaped content action so the pack layer stays as is?
  3. If no — is mapping list_document_blocks with a raw-block json column
    worth doing as the interim, knowing the text still needs JSON digging?

What I can and cannot verify

I can implement and unit-test the row-model change locally: it needs no gateway.

I cannot produce a submission-ready table. Per the source-pack skill's gate, a
new table needs a fingerprint captured from a live gateway and a phase-4 pass
where real rows settle column truth. I have no gateway running and no clean
Feishu app to authorize (the app I can authorize already carries 133 scopes from
other tools, so it cannot establish a minimum-scope set either). Whoever has the
live setup would need to finish those two steps.


@bakey the three open questions above are for you — particularly (1), since the Notion note suggests this wall was met and deferred deliberately rather than overlooked. Happy to implement the row-model half if you want it; happy to drop it if you have a different mechanism in mind.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions