feat(ts): complete schema-first phase 2

This commit is contained in:
elpresidank 2026-06-11 07:37:59 -05:00
parent 0746d7ffd5
commit be2370ee7b
24 changed files with 465 additions and 433 deletions

View file

@ -18,7 +18,7 @@ import {
} from "./async-processor.js"; } from "./async-processor.js";
import type { Spec } from "../spec/types.js"; import type { Spec } from "../spec/types.js";
import type { BackendConsumer, PubSubBackend } from "../backend/types.js"; import type { BackendConsumer, PubSubBackend } from "../backend/types.js";
import type { FlowDefinition } from "./flow.js"; import { FlowDefinition } from "./flow.js";
import { Flow, } from "./flow.js"; import { Flow, } from "./flow.js";
import { topics } from "../schema/topics.js"; import { topics } from "../schema/topics.js";
import type { import type {
@ -128,14 +128,9 @@ const ConfigPushSchema = S.Struct({
config: S.Record(S.String, S.Unknown), config: S.Record(S.String, S.Unknown),
}); });
const FlowDefinitionSchema = S.Struct({ const FlowDefinitions = S.Record(S.String, FlowDefinition);
topics: S.optionalKey(S.Record(S.String, S.String)),
parameters: S.optionalKey(S.Record(S.String, S.Unknown)),
});
const FlowDefinitionsSchema = S.Record(S.String, FlowDefinitionSchema); const decodeFlowDefinitions = S.decodeUnknownOption(FlowDefinitions);
const decodeFlowDefinitions = S.decodeUnknownOption(FlowDefinitionsSchema);
export function runFlowProcessorDefinitionScoped< export function runFlowProcessorDefinitionScoped<
FlowRequirements = never, FlowRequirements = never,

View file

@ -43,12 +43,14 @@ import type { ProducerSpec } from "../spec/producer-spec.js";
import type { RequestResponseSpec } from "../spec/request-response-spec.js"; import type { RequestResponseSpec } from "../spec/request-response-spec.js";
import type { Spec, SpecRuntimeRequirements } from "../spec/types.js"; import type { Spec, SpecRuntimeRequirements } from "../spec/types.js";
export interface FlowDefinition { export class FlowDefinition extends S.Class<FlowDefinition>("FlowDefinition")({
/** Topic overrides keyed by spec name */ /** Topic overrides keyed by spec name */
topics?: Record<string, string>; topics: S.optionalKey(S.Record(S.String, S.String)),
/** Parameter values keyed by spec name */ /** Parameter values keyed by spec name */
parameters?: Record<string, unknown>; parameters: S.optionalKey(S.Record(S.String, S.Unknown)),
} }, {
description: "Per-flow configuration: topic overrides and parameter values keyed by spec name.",
}) {}
export interface FlowProducer<T> { export interface FlowProducer<T> {
readonly send: (id: string, message: T) => Effect.Effect<void, MessagingDeliveryError>; readonly send: (id: string, message: T) => Effect.Effect<void, MessagingDeliveryError>;

View file

@ -3,22 +3,25 @@
*/ */
import { Config, Duration, Effect } from "effect"; import { Config, Duration, Effect } from "effect";
import * as S from "effect/Schema";
export interface MessagingRuntimeConfig { export class MessagingRuntimeConfig extends S.Class<MessagingRuntimeConfig>("MessagingRuntimeConfig")({
readonly consumerReceiveTimeout: Duration.Duration; consumerReceiveTimeout: S.Duration,
readonly consumerErrorBackoff: Duration.Duration; consumerErrorBackoff: S.Duration,
readonly rateLimitRetry: Duration.Duration; rateLimitRetry: S.Duration,
readonly rateLimitTimeout: Duration.Duration; rateLimitTimeout: S.Duration,
readonly requestTimeout: Duration.Duration; requestTimeout: S.Duration,
} }, {
description: "Messaging runtime timing windows for consumer receive, backoff, rate-limit retry, and request timeout.",
}) {}
export const defaultMessagingRuntimeConfig: MessagingRuntimeConfig = { export const defaultMessagingRuntimeConfig: MessagingRuntimeConfig = MessagingRuntimeConfig.make({
consumerReceiveTimeout: Duration.millis(2_000), consumerReceiveTimeout: Duration.millis(2_000),
consumerErrorBackoff: Duration.millis(1_000), consumerErrorBackoff: Duration.millis(1_000),
rateLimitRetry: Duration.millis(10_000), rateLimitRetry: Duration.millis(10_000),
rateLimitTimeout: Duration.millis(7_200_000), rateLimitTimeout: Duration.millis(7_200_000),
requestTimeout: Duration.millis(300_000), requestTimeout: Duration.millis(300_000),
}; });
const durationConfig = (name: string, defaultValue: Duration.Duration) => const durationConfig = (name: string, defaultValue: Duration.Duration) =>
Config.duration(name).pipe( Config.duration(name).pipe(
@ -48,11 +51,11 @@ export const loadMessagingRuntimeConfig = Effect.fn("loadMessagingRuntimeConfig"
defaultMessagingRuntimeConfig.requestTimeout, defaultMessagingRuntimeConfig.requestTimeout,
); );
return { return MessagingRuntimeConfig.make({
consumerReceiveTimeout, consumerReceiveTimeout,
consumerErrorBackoff, consumerErrorBackoff,
rateLimitRetry, rateLimitRetry,
rateLimitTimeout, rateLimitTimeout,
requestTimeout, requestTimeout,
} satisfies MessagingRuntimeConfig; });
}); });

View file

@ -16,12 +16,12 @@ import * as S from "effect/Schema";
import * as Command from "effect/unstable/cli/Command"; import * as Command from "effect/unstable/cli/Command";
import * as Flag from "effect/unstable/cli/Flag"; import * as Flag from "effect/unstable/cli/Flag";
export interface CliOpts { export class CliOpts extends S.Class<CliOpts>("CliOpts")({
gateway: string; gateway: S.String,
user: string; user: S.String,
token?: string; token: S.optionalKey(S.String),
flow: string; flow: S.String,
} }, { description: "Resolved TrustGraph CLI connection options." }) {}
export const rootCommand = Command.make("tg").pipe( export const rootCommand = Command.make("tg").pipe(
Command.withDescription("TrustGraph CLI - interact with TrustGraph services"), Command.withDescription("TrustGraph CLI - interact with TrustGraph services"),

View file

@ -430,7 +430,7 @@ const onAgentRequest = Effect.fn("AgentService.onRequest")(function* (
chunk_type: "explain", chunk_type: "explain",
content: "", content: "",
explain_id: explain.explainId, explain_id: explain.explainId,
explain_triples: explain.triples, explain_triples: [...explain.triples],
}); });
} }

View file

@ -15,10 +15,8 @@ import type {
TriplesQueryResponse, TriplesQueryResponse,
ToolRequest, ToolRequest,
ToolResponse, ToolResponse,
Term,
Triple,
} from "@trustgraph/base"; } from "@trustgraph/base";
import {Term as TermSchema} from "@trustgraph/base"; import { Term, Triple } from "@trustgraph/base";
import { Effect, Match } from "effect"; import { Effect, Match } from "effect";
import * as O from "effect/Option"; import * as O from "effect/Option";
import * as Predicate from "effect/Predicate"; import * as Predicate from "effect/Predicate";
@ -27,7 +25,7 @@ import type { AgentTool, ToolArg } from "./types.js";
import { agentToolError, } from "./types.js"; import { agentToolError, } from "./types.js";
const decodeJsonUnknown = S.decodeUnknownOption(S.UnknownFromJsonString); const decodeJsonUnknown = S.decodeUnknownOption(S.UnknownFromJsonString);
const decodeTerm = S.decodeUnknownOption(TermSchema); const decodeTerm = S.decodeUnknownOption(Term);
/** /**
* Format a Term to a human-readable string. * Format a Term to a human-readable string.
@ -64,10 +62,10 @@ function parseQuestion(input: string): string {
/** /**
* Explain data extracted from a graph-rag response. * Explain data extracted from a graph-rag response.
*/ */
export interface ExplainData { export class ExplainData extends S.Class<ExplainData>("ExplainData")({
explainId: string; explainId: S.String,
triples: Triple[]; triples: S.Array(Triple),
} }, { description: "Explain payload extracted from a graph-rag response: id plus supporting triples." }) {}
/** /**
* Query the knowledge graph for information about entities and their relationships. * Query the knowledge graph for information about entities and their relationships.

View file

@ -20,11 +20,11 @@ export const agentToolError = (operation: string, cause: unknown): AgentToolErro
message: errorMessage(cause), message: errorMessage(cause),
}); });
export interface ToolArg { export class ToolArg extends S.Class<ToolArg>("ToolArg")({
name: string; name: S.String,
type: string; type: S.String,
description: string; description: S.String,
} }, { description: "A named, typed argument accepted by an agent tool." }) {}
export interface AgentTool { export interface AgentTool {
name: string; name: string;
@ -43,10 +43,10 @@ export type ReActState =
| "final_answer" | "final_answer"
| "complete"; | "complete";
export interface ParsedEvent { export class ParsedEvent extends S.Class<ParsedEvent>("ParsedEvent")({
type: "thought" | "action" | "action_input" | "final_answer"; type: S.Literals(["thought", "action", "action_input", "final_answer"]),
content: string; content: S.String,
} }, { description: "A parsed ReAct stream event with its section content." }) {}
export type OnThought = (text: string, isFinal: boolean) => Effect.Effect<void, AgentToolError>; export type OnThought = (text: string, isFinal: boolean) => Effect.Effect<void, AgentToolError>;
export type OnObservation = (text: string, isFinal: boolean) => Effect.Effect<void, AgentToolError>; export type OnObservation = (text: string, isFinal: boolean) => Effect.Effect<void, AgentToolError>;

View file

@ -8,14 +8,15 @@
import * as MutableHashMap from "effect/MutableHashMap"; import * as MutableHashMap from "effect/MutableHashMap";
import * as O from "effect/Option"; import * as O from "effect/Option";
import * as S from "effect/Schema";
export interface CollectionEntry { export class CollectionEntry extends S.Class<CollectionEntry>("CollectionEntry")({
user: string; user: S.String,
collection: string; collection: S.String,
name: string; name: S.String,
description: string; description: S.String,
tags: string[]; tags: S.Array(S.String),
} }, { description: "A librarian collection registration with its metadata tags." }) {}
export interface CollectionManager { export interface CollectionManager {
readonly listCollections: (user: string) => CollectionEntry[]; readonly listCollections: (user: string) => CollectionEntry[];

View file

@ -39,10 +39,10 @@ export type TextCompletionRuntimeError =
| TextCompletionProviderError | TextCompletionProviderError
| TooManyRequestsError; | TooManyRequestsError;
export interface LanguageModelProviderRequest { export class LanguageModelProviderRequest extends S.Class<LanguageModelProviderRequest>("LanguageModelProviderRequest")({
readonly model: string; model: S.String,
readonly temperature: number; temperature: S.Finite,
} }, { description: "Resolved model id and temperature for a language-model call." }) {}
export interface LanguageModelProviderOptions<Requirements> { export interface LanguageModelProviderOptions<Requirements> {
readonly provider: string; readonly provider: string;

View file

@ -47,10 +47,10 @@ import * as MutableHashMap from "effect/MutableHashMap";
import * as O from "effect/Option"; import * as O from "effect/Option";
import * as S from "effect/Schema"; import * as S from "effect/Schema";
export interface PromptTemplate { export class PromptTemplate extends S.Class<PromptTemplate>("PromptTemplate")({
system: string; system: S.String,
prompt: string; prompt: S.String,
} }, { description: "A prompt template: system preamble plus prompt body." }) {}
export interface PromptTemplateConfig extends ProcessorConfig { export interface PromptTemplateConfig extends ProcessorConfig {
configKey?: string; configKey?: string;

View file

@ -4,22 +4,22 @@ import { errorMessage } from "@trustgraph/base";
import { Effect } from "effect"; import { Effect } from "effect";
import * as S from "effect/Schema"; import * as S from "effect/Schema";
export interface QdrantCollectionStatus { export class QdrantCollectionStatus extends S.Class<QdrantCollectionStatus>("QdrantCollectionStatus")({
readonly exists: boolean; exists: S.Boolean,
} }, { description: "Qdrant collection existence probe result." }) {}
export interface QdrantCollectionDescription { export class QdrantCollectionDescription extends S.Class<QdrantCollectionDescription>("QdrantCollectionDescription")({
readonly name: string; name: S.String,
} }, { description: "A named Qdrant collection." }) {}
export interface QdrantCollections { export class QdrantCollections extends S.Class<QdrantCollections>("QdrantCollections")({
readonly collections: ReadonlyArray<QdrantCollectionDescription>; collections: S.Array(QdrantCollectionDescription),
} }, { description: "Qdrant collection listing." }) {}
export interface QdrantScoredPoint { export class QdrantScoredPoint extends S.Class<QdrantScoredPoint>("QdrantScoredPoint")({
readonly score: number; score: S.Finite,
readonly payload?: unknown; payload: S.optionalKey(S.Unknown),
} }, { description: "A scored Qdrant search hit with optional payload." }) {}
export class QdrantClientError extends S.TaggedErrorClass<QdrantClientError>()("QdrantClientError", { export class QdrantClientError extends S.TaggedErrorClass<QdrantClientError>()("QdrantClientError", {
message: S.String, message: S.String,

View file

@ -20,18 +20,18 @@ export interface QdrantDocQueryConfig {
clientFactory?: QdrantClientFactory; clientFactory?: QdrantClientFactory;
} }
export interface ChunkMatch { export class ChunkMatch extends S.Class<ChunkMatch>("ChunkMatch")({
chunkId: string; chunkId: S.String,
score: number; score: S.Finite,
content?: string; content: S.optionalKey(S.String),
} }, { description: "A scored document-chunk match from embeddings query." }) {}
export interface DocEmbeddingsQueryRequest { export class DocEmbeddingsQueryRequest extends S.Class<DocEmbeddingsQueryRequest>("DocEmbeddingsQueryRequest")({
vector: number[]; vector: S.Array(S.Finite),
user: string; user: S.String,
collection: string; collection: S.String,
limit: number; limit: S.Finite,
} }, { description: "Document embeddings similarity query request." }) {}
export class QdrantDocEmbeddingsQueryError extends S.TaggedErrorClass<QdrantDocEmbeddingsQueryError>()( export class QdrantDocEmbeddingsQueryError extends S.TaggedErrorClass<QdrantDocEmbeddingsQueryError>()(
"QdrantDocEmbeddingsQueryError", "QdrantDocEmbeddingsQueryError",

View file

@ -10,8 +10,7 @@
* Python reference: trustgraph-flow/trustgraph/query/graph_embeddings/qdrant/service.py * Python reference: trustgraph-flow/trustgraph/query/graph_embeddings/qdrant/service.py
*/ */
import type { Term } from "@trustgraph/base"; import { Term, errorMessage } from "@trustgraph/base";
import { errorMessage, } from "@trustgraph/base";
import { Config, Context, Effect, Layer } from "effect"; import { Config, Context, Effect, Layer } from "effect";
import * as O from "effect/Option"; import * as O from "effect/Option";
import * as S from "effect/Schema"; import * as S from "effect/Schema";
@ -24,17 +23,17 @@ export interface QdrantGraphQueryConfig {
clientFactory?: QdrantClientFactory; clientFactory?: QdrantClientFactory;
} }
export interface EntityMatch { export class EntityMatch extends S.Class<EntityMatch>("EntityMatch")({
entity: Term; entity: Term,
score: number; score: S.Finite,
} }, { description: "A scored graph-entity match from embeddings query." }) {}
export interface GraphEmbeddingsQueryRequest { export class GraphEmbeddingsQueryRequest extends S.Class<GraphEmbeddingsQueryRequest>("GraphEmbeddingsQueryRequest")({
vector: number[]; vector: S.Array(S.Finite),
user: string; user: S.String,
collection: string; collection: S.String,
limit: number; limit: S.Finite,
} }, { description: "Graph embeddings similarity query request." }) {}
export class QdrantGraphEmbeddingsQueryError extends S.TaggedErrorClass<QdrantGraphEmbeddingsQueryError>()( export class QdrantGraphEmbeddingsQueryError extends S.TaggedErrorClass<QdrantGraphEmbeddingsQueryError>()(
"QdrantGraphEmbeddingsQueryError", "QdrantGraphEmbeddingsQueryError",

View file

@ -138,7 +138,7 @@ const onGraphRagRequest = Effect.fn("GraphRagService.onRequest")(function* (
endOfStream: true, endOfStream: true,
message_type: "explain", message_type: "explain",
explain_id: `explain-${requestId}`, explain_id: `explain-${requestId}`,
explain_triples: result.subgraph, explain_triples: [...result.subgraph],
}; };
yield* producer.send(requestId, response); yield* producer.send(requestId, response);

View file

@ -16,23 +16,22 @@ import type {
Term, Term,
TextCompletionRequest, TextCompletionRequest,
TextCompletionResponse, TextCompletionResponse,
Triple,
TriplesQueryRequest, TriplesQueryRequest,
TriplesQueryResponse, TriplesQueryResponse,
} from "@trustgraph/base"; } from "@trustgraph/base";
import { errorMessage } from "@trustgraph/base"; import { Triple, errorMessage } from "@trustgraph/base";
import { Context, Effect, Layer, Match } from "effect"; import { Context, Effect, Layer, Match } from "effect";
import * as O from "effect/Option"; import * as O from "effect/Option";
import * as S from "effect/Schema"; import * as S from "effect/Schema";
export interface GraphRagConfig { export class GraphRagConfig extends S.Class<GraphRagConfig>("GraphRagConfig")({
entityLimit?: number; entityLimit: S.optionalKey(S.Finite),
tripleLimit?: number; tripleLimit: S.optionalKey(S.Finite),
maxSubgraphSize?: number; maxSubgraphSize: S.optionalKey(S.Finite),
maxPathLength?: number; maxPathLength: S.optionalKey(S.Finite),
edgeScoreLimit?: number; edgeScoreLimit: S.optionalKey(S.Finite),
edgeLimit?: number; edgeLimit: S.optionalKey(S.Finite),
} }, { description: "Graph RAG retrieval tuning limits." }) {}
export interface GraphRagClients { export interface GraphRagClients {
llm: EffectRequestResponse<TextCompletionRequest, TextCompletionResponse>; llm: EffectRequestResponse<TextCompletionRequest, TextCompletionResponse>;
@ -53,10 +52,10 @@ export interface GraphRagQueryOptions {
readonly chunkCallback?: ChunkCallback; readonly chunkCallback?: ChunkCallback;
} }
export interface GraphRagResult { export class GraphRagResult extends S.Class<GraphRagResult>("GraphRagResult")({
answer: string; answer: S.String,
subgraph: Triple[]; subgraph: S.Array(Triple),
} }, { description: "Graph RAG answer with the supporting subgraph." }) {}
interface NormalizedGraphRagConfig { interface NormalizedGraphRagConfig {
entityLimit: number; entityLimit: number;

View file

@ -22,17 +22,17 @@ export interface QdrantDocEmbeddingsConfig {
clientFactory?: QdrantClientFactory; clientFactory?: QdrantClientFactory;
} }
export interface DocEmbeddingChunk { export class DocEmbeddingChunk extends S.Class<DocEmbeddingChunk>("DocEmbeddingChunk")({
chunkId: string; chunkId: S.String,
vector: number[]; vector: S.Array(S.Finite),
content?: string; content: S.optionalKey(S.String),
} }, { description: "A document chunk paired with its embedding vector." }) {}
export interface DocEmbeddingsMessage { export class DocEmbeddingsMessage extends S.Class<DocEmbeddingsMessage>("DocEmbeddingsMessage")({
user: string; user: S.String,
collection: string; collection: S.String,
chunks: DocEmbeddingChunk[]; chunks: S.Array(DocEmbeddingChunk),
} }, { description: "Document embeddings store message: chunks to upsert for a user collection." }) {}
export class QdrantDocEmbeddingsStoreError extends S.TaggedErrorClass<QdrantDocEmbeddingsStoreError>()( export class QdrantDocEmbeddingsStoreError extends S.TaggedErrorClass<QdrantDocEmbeddingsStoreError>()(
"QdrantDocEmbeddingsStoreError", "QdrantDocEmbeddingsStoreError",

View file

@ -8,8 +8,7 @@
* Python reference: trustgraph-flow/trustgraph/storage/graph_embeddings/qdrant/write.py * Python reference: trustgraph-flow/trustgraph/storage/graph_embeddings/qdrant/write.py
*/ */
import type { Term } from "@trustgraph/base"; import { Term, errorMessage } from "@trustgraph/base";
import { errorMessage, } from "@trustgraph/base";
import { Config, Context, Effect, Layer, Match, Random } from "effect"; import { Config, Context, Effect, Layer, Match, Random } from "effect";
import * as MutableHashSet from "effect/MutableHashSet"; import * as MutableHashSet from "effect/MutableHashSet";
import * as O from "effect/Option"; import * as O from "effect/Option";
@ -23,17 +22,17 @@ export interface QdrantGraphEmbeddingsConfig {
clientFactory?: QdrantClientFactory; clientFactory?: QdrantClientFactory;
} }
export interface GraphEmbeddingEntity { export class GraphEmbeddingEntity extends S.Class<GraphEmbeddingEntity>("GraphEmbeddingEntity")({
entity: Term; entity: Term,
vector: number[]; vector: S.Array(S.Finite),
chunkId?: string; chunkId: S.optionalKey(S.String),
} }, { description: "A graph entity paired with its embedding vector." }) {}
export interface GraphEmbeddingsMessage { export class GraphEmbeddingsMessage extends S.Class<GraphEmbeddingsMessage>("GraphEmbeddingsMessage")({
user: string; user: S.String,
collection: string; collection: S.String,
entities: GraphEmbeddingEntity[]; entities: S.Array(GraphEmbeddingEntity),
} }, { description: "Graph embeddings store message: entities to upsert for a user collection." }) {}
export class QdrantGraphEmbeddingsStoreError extends S.TaggedErrorClass<QdrantGraphEmbeddingsStoreError>()( export class QdrantGraphEmbeddingsStoreError extends S.TaggedErrorClass<QdrantGraphEmbeddingsStoreError>()(
"QdrantGraphEmbeddingsStoreError", "QdrantGraphEmbeddingsStoreError",

View file

@ -1217,16 +1217,18 @@ export interface TrustGraphMcpOptions {
readonly port?: number | undefined readonly port?: number | undefined
} }
export interface TrustGraphMcpConfigShape { const McpPathInput = S.Union([S.Literal("*"), S.TemplateLiteral(["/", S.String])])
readonly gatewayUrl: string
readonly user: string export class TrustGraphMcpConfigShape extends S.Class<TrustGraphMcpConfigShape>("TrustGraphMcpConfigShape")({
readonly token: string | undefined gatewayUrl: S.String,
readonly flowId: string user: S.String,
readonly name: string token: S.UndefinedOr(S.String),
readonly version: string flowId: S.String,
readonly mcpPath: HttpRouter.PathInput name: S.String,
readonly port: number version: S.String,
} mcpPath: McpPathInput,
port: S.Finite,
}, { description: "Resolved TrustGraph MCP server configuration." }) {}
const readNonEmpty = (value: string | undefined): string | undefined => const readNonEmpty = (value: string | undefined): string | undefined =>
value !== undefined && value.length > 0 ? value : undefined value !== undefined && value.length > 0 ? value : undefined

View file

@ -71,6 +71,44 @@ type WorkbenchError = WorkbenchPromiseError;
const isWorkbenchPromiseError = S.is(WorkbenchPromiseError); const isWorkbenchPromiseError = S.is(WorkbenchPromiseError);
const ClientTriple: S.Codec<Triple, Triple> = S.suspend(() =>
S.Struct({
s: ClientTerm,
p: ClientTerm,
o: ClientTerm,
g: S.optionalKey(S.String),
})
);
const ClientTerm: S.Codec<Term, Term> = S.suspend(() =>
S.Union([
S.Struct({
t: S.Literal("i"),
i: S.String,
}),
S.Struct({
t: S.Literal("b"),
d: S.String,
}),
S.Struct({
t: S.Literal("l"),
v: S.String,
dt: S.optionalKey(S.String),
ln: S.optionalKey(S.String),
}),
S.Struct({
t: S.Literal("t"),
tr: S.optionalKey(ClientTriple),
}),
])
);
const ClientExplainEvent: S.Codec<ExplainEvent, ExplainEvent> = S.Struct({
explainId: S.String,
explainGraph: S.String,
explainTriples: S.optionalKey(S.Array(ClientTriple).pipe(S.mutable)),
});
function errorMessage(error: unknown): string { function errorMessage(error: unknown): string {
if (isWorkbenchPromiseError(error)) return error.message; if (isWorkbenchPromiseError(error)) return error.message;
if (Predicate.isObject(error) && Predicate.hasProperty(error, "message")) { if (Predicate.isObject(error) && Predicate.hasProperty(error, "message")) {
@ -146,25 +184,25 @@ const mutationCounter = Metric.counter("trustgraph_workbench_mutation_total", {
// Shared types // Shared types
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export interface FeatureSwitches { export class FeatureSwitches extends S.Class<FeatureSwitches>("FeatureSwitches")({
flowClasses: boolean; flowClasses: S.Boolean,
submissions: boolean; submissions: S.Boolean,
tokenCost: boolean; tokenCost: S.Boolean,
schemas: boolean; schemas: S.Boolean,
structuredQuery: boolean; structuredQuery: S.Boolean,
ontologyEditor: boolean; ontologyEditor: S.Boolean,
agentTools: boolean; agentTools: S.Boolean,
mcpTools: boolean; mcpTools: S.Boolean,
llmModels: boolean; llmModels: S.Boolean,
} }, { description: "Workbench feature visibility switches." }) {}
export interface Settings { export class Settings extends S.Class<Settings>("Settings")({
user: string; user: S.String,
apiKey: string; apiKey: S.String,
collection: string; collection: S.String,
gatewayUrl: string; gatewayUrl: S.String,
featureSwitches: FeatureSwitches; featureSwitches: FeatureSwitches,
} }, { description: "Persisted workbench connection and display settings." }) {}
export interface WorkbenchApiFactory { export interface WorkbenchApiFactory {
readonly create: (settings: Settings) => BaseApi; readonly create: (settings: Settings) => BaseApi;
@ -176,31 +214,31 @@ export type ChatMode = "graph-rag" | "document-rag" | "agent";
export type MessageRole = "user" | "assistant" | "system"; export type MessageRole = "user" | "assistant" | "system";
export type AgentPhase = "think" | "observe" | "answer"; export type AgentPhase = "think" | "observe" | "answer";
export interface ChatMessage { export class ChatMessage extends S.Class<ChatMessage>("ChatMessage")({
id: string; id: S.String,
role: MessageRole; role: S.Literals(["user", "assistant", "system"]),
content: string; content: S.String,
timestamp: number; timestamp: S.Finite,
isStreaming?: boolean; isStreaming: S.optionalKey(S.Boolean),
metadata?: { metadata: S.optionalKey(S.Struct({
model?: string; model: S.optionalKey(S.String),
inTokens?: number; inTokens: S.optionalKey(S.Finite),
outTokens?: number; outTokens: S.optionalKey(S.Finite),
}; })),
agentPhases?: { agentPhases: S.optionalKey(S.Struct({
think: string; think: S.String,
observe: string; observe: S.String,
answer: string; answer: S.String,
}; })),
activePhase?: AgentPhase; activePhase: S.optionalKey(S.Literals(["think", "observe", "answer"])),
explainEvents?: ExplainEvent[]; explainEvents: S.optionalKey(S.Array(ClientExplainEvent).pipe(S.mutable)),
} }, { description: "A rendered chat transcript message." }) {}
export interface ConversationState { export class ConversationState extends S.Class<ConversationState>("ConversationState")({
messages: ChatMessage[]; messages: S.Array(ChatMessage).pipe(S.mutable),
input: string; input: S.String,
chatMode: ChatMode; chatMode: S.Literals(["graph-rag", "document-rag", "agent"]),
} }, { description: "Persisted workbench chat state." }) {}
export interface FlowSummary { export interface FlowSummary {
id: string; id: string;
@ -216,60 +254,60 @@ export interface ProcessingMetadata {
[key: string]: unknown; [key: string]: unknown;
} }
export interface UploadProgress { export class UploadProgress extends S.Class<UploadProgress>("UploadProgress")({
phase: "preparing" | "uploading" | "finalizing"; phase: S.Literals(["preparing", "uploading", "finalizing"]),
chunksTotal: number; chunksTotal: S.Finite,
chunksUploaded: number; chunksUploaded: S.Finite,
bytesTotal: number; bytesTotal: S.Finite,
bytesUploaded: number; bytesUploaded: S.Finite,
} }, { description: "Current chunked document upload progress." }) {}
export interface UploadForm { export class UploadForm extends S.Class<UploadForm>("UploadForm")({
file: File | null; file: S.NullOr(S.File),
title: string; title: S.String,
tags: string; tags: S.String,
comments: string; comments: S.String,
uploading: boolean; uploading: S.Boolean,
dragOver: boolean; dragOver: S.Boolean,
progress: UploadProgress | null; progress: S.NullOr(UploadProgress),
} }, { description: "Workbench document upload form state." }) {}
export interface McpServerConfig { export class McpServerConfig extends S.Class<McpServerConfig>("McpServerConfig")({
url: string; url: S.String,
"remote-name"?: string; "remote-name": S.optionalKey(S.String),
"auth-token"?: string; "auth-token": S.optionalKey(S.String),
} }, { description: "Workbench MCP server config entry payload." }) {}
export interface McpServerEntry { export class McpServerEntry extends S.Class<McpServerEntry>("McpServerEntry")({
key: string; key: S.String,
config: McpServerConfig; config: McpServerConfig,
} }, { description: "Workbench MCP server config entry." }) {}
export interface ToolArgument { export class ToolArgument extends S.Class<ToolArgument>("ToolArgument")({
name: string; name: S.String,
type: string; type: S.String,
description: string; description: S.String,
} }, { description: "Workbench MCP tool argument descriptor." }) {}
export interface ToolConfig { export class ToolConfig extends S.Class<ToolConfig>("ToolConfig")({
type: string; type: S.String,
name: string; name: S.String,
description: string; description: S.String,
"mcp-tool"?: string; "mcp-tool": S.optionalKey(S.String),
group?: string[]; group: S.optionalKey(S.Array(S.String).pipe(S.mutable)),
arguments?: ToolArgument[]; arguments: S.optionalKey(S.Array(ToolArgument).pipe(S.mutable)),
} }, { description: "Workbench tool config entry payload." }) {}
export interface ToolEntry { export class ToolEntry extends S.Class<ToolEntry>("ToolEntry")({
key: string; key: S.String,
config: ToolConfig; config: ToolConfig,
} }, { description: "Workbench tool config entry." }) {}
export interface TokenCost { export class TokenCost extends S.Class<TokenCost>("TokenCost")({
model: string; model: S.String,
input_price: number; input_price: S.Finite,
output_price: number; output_price: S.Finite,
} }, { description: "Model token pricing row." }) {}
export interface CollectionSummary { export interface CollectionSummary {
id?: string; id?: string;
@ -280,61 +318,61 @@ export interface CollectionSummary {
[key: string]: unknown; [key: string]: unknown;
} }
export interface Notification { export class Notification extends S.Class<Notification>("Notification")({
id: string; id: S.String,
type: "success" | "error" | "warning" | "info"; type: S.Literals(["success", "error", "warning", "info"]),
title: string; title: S.String,
description?: string; description: S.optionalKey(S.String),
} }, { description: "Transient workbench notification toast." }) {}
export interface McpServerForm { export class McpServerForm extends S.Class<McpServerForm>("McpServerForm")({
key: string; key: S.String,
url: string; url: S.String,
remoteName: string; remoteName: S.String,
authToken: string; authToken: S.String,
showToken: boolean; showToken: S.Boolean,
saving: boolean; saving: S.Boolean,
keyError: string; keyError: S.String,
} }, { description: "Editable MCP server dialog state." }) {}
export interface McpToolForm { export class McpToolForm extends S.Class<McpToolForm>("McpToolForm")({
key: string; key: S.String,
name: string; name: S.String,
description: string; description: S.String,
mcpTool: string; mcpTool: S.String,
group: string; group: S.String,
args: ToolArgument[]; args: S.Array(ToolArgument).pipe(S.mutable),
saving: boolean; saving: S.Boolean,
keyError: string; keyError: S.String,
} }, { description: "Editable MCP tool dialog state." }) {}
export interface StartFlowForm { export class StartFlowForm extends S.Class<StartFlowForm>("StartFlowForm")({
id: string; id: S.String,
blueprint: string; blueprint: S.String,
description: string; description: S.String,
paramsJson: string; paramsJson: S.String,
submitting: boolean; submitting: S.Boolean,
paramsError: string | null; paramsError: S.NullOr(S.String),
submitted: boolean; submitted: S.Boolean,
definitionExpanded: boolean; definitionExpanded: S.Boolean,
} }, { description: "Start-flow dialog form state." }) {}
export interface CollectionForm { export class CollectionForm extends S.Class<CollectionForm>("CollectionForm")({
id: string; id: S.String,
name: string; name: S.String,
description: string; description: S.String,
tags: string; tags: S.String,
submitting: boolean; submitting: S.Boolean,
} }, { description: "Collection creation form state." }) {}
export interface GraphViewState { export class GraphViewState extends S.Class<GraphViewState>("GraphViewState")({
searchTerm: string; searchTerm: S.String,
selectedNodeId: string | null; selectedNodeId: S.NullOr(S.String),
selectedNodeLabel: string | null; selectedNodeLabel: S.NullOr(S.String),
showLabels: boolean; showLabels: S.Boolean,
showTypes: boolean; showTypes: S.Boolean,
nodeLimit: number; nodeLimit: S.Finite,
} }, { description: "Workbench graph display controls." }) {}
const DEFAULT_FEATURE_SWITCHES: FeatureSwitches = { const DEFAULT_FEATURE_SWITCHES: FeatureSwitches = {
flowClasses: false, flowClasses: false,
@ -356,50 +394,6 @@ export const DEFAULT_SETTINGS: Settings = {
featureSwitches: DEFAULT_FEATURE_SWITCHES, featureSwitches: DEFAULT_FEATURE_SWITCHES,
}; };
const SettingsSchema = S.Struct({
user: S.String,
apiKey: S.String,
collection: S.String,
gatewayUrl: S.String,
featureSwitches: S.Struct({
flowClasses: S.Boolean,
submissions: S.Boolean,
tokenCost: S.Boolean,
schemas: S.Boolean,
structuredQuery: S.Boolean,
ontologyEditor: S.Boolean,
agentTools: S.Boolean,
mcpTools: S.Boolean,
llmModels: S.Boolean,
}),
});
const ChatMessageSchema = S.Struct({
id: S.String,
role: S.Union([S.Literal("user"), S.Literal("assistant"), S.Literal("system")]),
content: S.String,
timestamp: S.Finite,
isStreaming: S.optionalKey(S.Boolean),
metadata: S.optionalKey(S.Struct({
model: S.optionalKey(S.String),
inTokens: S.optionalKey(S.Finite),
outTokens: S.optionalKey(S.Finite),
})),
agentPhases: S.optionalKey(S.Struct({
think: S.String,
observe: S.String,
answer: S.String,
})),
activePhase: S.optionalKey(S.Union([S.Literal("think"), S.Literal("observe"), S.Literal("answer")])),
explainEvents: S.optionalKey(S.Array(S.Unknown)),
});
const ConversationSchema = S.Struct({
messages: S.Array(ChatMessageSchema),
input: S.String,
chatMode: S.Union([S.Literal("graph-rag"), S.Literal("document-rag"), S.Literal("agent")]),
});
const ThemeSchema = S.Union([S.Literal("dark"), S.Literal("light")]); const ThemeSchema = S.Union([S.Literal("dark"), S.Literal("light")]);
const FlowIdSchema = S.String; const FlowIdSchema = S.String;
@ -523,11 +517,14 @@ const randomId = Effect.fn("trustgraph.workbench.randomId")(function*(prefix: st
function metadataFrom(metadata: StreamingMetadata | undefined): ChatMessage["metadata"] | undefined { function metadataFrom(metadata: StreamingMetadata | undefined): ChatMessage["metadata"] | undefined {
if (metadata === undefined) return undefined; if (metadata === undefined) return undefined;
const result: NonNullable<ChatMessage["metadata"]> = {}; if (metadata.model === undefined && metadata.in_token === undefined && metadata.out_token === undefined) {
if (metadata.model !== undefined) result.model = metadata.model; return undefined;
if (metadata.in_token !== undefined) result.inTokens = metadata.in_token; }
if (metadata.out_token !== undefined) result.outTokens = metadata.out_token; return {
return Object.keys(result).length > 0 ? result : undefined; ...(metadata.model !== undefined ? { model: metadata.model } : {}),
...(metadata.in_token !== undefined ? { inTokens: metadata.in_token } : {}),
...(metadata.out_token !== undefined ? { outTokens: metadata.out_token } : {}),
};
} }
function withoutActivePhase(message: ChatMessage): ChatMessage { function withoutActivePhase(message: ChatMessage): ChatMessage {
@ -609,40 +606,8 @@ const StreamingEnvelopeSchema = S.Struct({
}); });
type StreamingEnvelope = typeof StreamingEnvelopeSchema.Type; type StreamingEnvelope = typeof StreamingEnvelopeSchema.Type;
const ClientTripleSchema: S.Codec<Triple, Triple> = S.suspend(() =>
S.Struct({
s: ClientTermSchema,
p: ClientTermSchema,
o: ClientTermSchema,
g: S.optionalKey(S.String),
})
);
const ClientTermSchema: S.Codec<Term, Term> = S.suspend(() =>
S.Union([
S.Struct({
t: S.Literal("i"),
i: S.String,
}),
S.Struct({
t: S.Literal("b"),
d: S.String,
}),
S.Struct({
t: S.Literal("l"),
v: S.String,
dt: S.optionalKey(S.String),
ln: S.optionalKey(S.String),
}),
S.Struct({
t: S.Literal("t"),
tr: S.optionalKey(ClientTripleSchema),
}),
])
);
const decodeStreamingEnvelope = S.decodeUnknownOption(StreamingEnvelopeSchema); const decodeStreamingEnvelope = S.decodeUnknownOption(StreamingEnvelopeSchema);
const decodeClientTriples = S.decodeUnknownOption(S.Array(ClientTripleSchema).pipe(S.mutable)); const decodeClientTriples = S.decodeUnknownOption(S.Array(ClientTriple).pipe(S.mutable));
function gatewayHttpBaseUrl(settings: Settings): string { function gatewayHttpBaseUrl(settings: Settings): string {
const raw = settings.gatewayUrl.trim(); const raw = settings.gatewayUrl.trim();
@ -1532,7 +1497,7 @@ function withActivity<A, R>(
export const settingsAtom = Atom.kvs({ export const settingsAtom = Atom.kvs({
runtime: workbenchRuntime, runtime: workbenchRuntime,
key: "trustgraph-workbench-settings-v1", key: "trustgraph-workbench-settings-v1",
schema: S.toCodecJson(SettingsSchema), schema: S.toCodecJson(Settings),
defaultValue: legacySettings, defaultValue: legacySettings,
}).pipe(Atom.keepAlive) as Atom.Writable<Settings, Settings>; }).pipe(Atom.keepAlive) as Atom.Writable<Settings, Settings>;
@ -1553,7 +1518,7 @@ export const flowIdAtom = Atom.kvs({
export const conversationAtom = Atom.kvs({ export const conversationAtom = Atom.kvs({
runtime: workbenchRuntime, runtime: workbenchRuntime,
key: "trustgraph-workbench-conversation-v1", key: "trustgraph-workbench-conversation-v1",
schema: S.toCodecJson(ConversationSchema), schema: S.toCodecJson(ConversationState),
defaultValue: legacyConversation, defaultValue: legacyConversation,
}).pipe(Atom.keepAlive) as unknown as Atom.Writable<ConversationState, ConversationState>; }).pipe(Atom.keepAlive) as unknown as Atom.Writable<ConversationState, ConversationState>;
@ -1855,17 +1820,17 @@ export const collectionsAtom = queryAtom(
{ reactivityKeys: ["collections"] }, { reactivityKeys: ["collections"] },
).pipe(Atom.setIdleTTL("2 minutes")); ).pipe(Atom.setIdleTTL("2 minutes"));
export interface GraphTriplesInput { export class GraphTriplesInput extends S.Class<GraphTriplesInput>("GraphTriplesInput")({
readonly flowId: string; flowId: S.String,
readonly collection: string; collection: S.String,
readonly limit: number; limit: S.Finite,
} }, { description: "Workbench graph triples query atom input." }) {}
export interface ExplainTriplesInput { export class ExplainTriplesInput extends S.Class<ExplainTriplesInput>("ExplainTriplesInput")({
readonly events: ExplainEvent[]; events: S.Array(ClientExplainEvent).pipe(S.mutable),
readonly flowId: string; flowId: S.String,
readonly collection: string; collection: S.String,
} }, { description: "Workbench explain triples query atom input." }) {}
const atomFamilyKeySeparator = "\u001f"; const atomFamilyKeySeparator = "\u001f";
const explainGraphSeparator = "\u001e"; const explainGraphSeparator = "\u001e";
@ -2139,13 +2104,13 @@ export const deleteMcpToolAtom = commandAtom<string, void>("deleteMcpTool", Effe
const chunkedUploadThreshold = 1_000_000; const chunkedUploadThreshold = 1_000_000;
export interface UploadDocumentInput { export class UploadDocumentInput extends S.Class<UploadDocumentInput>("UploadDocumentInput")({
readonly base64: string; base64: S.String,
readonly mimeType: string; mimeType: S.String,
readonly title: string; title: S.String,
readonly comments: string; comments: S.String,
readonly tags: string[]; tags: S.Array(S.String).pipe(S.mutable),
} }, { description: "Workbench document upload command payload." }) {}
const uploadDocumentEffect = Effect.fn("trustgraph.workbench.uploadDocument.effect")(function*( const uploadDocumentEffect = Effect.fn("trustgraph.workbench.uploadDocument.effect")(function*(
input: UploadDocumentInput, input: UploadDocumentInput,

View file

@ -1,5 +1,6 @@
import type { Triple, Term } from "@trustgraph/client"; import type { Triple, Term } from "@trustgraph/client";
import { Match } from "effect"; import { Match } from "effect";
import * as S from "effect/Schema";
import type { ForceGraphProps, NodeObject, LinkObject } from "react-force-graph-2d"; import type { ForceGraphProps, NodeObject, LinkObject } from "react-force-graph-2d";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -27,10 +28,23 @@ export interface GraphLink extends LinkObject {
label: string; label: string;
} }
export interface GraphData { const GraphNodeValue: S.Codec<GraphNode, GraphNode> = S.Struct({
nodes: GraphNode[]; id: S.String,
links: GraphLink[]; label: S.String,
} color: S.optionalKey(S.String),
degree: S.Finite,
});
const GraphLinkValue: S.Codec<GraphLink, GraphLink> = S.Struct({
source: S.String,
target: S.String,
label: S.String,
});
export class GraphData extends S.Class<GraphData>("GraphData")({
nodes: S.Array(GraphNodeValue).pipe(S.mutable),
links: S.Array(GraphLinkValue).pipe(S.mutable),
}, { description: "Renderable graph nodes and links derived from triples." }) {}
export const DEFAULT_GRAPH_NODE_COLOR = "#82b582"; export const DEFAULT_GRAPH_NODE_COLOR = "#82b582";

View file

@ -11,14 +11,14 @@ import {
settingsAtom, settingsAtom,
} from "@/atoms/workbench"; } from "@/atoms/workbench";
import type { BaseApi } from "@trustgraph/client"; import type { BaseApi } from "@trustgraph/client";
import type { MockWorkbenchFixture } from "@/qa/mock-api"; import { MockWorkbenchFixture, makeMockBaseApi, qaSettingsFromFixture, } from "@/qa/mock-api";
import { makeMockBaseApi, qaSettingsFromFixture, } from "@/qa/mock-api"; import { Schema as S } from "effect";
export interface WorkbenchQaWindowConfig { export class WorkbenchQaWindowConfig extends S.Class<WorkbenchQaWindowConfig>("WorkbenchQaWindowConfig")({
readonly enabled?: boolean; enabled: S.optionalKey(S.Boolean),
readonly fixture?: MockWorkbenchFixture; fixture: S.optionalKey(MockWorkbenchFixture),
readonly flowId?: string; flowId: S.optionalKey(S.String),
} }, { description: "Browser-provided workbench QA boot configuration." }) {}
declare global { declare global {
interface Window { interface Window {

View file

@ -4,42 +4,97 @@ import { Clock, Effect, Match, Option, Schema as S } from "effect";
type ConfigValues = Record<string, Record<string, unknown>>; type ConfigValues = Record<string, Record<string, unknown>>;
export interface MockWorkbenchFixture { const UnknownRecord = S.Record(S.String, S.Unknown);
readonly settings?: { const ConfigValuesRecord = S.Record(S.String, UnknownRecord);
readonly user?: string;
readonly apiKey?: string; const ClientTerm: S.Codec<Triple["s"], Triple["s"]> = S.suspend(() =>
readonly gatewayUrl?: string; S.Union([
readonly collection?: string; S.Struct({ t: S.Literal("i"), i: S.String }),
readonly featureSwitches?: Record<string, boolean>; S.Struct({ t: S.Literal("b"), d: S.String }),
}; S.Struct({
readonly flows?: { t: S.Literal("l"),
readonly activeIds?: string[]; v: S.String,
readonly definitions?: Record<string, Record<string, unknown>>; dt: S.optionalKey(S.String),
readonly blueprints?: Record<string, Record<string, unknown>>; ln: S.optionalKey(S.String),
}; }),
readonly config?: { S.Struct({
readonly prompt?: Record<string, unknown>; t: S.Literal("t"),
readonly valuesByType?: ConfigValues; tr: S.optionalKey(ClientTriple),
}; }),
readonly library?: { ])
readonly documents?: DocumentMetadata[]; );
readonly processing?: ProcessingMetadata[];
readonly metadataById?: Record<string, DocumentMetadata>; const ClientTriple: S.Codec<Triple, Triple> = S.suspend(() =>
}; S.Struct({
readonly knowledge?: { s: ClientTerm,
readonly kgCores?: string[]; p: ClientTerm,
readonly deCores?: string[]; o: ClientTerm,
readonly loadedKgCores?: string[]; g: S.optionalKey(S.String),
}; })
readonly collections?: Array<Record<string, unknown>>; );
readonly graph?: {
readonly triplesByFlowCollection?: Record<string, Triple[]>; const DocumentMetadataValue: S.Codec<DocumentMetadata, DocumentMetadata> = S.Struct({
readonly explainTriplesByGraph?: Record<string, Triple[]>; id: S.optionalKey(S.String),
}; time: S.optionalKey(S.Finite),
readonly chat?: { kind: S.optionalKey(S.String),
readonly delayFrames?: number; title: S.optionalKey(S.String),
}; comments: S.optionalKey(S.String),
} metadata: S.optionalKey(S.Array(ClientTriple).pipe(S.mutable)),
user: S.optionalKey(S.String),
tags: S.optionalKey(S.Array(S.String).pipe(S.mutable)),
parentId: S.optionalKey(S.String),
documentType: S.optionalKey(S.String),
"parent-id": S.optionalKey(S.String),
"document-type": S.optionalKey(S.String),
});
const ProcessingMetadataValue: S.Codec<ProcessingMetadata, ProcessingMetadata> = S.Struct({
id: S.optionalKey(S.String),
"document-id": S.optionalKey(S.String),
documentId: S.optionalKey(S.String),
time: S.optionalKey(S.Finite),
flow: S.optionalKey(S.String),
user: S.optionalKey(S.String),
collection: S.optionalKey(S.String),
tags: S.optionalKey(S.Array(S.String).pipe(S.mutable)),
});
export class MockWorkbenchFixture extends S.Class<MockWorkbenchFixture>("MockWorkbenchFixture")({
settings: S.optionalKey(S.Struct({
user: S.optionalKey(S.String),
apiKey: S.optionalKey(S.String),
gatewayUrl: S.optionalKey(S.String),
collection: S.optionalKey(S.String),
featureSwitches: S.optionalKey(S.Record(S.String, S.Boolean)),
})),
flows: S.optionalKey(S.Struct({
activeIds: S.optionalKey(S.Array(S.String).pipe(S.mutable)),
definitions: S.optionalKey(S.Record(S.String, UnknownRecord)),
blueprints: S.optionalKey(S.Record(S.String, UnknownRecord)),
})),
config: S.optionalKey(S.Struct({
prompt: S.optionalKey(UnknownRecord),
valuesByType: S.optionalKey(ConfigValuesRecord),
})),
library: S.optionalKey(S.Struct({
documents: S.optionalKey(S.Array(DocumentMetadataValue).pipe(S.mutable)),
processing: S.optionalKey(S.Array(ProcessingMetadataValue).pipe(S.mutable)),
metadataById: S.optionalKey(S.Record(S.String, DocumentMetadataValue)),
})),
knowledge: S.optionalKey(S.Struct({
kgCores: S.optionalKey(S.Array(S.String).pipe(S.mutable)),
deCores: S.optionalKey(S.Array(S.String).pipe(S.mutable)),
loadedKgCores: S.optionalKey(S.Array(S.String).pipe(S.mutable)),
})),
collections: S.optionalKey(S.Array(UnknownRecord).pipe(S.mutable)),
graph: S.optionalKey(S.Struct({
triplesByFlowCollection: S.optionalKey(S.Record(S.String, S.Array(ClientTriple).pipe(S.mutable))),
explainTriplesByGraph: S.optionalKey(S.Record(S.String, S.Array(ClientTriple).pipe(S.mutable))),
})),
chat: S.optionalKey(S.Struct({
delayFrames: S.optionalKey(S.Finite),
})),
}, { description: "Seed fixture for deterministic workbench QA runs." }) {}
interface UploadSession { interface UploadSession {
readonly metadata: DocumentMetadata; readonly metadata: DocumentMetadata;

Binary file not shown.

File diff suppressed because one or more lines are too long