mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-29 19:35:20 +02:00
- Added new environment variables for controlling task execution limits, including `SURFSENSE_SUBAGENT_INVOKE_TIMEOUT_SECONDS`, `SURFSENSE_TASK_BATCH_CONCURRENCY`, and `SURFSENSE_TASK_BATCH_MAX_SIZE`. - Updated documentation to reflect new batch processing capabilities for `task` calls, allowing for concurrent execution of multiple subagent tasks. - Improved error handling and receipt generation for deliverables, ensuring consistent feedback on task status. - Refactored middleware to incorporate search space ID for better task management.
161 lines
5.8 KiB
Python
161 lines
5.8 KiB
Python
"""Receipt: structured handle returned by every mutating subagent tool.
|
|
|
|
Generalises the Hermes ``entry`` dict (see ``references/hermes-agent/tools/
|
|
delegate_tool.py:1663-1697``) for our 5 deliverable types + 15 connectors +
|
|
KB writes. The supervisor reads the Receipt to verify what actually happened
|
|
without round-tripping through LLM paraphrase.
|
|
|
|
**Why this lives under ``app.agents.shared`` and not under either of the
|
|
two agent packages:** the Receipt is a *contract* shared between
|
|
``multi_agent_chat`` (where mutating tools emit it) and ``new_chat``
|
|
(where ``filesystem_state.SurfSenseFilesystemState`` declares the
|
|
``receipts`` reducer that accumulates it, and where
|
|
``middleware.kb_persistence`` emits its own KB-write receipts). Putting
|
|
the contract in either package would create a bidirectional import
|
|
between the two — see the commit that introduced this module for the
|
|
``ImportError`` chain it broke.
|
|
|
|
Each mutating tool wraps its native return shape into a Receipt via
|
|
:func:`make_receipt` (or builds one directly) and returns it under the
|
|
``"receipt"`` key alongside its existing payload. The subagent boundary
|
|
machinery in ``checkpointed_subagent_middleware.task_tool`` then folds
|
|
the receipt into the parent's ``receipts`` state via the append reducer.
|
|
|
|
The KB write path is the one exception: file-tool calls cannot emit a
|
|
durable receipt because the actual DB writes happen end-of-turn inside
|
|
:class:`app.agents.new_chat.middleware.kb_persistence.KnowledgeBasePersistenceMiddleware`.
|
|
KB tools therefore emit a *provisional* receipt with ``status="pending"``;
|
|
the persistence middleware flips it to ``"success"`` or ``"failed"``
|
|
before returning control to the parent.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Literal, TypedDict
|
|
|
|
# Subagent that emitted this receipt.
|
|
ReceiptRoute = Literal[
|
|
"deliverables",
|
|
"knowledge_base",
|
|
"notion",
|
|
"slack",
|
|
"gmail",
|
|
"linear",
|
|
"jira",
|
|
"clickup",
|
|
"confluence",
|
|
"calendar",
|
|
"luma",
|
|
"airtable",
|
|
"google_drive",
|
|
"dropbox",
|
|
"onedrive",
|
|
"discord",
|
|
"teams",
|
|
]
|
|
|
|
# Within-route kind of artefact / external resource the operation touched.
|
|
# Left as ``str`` rather than a giant union so each route file documents
|
|
# its own enum next to its tools.
|
|
ReceiptType = str
|
|
|
|
# Operation verb. Kept open for the same reason as ``ReceiptType``.
|
|
ReceiptOperation = str
|
|
|
|
# Pending = async backend (Celery podcast / video) that the orchestrator
|
|
# will surface progress for out of band; persistence-MW flipped this to
|
|
# ``success`` for KB writes that committed.
|
|
ReceiptStatus = Literal["success", "pending", "failed"]
|
|
|
|
|
|
class Receipt(TypedDict, total=False):
|
|
"""Structured per-mutation handle returned to the parent subagent.
|
|
|
|
All fields are ``NotRequired`` (TypedDict ``total=False``) so each
|
|
route's tool can populate only the fields it actually has — e.g. Gmail
|
|
never sets ``verifiable_url`` because Gmail doesn't expose per-message
|
|
URLs. The receipts state reducer treats missing keys as missing rather
|
|
than ``null`` so we don't double-count.
|
|
"""
|
|
|
|
route: ReceiptRoute
|
|
"""Subagent name. Lets the orchestrator filter ``state['receipts']``
|
|
by route without re-deriving from ``type``."""
|
|
|
|
type: ReceiptType
|
|
"""Within-route kind. e.g. for ``deliverables`` one of ``{report,
|
|
podcast, video_presentation, resume, image}``; for ``notion`` ``page``;
|
|
for ``slack`` ``message``."""
|
|
|
|
operation: ReceiptOperation
|
|
"""Verb. e.g. ``generate`` (deliverables), ``create`` / ``update`` /
|
|
``delete`` (most connectors), ``send`` / ``post`` (chat), ``write_file``
|
|
/ ``edit_file`` / ``rm`` / ``rmdir`` / ``move_file`` / ``mkdir`` (KB)."""
|
|
|
|
status: ReceiptStatus
|
|
"""``success`` / ``pending`` / ``failed``. The verification teaching
|
|
in ``shared/snippets/verifiable_handle.md`` keys off this field."""
|
|
|
|
external_id: str | None
|
|
"""Backend identifier. Report row id, Notion ``page_id``, Slack ``ts``,
|
|
Gmail ``message_id``, Linear identifier, KB ``virtualPath``, etc.
|
|
``None`` only when the operation failed before the backend assigned one."""
|
|
|
|
verifiable_url: str | None
|
|
"""URL the parent can pass to ``scrape_webpage`` to verify the
|
|
operation. ``None`` when no public URL exists (Gmail, KB, raw images
|
|
stored in the DB)."""
|
|
|
|
preview: str | None
|
|
"""Short snippet (~200 chars) of what was produced. First lines of
|
|
a generated report's markdown, transcript opener for a podcast,
|
|
thumbnail URL for an image. Lets the orchestrator decide whether to
|
|
re-render in the UI without re-loading the artefact."""
|
|
|
|
error: str | None
|
|
"""Filled iff ``status == "failed"``. Plain-text reason; the parent
|
|
surfaces it in its own ``next_step``."""
|
|
|
|
|
|
def make_receipt(
|
|
*,
|
|
route: ReceiptRoute,
|
|
type: str,
|
|
operation: str,
|
|
status: ReceiptStatus,
|
|
external_id: str | None = None,
|
|
verifiable_url: str | None = None,
|
|
preview: str | None = None,
|
|
error: str | None = None,
|
|
) -> Receipt:
|
|
"""Construct a :class:`Receipt` with non-``None`` fields only.
|
|
|
|
Drops keys whose value is ``None`` so downstream consumers can use
|
|
``"verifiable_url" in receipt`` to distinguish "tool returned no URL"
|
|
from "tool deliberately surfaced ``null``".
|
|
"""
|
|
out: dict[str, Any] = {
|
|
"route": route,
|
|
"type": type,
|
|
"operation": operation,
|
|
"status": status,
|
|
}
|
|
if external_id is not None:
|
|
out["external_id"] = external_id
|
|
if verifiable_url is not None:
|
|
out["verifiable_url"] = verifiable_url
|
|
if preview is not None:
|
|
out["preview"] = preview
|
|
if error is not None:
|
|
out["error"] = error
|
|
return out # type: ignore[return-value]
|
|
|
|
|
|
__all__ = [
|
|
"Receipt",
|
|
"ReceiptOperation",
|
|
"ReceiptRoute",
|
|
"ReceiptStatus",
|
|
"ReceiptType",
|
|
"make_receipt",
|
|
]
|