import { EntityServiceRegistry } from "../../../common/registries/entity.service.registry"; import { BlockNoteService } from "../../../core/blocknote/services/blocknote.service"; import { GraphCatalogService } from "../../graph/services/graph.catalog.service"; import { ScopeGuard } from "../../graph/services/scope.guard"; import { ToolCallRecord, ToolFactory, UserContext } from "../../graph/tools/tool.factory"; import { OperatorRetrievalContext, OperatorToolDefinition } from "../interfaces/operator.tool.interface"; /** Payload of `create_entity`. */ export interface CreateEntityInput { type: string; fields: Record; relationships?: Record; } /** Payload of `update_entity`. */ export interface UpdateEntityInput { type: string; id: string; fields: Record; } /** Payload of `delete_entity`. */ export interface DeleteEntityInput { type: string; id: string; } /** Payload of `link_entities` and `unlink_entities`. */ export interface LinkEntitiesInput { type: string; id: string; relationship: string; targetIds: string[]; } /** * Generic, catalog-driven create/update/delete/link/unlink tools for the operator. * * Three invariants make these safe enough to expose to an LLM: * * 1. **Opt-in.** Only descriptors that declare `chat.writable` are touchable, and * only when the caller's modules grant access. When nothing is writable the * tools are not built at all, so a host application that never opts in sees * exactly the tool set it had before. `chat.writable` may narrow further to an * allow-list of fields and relationships, which `describe_entity` reports and * every check here enforces. * 2. **Scoped.** Every id in every payload is checked through `ScopeGuard`, and * `create` overwrites the scope relationship with the run's OWN scope id — the * model cannot name a different one. The scope relationship itself can never be * re-pointed, so a record cannot be moved between scope roots. * 3. **Service-only, through the same DTO path the controllers use.** Writes go * through `EntityServiceRegistry.get(type)`, i.e. the AbstractService, never the * repository — and always via its `*FromDTO` helpers with a `JsonApiDTOData`, * exactly as an HTTP controller does. That is what makes a host application's * OVERRIDE of `createFromDTO` / `patchFromDTO` fire (ownership checks, * knowledge-graph and summariser scheduling, cache invalidation) and what lets * `mapDTOToParams` fill `contextKey` relationships from CLS. Calling the plain * `create` / `patch` would skip every one of those: the record is written, the * app's read query for its type then finds nothing (a required owner edge is * missing) and the tool reports `not found` for a record that exists. No Cypher * is written here. */ export declare class EntityWriteTools { private readonly catalog; private readonly registry; private readonly scopeGuard; /** * Optional in the TYPE signature only, so unit tests can construct the three * collaborators they exercise. Nest has no notion of `?` and still resolves it * from GraphModule — a missing provider fails loudly at boot rather than * silently dropping the per-turn tool-call audit trail. */ private readonly factory?; /** * Optional in the TYPE signature for the same reason as `factory`, and * resolved from GraphModule (which re-exports BlockNoteModule) exactly the * same way. Without it a rich-text field would pass through as the model's * raw markdown; see `convertRichtextFields`. */ private readonly blockNote?; private readonly logger; constructor(catalog: GraphCatalogService, registry: EntityServiceRegistry, scopeGuard: ScopeGuard, /** * Optional in the TYPE signature only, so unit tests can construct the three * collaborators they exercise. Nest has no notion of `?` and still resolves it * from GraphModule — a missing provider fails loudly at boot rather than * silently dropping the per-turn tool-call audit trail. */ factory?: ToolFactory, /** * Optional in the TYPE signature for the same reason as `factory`, and * resolved from GraphModule (which re-exports BlockNoteModule) exactly the * same way. Without it a rich-text field would pass through as the model's * raw markdown; see `convertRichtextFields`. */ blockNote?: BlockNoteService); /** * Builds the five write tools for one operator turn. * * Returns `[]` when the caller can write nothing. This is what keeps hosts that * declare no `chat.writable` descriptor on exactly the behaviour they had before * these tools existed. */ buildDefinitions(ctx: OperatorRetrievalContext, recorder: ToolCallRecord[]): OperatorToolDefinition[]; createEntity(input: CreateEntityInput, ctx: UserContext, recorder: ToolCallRecord[]): Promise; updateEntity(input: UpdateEntityInput, ctx: UserContext, recorder: ToolCallRecord[]): Promise; deleteEntity(input: DeleteEntityInput, ctx: UserContext, recorder: ToolCallRecord[]): Promise; linkEntities(input: LinkEntitiesInput, ctx: UserContext, recorder: ToolCallRecord[]): Promise; unlinkEntities(input: LinkEntitiesInput, ctx: UserContext, recorder: ToolCallRecord[]): Promise; private prepareCreate; private prepareUpdate; private prepareDelete; private prepareLink; /** The `validate` hook's answer: the rejection message, or `null` when the call may proceed. */ private rejection; private resolveWritable; /** * Reject any field the catalog does not declare, any field the descriptor does * not open to the assistant, and any type mismatch. The writable list is what * keeps the model out of the fields the system generates itself (a tldr, a * summary, an ai status): they are described — the model can read them — but * writing them would overwrite generated content with a guess. */ private validateFields; /** * Reject unknown or reverse-only relationship keys, and any target id that is * outside the run's scope. The scope relationship never reaches this method — * callers strip it first. */ private validateRelationships; /** * The message for a relationship the generic write path must never touch. The * reasons themselves live in `writable.rules` — the same predicate * `describe_entity` uses to tell the model what it may write, so what the model * is told and what is enforced here cannot drift. */ private rejectUnwritableRelationship; /** `null` when every id is inside the run's scope, otherwise the error to return. */ private requireInScope; /** * The JSON:API `relationships` object a create sends, built from three sources: * * 1. what the model supplied, keyed by the descriptor's `dtoKey` (NOT the catalog * name — `mapDTOToParams` looks relationships up by `dtoKey`, so a payload * keyed by name is dropped without a word whenever the two differ); * 2. the run's OWN scope root, which overwrites anything the model named — the * model's value was already stripped while preparing; * 3. the owner, from the run's user. The host application's clients send this on * every create, and its read query for the type requires the edge: without it * the record is written and then cannot be read back. */ private createRelationships; /** A payload map from raw tool args: anything that is not a plain object is empty. */ private toValueMap; /** An id from raw tool args; a missing one becomes `""`, which no scope check passes. */ private toId; private writableTypes; private matchesType; private applyLink; /** * Runs a write and converts a thrown framework error into the `{ error }` shape * every tool method returns, matching the read tools' contract: the operator * turns a returned error into a ToolMessage the model can recover from, whereas * a thrown one aborts the tool node. */ private dispatch; private capture; private resolveRef; /** Resolve the record the tool call targets, from `args.type` / `args.id`. */ private resolveTargetRef; /** Relationship payloads are `Record` by schema; arrays are accepted defensively. */ private toIdList; /** `"Marcus"` when resolved, bare `(not found)` when not — an id is never rendered. */ private quote; private summariseCreate; private summariseUpdate; private summariseDelete; private summariseLink; private argType; private summariseLabel; private presentCreate; private presentUpdate; private presentDelete; private presentLink; private resolveLinkTargets; private presentAttributes; } //# sourceMappingURL=entity-write.tools.d.ts.map