/**
* `read_value` hands the model the value behind a handle the run holds — the
* one tool whose answer is a fabric value itself rather than its shape or a
* reference to it.
*
* Two things bound it. A handle is read only once `describe_handle` has
* answered for it in this run, so a value is read after its shape was looked
* at rather than instead of looking; the prompt loop records that answer on
* the handle's entry. And what is read is measured the way a `call_verb` or
* `run_pattern` result is before it is released: through a transaction of its
* own, against the run's release ceiling, with the model as the sink. Where
* the run's posture refuses it, the refusal names the atoms that kept it back
* and the route that can still use the value — a subagent that reads it and
* returns what the task needs. Where it is released, the label of what was
* read travels on the output, and the prompt loop joins it into the run's
* model context, so everything the model writes afterwards carries it.
*
* A piece's derived fields live in documents its run writes, so a read that
* lands in one no run has written runs the piece first, as `call_verb` does,
* where the run is authorized for side effects.
* "Holds nothing at that path" is then a statement about the path rather than
* about a piece nobody ran.
*/
import type { JSONSchema } from "@commonfabric/api";
import {
type Cell,
getPatternIdentityRef,
type MemorySpace,
type NormalizedFullLink,
type Runtime,
} from "@commonfabric/runner";
import {
cfcLabelViewForCellFailClosedWithStatus,
CfcReadCeilingError,
collectConsumedLabel,
describeSinkReleaseRefusal,
type IFCLabel,
} from "@commonfabric/runner/cfc";
import { entityKindOfIdString } from "@commonfabric/runner/entity-kind";
import {
createLLMFriendlyLink,
parseLLMFriendlyLink,
} from "@commonfabric/runner/shared";
import { disclosedCfcLabels } from "../cfc-label-disclosure.ts";
import type { HarnessToolDescriptor } from "../contracts/tool-descriptor.ts";
import { errorMessage } from "../error-message.ts";
import { scrubBareFabricIdentifiers } from "../fabric-identifier-scrub.ts";
import {
admitsFabricReference,
type HarnessForeignSpaces,
} from "../foreign-spaces.ts";
import { resolveHandleRef, resolveHandleToken } from "../handle-table.ts";
import { replacedAt, storedLinkPaths } from "./call-verb.ts";
import { sessionPieces } from "./piece-source.ts";
import { asSerializableValue } from "./run-pattern.ts";
import type { HarnessToolDefinition } from "./types.ts";
export interface ReadValueToolInput {
token: string;
/** A path within the value, its segments separated by `/`. */
select?: string;
}
export interface ReadValueToolSuccessOutput {
outputId: string;
status: "ok";
/** The handle that was read, echoed so a reply stands on its own. */
token: string;
/**
* The value, as stored. A position holding a link to another document comes
* back as `{"@link":
}`, which reaches the model as that
* document's handle token.
*/
value: unknown;
/**
* The label of what was read. The prompt loop moves it into the run's
* model context and keeps it off the model-facing output.
*/
cfc: { version: 1; observedLabel?: IFCLabel };
}
export interface ReadValueToolErrorOutput {
outputId: string;
status: "error";
message: string;
}
export type ReadValueToolOutput =
| ReadValueToolSuccessOutput
| ReadValueToolErrorOutput;
/** The sink a read value is released to, named in a release refusal. */
const READ_VALUE_SINK = "read_value";
/**
* Whether the position `select` names under `link` lands in a derived
* document no run has derived. A piece's derived fields live in `computed:`
* documents its run writes, so until the piece has run somewhere the store
* holds no record of one, and a read there finds nothing however real the
* path is. A position this cannot settle — one the read ceiling withholds,
* or in a space the run may not read — is left to the measured read, which
* says what stopped it.
*/
const landsUnderived = async (
runtime: Runtime,
link: NormalizedFullLink,
select: readonly string[],
sessionSpace: MemorySpace,
foreignSpaces: HarnessForeignSpaces,
): Promise => {
try {
const selected = runtime.getCellFromLink({
...link,
path: [...link.path, ...select],
schema: undefined,
});
await selected.pull();
const at = selected.resolveAsCell().getAsNormalizedFullLink();
if (
entityKindOfIdString(at.id) !== "computed" ||
!admitsFabricReference(at.space, sessionSpace, foreignSpaces)
) {
return false;
}
const document = runtime.getCellFromLink({
...at,
path: [],
schema: undefined,
});
await document.pull();
return document.getRaw() === undefined;
} catch {
return false;
}
};
/**
* The confidentiality atom types `cell` carries, for a refusal to name. A
* label that cannot be read names nothing.
*/
const confidentialityAtoms = (cell: Cell | undefined): string[] =>
cell === undefined ? [] : [
...new Set(
disclosedCfcLabels(cfcLabelViewForCellFailClosedWithStatus(cell).view)
.flatMap((label) => label.confidentiality.flat()),
),
];
export const readValueToolDescriptor: HarnessToolDescriptor = {
toolId: "read_value",
title: "Read Value",
description:
"Return the value behind a described handle token this run holds, as stored, with a position linking to another document returned as that document's handle token.",
effectClass: "read",
inputSchema: {
type: "object",
properties: {
token: {
type: "string",
description: "A handle token of the form cfh:a:.",
},
select: {
type: "string",
description: "A path within the value, such as items/0/title.",
},
},
required: ["token"],
additionalProperties: false,
} satisfies JSONSchema,
outputSchema: {
type: "object",
properties: {
outputId: { type: "string" },
status: { enum: ["ok", "error"] },
token: { type: "string" },
value: {},
message: { type: "string" },
},
required: ["outputId", "status"],
additionalProperties: false,
} satisfies JSONSchema,
tags: ["fabric", "handle", "read"],
};
export const readValueTool: HarnessToolDefinition<
ReadValueToolInput,
ReadValueToolOutput
> = {
descriptor: readValueToolDescriptor,
async invoke(context, input) {
const outputId = context.nextOutputId("read_value");
const fail = (message: string): ReadValueToolErrorOutput => ({
outputId,
status: "error",
message: scrubBareFabricIdentifiers(message),
});
const handle = typeof input.token === "string" ? input.token.trim() : "";
if (handle.length === 0) {
return fail("read_value requires a handle token");
}
// A token reaches a tool as the address it stands for, so holding the
// handle is what tells a delegated reference from a composed one.
const table = context.handleTable;
const entry = table === undefined
? undefined
: resolveHandleToken(table, handle) ?? resolveHandleRef(table, handle);
if (entry === undefined || entry.capability !== undefined) {
return fail("read_value can only read a handle this run holds");
}
if (entry.described !== true) {
return fail(
`describe it first: run describe_handle on ${entry.token}, then read_value`,
);
}
const select = typeof input.select === "string"
? input.select.split("/").filter((segment) => segment.length > 0)
: [];
const session = await sessionPieces("read_value", context);
if (!session.ok) return fail(session.message);
const { pieces } = session;
const space = pieces.getSpace();
let link;
try {
link = parseLLMFriendlyLink(
entry.ref.startsWith("/") ? entry.ref : `/${entry.ref}`,
space,
);
} catch {
return fail("read_value can only read a handle this run holds");
}
if (!admitsFabricReference(link.space, space, session.foreignSpaces)) {
return fail(
"read_value can only read this run's space or one the run was admitted to",
);
}
const { runtime } = pieces;
const releaseTx = runtime.edit();
let value: unknown;
let refusal: string | undefined;
let observedLabel: IFCLabel | undefined;
let measured: Cell | undefined;
try {
// A derived value is the piece's to produce, so a read that lands where
// no run has produced it runs the piece, as `call_verb` does, and reads
// what the run derives. Only a piece can be run: a handle on the
// derived document itself names no piece, and says so.
if (
await landsUnderived(
runtime,
link,
select,
space,
session.foreignSpaces,
)
) {
const piece = runtime.getCellFromLink({
...link,
path: [],
schema: undefined,
});
await piece.sync();
if (getPatternIdentityRef(piece) === undefined) {
return fail(
`${entry.token} is a value its piece derives, and it holds none until that piece runs; read it through the piece's handle, which runs the piece`,
);
}
// Running the piece commits what it derives and fires what its
// builtins send, so it waits on the authorization a side effect
// needs, even though the read itself does not.
if (context.sideEffectsAuthorized !== true) {
return fail(
`${entry.token} holds a value its piece derives, and the piece has not run; running it is a side effect this run is not authorized for. A run under a direct command reads it, which runs the piece`,
);
}
await runtime.startWithCommit(piece);
await runtime.idle();
}
// A link at the position itself is followed, so a piece's field reads
// as the value it aliases. Links within that value stay links. The
// position is pulled before it is resolved: a resolution stops at a
// document this replica has not loaded yet, which every link the
// selected path crosses can be.
const selected = runtime.getCellFromLink({
...link,
path: [...link.path, ...select],
schema: undefined,
}).withTx(releaseTx);
measured = selected;
await selected.pull();
const read = selected.resolveAsCell();
measured = read;
// Following the link widens nothing: the value it lands on must be in
// a space the run may read, as the handle's own space had to be.
const at = read.getAsNormalizedFullLink();
if (!admitsFabricReference(at.space, space, session.foreignSpaces)) {
return fail(
"read_value can only read this run's space or one the run was admitted to",
);
}
await read.pull();
const denied = runtime.storageManager.authorizationError?.(link.space);
if (denied !== undefined) return fail(denied.message);
const raw = read.getRaw();
if (raw === undefined) {
return fail(`${entry.token} holds nothing at that path`);
}
value = asSerializableValue(raw);
for (const path of storedLinkPaths(raw)) {
const target = runtime.getCellFromLink({
...at,
path: [...at.path, ...path.map(String)],
}).withTx(releaseTx).resolveAsCell().getAsNormalizedFullLink();
value = replacedAt(value, path, {
"@link": createLLMFriendlyLink(target, space),
});
}
const releaseGateRejects = runtime.cfcEnforcementMode !== "disabled" &&
runtime.cfcEnforcementMode !== "observe";
refusal = releaseGateRejects
? describeSinkReleaseRefusal(releaseTx, releaseTx, READ_VALUE_SINK, [])
?.reason
: undefined;
const { confidentiality } = collectConsumedLabel(releaseTx);
observedLabel = confidentiality.length > 0
? { confidentiality: [...confidentiality] }
: undefined;
} catch (error) {
// The session's read ceiling withholds a value before any release is
// measured, and binds every reader the session runs, so the refusal
// says which limit held rather than offering a route around it.
if (error instanceof CfcReadCeilingError) {
const atoms = confidentialityAtoms(measured).join(", ");
return fail(
`the value was withheld: its label${
atoms === "" ? "" : ` (${atoms})`
} is above this session's read ceiling; describe_handle lists the labels ${entry.token} carries`,
);
}
return fail(
`read_value could not read ${entry.token}: ${errorMessage(error)}`,
);
} finally {
releaseTx.abort("read_value release measurement");
}
if (refusal !== undefined) {
return fail(
`the value was withheld: what it carries is above this run's release ceiling (${refusal}). delegate_task can hand ${entry.token} to a subagent that reads it and returns what the task needs.`,
);
}
return {
outputId,
status: "ok",
token: entry.token,
value,
cfc: {
version: 1,
...(observedLabel !== undefined ? { observedLabel } : {}),
},
};
},
};