Skip to content

Targeting moves to the first user message: document_context() replaces doc_id on the agent surfaces; folder_context() and chat(folder_id=) add folders - #495

Merged
rejojer merged 7 commits into
mainfrom
feat/document-context
Sep 10, 2026
Merged

Targeting moves to the first user message: document_context() replaces doc_id on the agent surfaces; folder_context() and chat(folder_id=) add folders#495
rejojer merged 7 commits into
mainfrom
feat/document-context

Conversation

@rejojer

@rejojer rejojer commented Sep 9, 2026

Copy link
Copy Markdown
Member

What

The doc-targeting block ("The user has specified document: …") is conversation content, not guidance. It now lives in exactly one place on every surface: the first user message. Nothing is appended to the system prompt any more.

  • chat(): all three lanes prepend the block as the first user message. The Messages lane moved off its separate system block; the OpenAI lanes already did this.
  • New client.document_context(doc_id): returns the block text for callers who own the conversation (the framework routes). Lead your first message with it:
    agent = Agent(**client.openai_agent_config())
    Runner.run_sync(agent, [
        {"role": "user", "content": client.document_context(doc_id)},
        {"role": "user", "content": question},
    ])
    (Anthropic tool_runner and Claude Agent SDK: same text, prepended to the first message / prompt.)
  • BREAKING: doc_id removed from eight methods — agent_instructions(), openai_agent_config(), anthropic_runner_config(), claude_agent_config(), agent_tools(), as_openai_tools(), as_anthropic_tools(), as_claude_mcp(). The prompt-side targeting moves to document_context(); the tool-layer allowlist was local-only and raised on cloud, so it is no longer a public knob (it stays internal to local chat(doc_id=)).
  • The block is O(1): one get_document per id. Names are unique per library (uploads suffix a taken name), so the same-name shadow check and its full-library listing sweep went (10 s → 1 s on a 749-doc cloud library). The cloud metadata endpoint carries metadata, so local get_document() gained that key for parity.
  • Rendering matches the cloud's managed chat: same wording, one document as a JSON object, several as a list, the metadata row as returned.

Folders (#499, rebased in)

The managed /chat/completions already takes folder_id and renders a folder block ahead of the document block (compute api.py:491, :3182); the SDK now sends it and mirrors it.

  • New client.folder_context(folder_id): the folder analogue of document_context(): the folder's name, an id/name/description metadata row (the server's FolderContext), and the directive to discover its documents with browse_documents / search_documents(folder_id=..., recursive=true). One list_folders() call: the endpoint has no LIMIT, lists the same userId + api_surface partition the managed chat validates against, and carries the same plan gate, so a non-Max key fails here rather than mid-run. Cloud-only: a local client raises.
  • chat(folder_id=) (also chat_completions() and the responses / messages lanes): the managed lane sends the field and lets the server render and gate it; the own-model lanes lead the conversation with one user message, folder block then document block, joined as the server joins them. folder_id joins the prompt-cache key like doc_id.
  • "root" (and "") is the library itself: folder_context returns "", chat places nothing, on every lane and in every mode, as the server treats folder_id="root".
  • folder_id is keyword-only on chat() and trailing on every other signature: no positional call shifts.

Why

System prompt = who you are and how you work. A per-request target there breaks the cached prefix, sticks across turns, and on cloud splices SDK text onto the server-served instructions. The cloud's own managed chat (compute api.py) and the web app both place document context in the user turn; this aligns the SDK with them.

Verification

  • python -m pytest tests: 480 passed. Without-frameworks CI leg simulated (agents / anthropic / claude_agent_sdk blocked): green.
  • Live, real models, local store + cloud library × {chat() answer lane, chat_completions(), protocol="responses", protocol="messages", OpenAI Agents, Anthropic tool_runner, Claude Agent SDK}: 14/14 answered from the targeted document; every tool call carried the exact doc_name, no browse detours.
  • Folders: 437/437 in test_client / test_agent_tools / test_local_chat with the folder tests; pyright: zero new diagnostics against the base; live: managed chat(q, folder_id=F) lists the folder's documents, folder_id="root" answers normally, own-model over cloud tools calls browse_documents(folder_id=F, recursive=true) first and gives the same list.

Follow-ups (not in this PR)

  • docs site sdk/agents.mdx: drop doc_id from the agent_instructions() / tool-helper examples and tables, document document_context(); land with the release. The folder docs (chat.mdx, documents.mdx) are in VectifyAI/pageindex-docs#53.
  • Release notes: BREAKING line for the eight methods; get_document() now returns metadata locally too. Plus folder_context() and chat(folder_id=).
  • Cloud: metadata mixes internal keys (thumbnailKey, …) into user tags; compute's managed chat dumps next_steps / user_id into its own block; compute's json.dumps escapes non-ASCII names while the web app and SDK keep them raw.

https://claude.ai/code/session_01VxguPoTv2BmzS9d3erzTrS
https://claude.ai/code/session_01NsQjN5HMmmX4n5NVsZWYaw

@rejojer rejojer changed the title Document targeting moves to the first user message: document_context() replaces doc_id on the agent surfaces Targeting moves to the first user message: document_context() replaces doc_id on the agent surfaces; folder_context() and chat(folder_id=) add folders Sep 10, 2026
@rejojer

rejojer commented Sep 10, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-10T12:07:09.811842Z 41ebe98 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d439b19292

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pageindex/agent_tools.py
Comment on lines 1644 to 1645
"Use this document's name to retrieve its content with "
"get_document_structure() and get_page_content()."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve folder identity when targeting a document

When a cloud library contains same-name documents in different folders, this directive tells the model to retrieve by name alone even though the cloud tool contract explicitly requires folder_id to disambiguate such names (agent_tools.py:47-55). The removed shadow check previously prevented silently targeting an ambiguous name; now document_context(doc_id) and own-model chat(doc_id=...) can read the wrong document or fail despite get_document() supplying its folderId. Include the target folder ID in the retrieval directive, or retain an ambiguity check.

Useful? React with 👍 / 👎.

…es doc_id on the agent surfaces

The doc-targeting block ("The user has specified document: ...") was
appended to the system prompt by agent_instructions() and the three
*_agent_config bundles, and placed as its own system block by the Messages
chat lane. A per-request target in the system prompt breaks the cached
prefix, sticks across turns, and on cloud splices SDK text onto the
server-served instructions. The cloud's managed chat puts the same block at
the head of the conversation, so every surface now does the same.

- chat(): all three lanes prepend the block as the first user message
  (the Messages lane moved off its system block).
- New client.document_context(doc_id): the block text for callers who own
  the conversation (the framework routes), to lead their first message.
- BREAKING: doc_id removed from agent_instructions(), openai_agent_config(),
  anthropic_runner_config(), claude_agent_config(), agent_tools(),
  as_openai_tools(), as_anthropic_tools() and as_claude_mcp(). The
  tool-layer allowlist was local-only and raised on cloud; it stays
  internal to local chat(doc_id=).
- The block is one get_document per id: names are unique per library
  (uploads suffix a taken name), so the shadow check and its listing sweep
  went. The cloud endpoint carries metadata, so local get_document() gained
  the key for parity. Rendering matches the cloud's: one document is an
  object, several a list.

Live-verified on a local store and the cloud library across chat() (answer
lane, chat_completions, responses, messages) and the OpenAI Agents,
Anthropic tool_runner and Claude Agent SDK routes.

Claude-Session: https://claude.ai/code/session_01VxguPoTv2BmzS9d3erzTrS
doc_id sat first in the positional list on agent_instructions,
openai_agent_config and claude_agent_config, and second on
anthropic_runner_config and as_claude_mcp. With it removed, a v0.2.15 call
such as openai_agent_config("pi-a") no longer failed: the id landed on
include_management (truthy, so the management tools came along and
targeting silently vanished) or on server_name. A bare * after the
surviving leading positional turns those calls into an immediate
TypeError; every in-repo caller already passes keywords.

Claude-Session: https://claude.ai/code/session_017FumozBm2xbT2SG6WBxjMe
5451e4e put a `*` on the client methods that dropped doc_id, so a stale
positional call raises instead of landing on the next parameter.
build_claude_mcp, which as_claude_mcp delegates to, dropped doc_ids the
same way and let server_name move into its slot: a stale
build_claude_mcp(client, False, ['pi-a']) built an in-process server
named ['pi-a'], or on cloud returned the http config with the argument
dropped. Now keyword-only, covered by the same test.

Doc truths: document_context no longer claims a later turn can move on
to another document (chat's docstrings say to keep doc_id constant, and
the block is prepended at the head of the conversation, so a retarget
lands behind the history); submit_document's metadata inventory and
CloudAPI.get_document's key list both name the metadata and folderId
keys get_document returns.

Docstring trims: the "names are unique per library" rationale sentence
and the multi-line test docstrings, one of which still described the
listing backfill this PR removed.

Claude-Session: https://claude.ai/code/session_018FoZJ97cuM9hAjrk3goZ6u
The managed /chat/completions already takes folder_id and renders a
folder targeting block ahead of the document block (pageindex-compute
api.py:491, :3182); the SDK had no way to send it, and own-model chat
had no folder analogue of document_context(). Both land here.

folder_context(folder_id) renders that block as the managed chat
renders its own: the name, an id/name/description metadata row (the
server's FolderContext), and the directive to discover the folder's
documents with browse_documents / search_documents(folder_id=...,
recursive=true). One list_folders() call picks the folder by id: the
endpoint has no LIMIT and lists the same userId + api_surface
partition the managed chat validates against, so what the helper finds
is what the server accepts, and it carries the same plan gate, so a
non-Max key fails here rather than mid-run. A missing folder raises.
Cloud-only: local libraries have no folders.

chat(folder_id=) places it. The managed lane sends the field and lets
the server render and gate it; the three own-model lanes lead the
conversation with one user message, folder block then document block,
joined as the server joins them. folder_id joins the prompt-cache key
for the reason doc_id does: the block is byte-identical for every
conversation about a folder.

"root" (and "") targets nothing on every lane and in every mode, as
the server treats folder_id="root": folder_context returns "" and chat
places nothing. folder_id is keyword-only on chat() and trailing on
every other signature, so no positional call shifts.

Tests: the wire field on the managed lane; block order and root/"" on
the shared renderer; folder_id threaded through the chat, responses
and messages engines; the local cloud-only refusal.

Claude-Session: https://claude.ai/code/session_01NsQjN5HMmmX4n5NVsZWYaw
_conversation_cache_key splices folder_id into the seed only when one
is set, so a conversation without a folder keeps the prompt_cache_key it
had before folders existed instead of cold-starting on upgrade; the
released key is pinned.

folder_id gets the bare * on chat_completions, _responses and _messages,
keyword-only on every chat surface as on chat(); every caller already
passes it by keyword.

The three *_agent_config docstrings name the context helpers in the
order the lanes place them: folder, then documents.

test_doc_targeting_is_one_lookup_per_document now counts get_document
calls; it accepted a second lookup per id before.

Claude-Session: https://claude.ai/code/session_01T2i8viYf3Cb6wZcjToLEDD
…xact payload

The lane's payload assertion is exact, and folder_id is a managed chat
request field, so it appears there too — unset on a folder-less call.
@rejojer
rejojer force-pushed the feat/document-context branch from d9c8986 to c5a3c61 Compare September 10, 2026 11:52
chat(protocol="chat_completions") reached main while this branch was
open, and it is a managed chat_completions call site of its own: a
folder_id passed there was dropped on the way to the endpoint, scoping
nothing with no error. It carries it now, asserted on the wire beside
the answer lane's.
@rejojer
rejojer merged commit a3364c8 into main Sep 10, 2026
9 checks passed
@rejojer
rejojer deleted the feat/document-context branch September 10, 2026 12:02

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 41ebe98276

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pageindex/cloud_api.py
Comment on lines +231 to +232
if folder_id:
payload["folder_id"] = folder_id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reserve folder_id from extra_body overrides

When callers pass both the new folder_id= argument and a legacy or composed extra_body containing folder_id, the later payload.update(extra_body or {}) silently replaces this value, potentially sending the managed chat to a different folder. _refuse_skeleton() already reserves stream and doc_id for exactly this reason, so add folder_id to that argument-key validation rather than allowing the explicit scope to be overridden.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant