Graph control
In shortReference for graph-control methods on the TypeScript client: ping, graph, graphTools, observability, graphStateSchema, stopGraph, and fixGraph.
- 8 min read
- 10 sections
- Updated
- v0.10.0
- Markdown
Seven methods on TenxGraphClient inspect the graph and control its execution. They check server health, retrieve the graph’s topology, list available tools, fetch observability data for a run, return the state schema, and stop or repair a thread. All are imported from 10xgraph-client and called on a client instance.
| Method | Endpoint | Purpose |
|---|---|---|
ping() |
GET /ping |
Verify server connectivity |
graph() |
GET /v1/graph |
Get graph topology and capabilities |
graphTools() |
GET /v1/graph/tools |
List available tools |
observability(threadId, runId?) |
GET /v1/observability/{thread_id} |
Retrieve traces and metrics for a run |
graphStateSchema() |
GET /v1/graph:StateSchema |
Get the state schema as JSON Schema |
stopGraph(threadId, config?) |
POST /v1/graph/stop |
Request graph to halt execution |
fixGraph(threadId, config?) |
POST /v1/graph/fix |
Repair thread with orphaned tool calls |
Every response follows the same envelope: the payload under data, and { request_id, timestamp, message } under metadata.
ping()
ping(): Promise<PingResponse>Liveness check. Use it to fail fast at startup instead of discovering a bad baseUrl on the first invoke().
// Response type for ping()
interface PingResponse {
data: string; // the server's pong payload
metadata: ResponseMetadata;
}import { TenxGraphClient } from '10xgraph-client';
const baseUrl = 'http://127.0.0.1:8000';
const client = new TenxGraphClient({ baseUrl });
// Fail fast at startup if the server is unreachable.
try {
await client.ping();
} catch {
throw new Error(`10xGraph API not reachable at ${baseUrl}`);
}ping() does not require the graph to be healthy. It only proves the HTTP server is answering. Use graph() when you need to know the graph itself loaded.
graph()
graph(): Promise<GraphResponse>Returns the graph’s topology and the server capabilities attached to it.
interface GraphResponse {
data: {
info: {
node_count: number;
edge_count: number;
checkpointer: boolean;
checkpointer_type: string;
publisher: boolean;
store: boolean;
interrupt_before: string[];
interrupt_after: string[];
context_type: string;
id_generator: string;
id_type: string;
state_type: string;
state_fields: string[];
};
nodes: Array<{ id: string; name: string }>;
edges: Array<{ id: string; source: string; target: string }>;
};
metadata: ResponseMetadata;
}info is the honest answer to “what can this deployment do”:
| Field | Why it matters |
|---|---|
checkpointer / checkpointer_type |
false means config.thread_id will not persist anything between calls. |
store |
false means the memory methods have no backend. |
interrupt_before / interrupt_after |
Node names where the graph pauses for human input. See execution_meta.status below. |
id_type / id_generator |
How thread and message ids are minted on this server. |
state_fields |
The field names of the graph’s state type; graphStateSchema() returns their full schema. |
The server also returns info.is_realtime, which is true when the graph is rooted at a live agent and therefore accepts realtime() over WS /v1/graph/live rather than invoke/stream/wsStream. It is not on the GraphInfo TypeScript interface yet, so read it with a cast:
const { info } = (await client.graph()).data;
const liveCapable = Boolean((info as { is_realtime?: boolean }).is_realtime);graphTools()
graphTools(): Promise<GraphToolsResponse>Lists every tool exposed by the graph’s tool nodes, grouped by node, each tagged with where it came from.
type ToolSource = 'local' | 'mcp' | 'remote';
interface GraphTool {
name: string;
description: string;
source: ToolSource;
parameters: Record<string, any>; // JSON Schema, OpenAI function-calling shape
}
interface GraphToolNode {
node_name: string;
tool_count: number;
tools: GraphTool[];
}
interface GraphToolsResponse {
data: {
node_count: number;
tool_count: number;
nodes: GraphToolNode[];
};
metadata: ResponseMetadata;
}source |
Origin |
|---|---|
local |
A Python function registered on the tool node. |
mcp |
Discovered from an MCP server attached to the node. |
remote |
Declared in server 10xgraph.json, executed by a matching client handler. |
A graph with no tool nodes returns nodes: [] and tool_count: 0. That is a valid graph, not an error.
See graph-utilities for worked examples, including verifying that your remote tools registered.
observability(threadId, runId?)
observability(threadId: string, runId?: string): Promise<ObservabilityResponse>Returns the reconstructed trace for one run of a thread: a span tree, an event list, and aggregated token usage. Omitting runId returns the most recent run.
interface ObservabilityResponse {
data: {
thread_id: string;
run_count: number;
run_ids: string[]; // oldest first
run: ObsRun | null; // null when nothing has been recorded
};
metadata: ResponseMetadata;
}
interface ObsRun {
run_id: string;
thread_id: string;
status: string;
started_at: number | null; // UNIX seconds
finished_at: number | null;
duration_ms: number;
spans: ObsSpan[];
events: ObsEvent[];
usage: ObsTokenUsage;
llm_calls: number;
tool_calls: number;
iterations: number;
}
interface ObsSpan {
id: string;
name: string;
kind: 'root' | 'node' | 'llm' | 'tool';
parent: string | null; // span id; null for the root
start_ms: number; // offset from the run start
duration_ms: number;
model?: string | null; // set on 'llm' spans
input_tokens?: number | null;
output_tokens?: number | null;
}
interface ObsEvent {
id: string;
type: string;
node: string;
offset_ms: number; // offset from the run start
summary: string;
}
interface ObsTokenUsage {
prompt_tokens: number;
completion_tokens: number;
reasoning_tokens: number;
total_tokens: number;
}threadId is typed string here, unlike most thread methods which accept string | number.
Spans nest root → node → llm | tool via parent. start_ms and offset_ms are relative to the start of the run, so add started_at * 1000 to place them on a wall clock.
A server with telemetry recording disabled returns run_count: 0 and run: null instead of raising.
graphStateSchema()
graphStateSchema(): Promise<StateSchemaResponse>Returns the JSON Schema of the graph’s state type. Use it to build dynamic forms, validate an initial_state before invoking, or generate types.
interface StateSchemaResponse {
data: {
title?: string;
description?: string;
type?: string;
properties: Record<string, FieldSchema>;
required?: string[];
$defs?: Record<string, any>;
[key: string]: any;
};
metadata: ResponseMetadata;
}
interface FieldSchema {
type?: string | string[];
description?: string;
default?: any;
items?: any;
properties?: Record<string, FieldSchema>;
required?: string[];
enum?: any[];
$ref?: string;
$defs?: Record<string, any>;
anyOf?: any[];
allOf?: any[];
oneOf?: any[];
[key: string]: any;
}This is the schema of the state type the graph is compiled with, which may be a subclass of AgentState carrying your own fields.
stopGraph(threadId, config?)
stopGraph(threadId: string, config?: Record<string, any>): Promise<StopGraphResponse>Sets a stop flag on the thread. The graph checks it between nodes and halts before starting the next one.
// Declared TypeScript type
interface StopGraphResponse {
data: {
success: boolean;
message: string;
thread_id: string;
stopped_at?: string; // ISO timestamp
};
metadata: ResponseMetadata;
}The server currently fills data with the result of the core stop call instead: { ok: boolean, running?: boolean, reason?: string }. reason is "not-running" when the thread exists but is idle, "no-state" when the thread has no state, and "no-checkpointer" when the graph has no checkpointer (stop needs one). Do not rely on success, message or stopped_at; check data.ok and data.running with a cast if you need the outcome.
It is a request, not a guarantee: the currently executing node runs to completion, so one more message may still arrive. Stopping a thread that is not running is not an error.
Breaking out of a for await loop over stream() closes your end of the connection but does not stop the graph. Call stopGraph() as well.
fixGraph(threadId, config?)
fixGraph(threadId: string, config?: Record<string, any>): Promise<FixGraphResponse>Repairs a thread by removing every message in its saved state that has a tool call with empty content, which can be left behind when a run is cut off mid-tool-call. Those leftovers can leave the thread stuck.
interface FixGraphResponse {
data: {
success: boolean;
message: string;
removed_count: number;
state?: Record<string, any>; // state after the repair, when available
};
metadata: ResponseMetadata;
}removed_count: 0 means no such messages were found. If the thread has no saved state, the server returns success: false with removed_count: 0.
AgentState
AgentState is the state container the graph carries between nodes. It is returned by invoke() (as result.state), by stream() on state chunks, and by threadState().
class AgentState {
context: Message[];
context_summary: string | null;
execution_meta: ExecutionMeta;
}The exported ExecutionMeta TypeScript interface declares interrupt, is_running, is_interrupted and is_stopped_requested, but the server does not send those fields. What arrives on the wire is the Python ExecutionState, so read these fields instead (cast execution_meta to any or to your own interface):
| Field | Description |
|---|---|
context |
The conversation as the graph sees it: every Message currently in the working window. |
context_summary |
A rolling summary of messages trimmed out of context, or null when nothing has been summarized. |
execution_meta.current_node |
Node the graph is at. |
execution_meta.step |
Steps executed in this run. Compare against recursion_limit. |
execution_meta.status |
One of running, interrupted_before, interrupted_after, completed or error. A value starting with interrupted means the graph is paused for human input. |
execution_meta.interrupted_node |
Node where the graph paused, or null. |
execution_meta.interrupt_reason |
Reason recorded for the pause, or null. |
execution_meta.interrupt_data |
Extra data attached to the pause, or null. |
execution_meta.stop_current_execution |
none, stop_requested or stopped, set by stopGraph(). |
State is only returned when you request response_granularity: 'full'.
The constructor takes a partial object and Object.assigns it, so a graph compiled with a custom state type carries its extra fields through at runtime even though they are not on the TypeScript class. Read graphStateSchema() for the authoritative field list of a given deployment.
Detecting a human-in-the-loop pause
An interrupt is not an error. The run ends normally and the state comes back paused:
import { TenxGraphClient, Message } from '10xgraph-client';
const client = new TenxGraphClient({ baseUrl: 'http://127.0.0.1:8000' });
// Placeholder: replace with your own approval UI.
async function askUser(reason: string, data?: Record<string, any>): Promise<boolean> {
console.log('Approval needed:', reason, data);
return true;
}
const userMessage = Message.text_message('Delete the old reports', 'user');
const result = await client.invoke([userMessage], {
config: { thread_id: 'approval-1' },
response_granularity: 'full',
});
// The wire shape differs from the exported ExecutionMeta type, so cast it.
const meta = result.state?.execution_meta as any;
if (meta?.status?.startsWith('interrupted')) {
// Show the approval UI. `interrupt_data` carries whatever the node attached.
const approved = await askUser(meta.interrupt_reason ?? meta.interrupted_node, meta.interrupt_data ?? undefined);
// Resume by invoking the same thread again.
await client.invoke([Message.text_message(approved ? 'approved' : 'rejected', 'user')], {
config: { thread_id: 'approval-1' },
});
}graph().data.info.interrupt_before and interrupt_after tell you up front which nodes can pause, so a UI can be built for them before the first pause happens.
What you learned
ping()andgraph()give you early feedback on server health and what the deployment supports.graphTools()shows what the model can actually call, taggedlocal,mcp, orremote.observability()reconstructs a run as spans and events with token usage, andrun_idslists all runs.stopGraph()halts execution cooperatively;fixGraph()removes messages with empty tool calls from a thread.execution_meta.statusstarting withinterruptedtells you the graph is paused waiting for human input.
Next step
See graph-utilities for task-oriented recipes, or remote-tools for registering client-side tools that the server can invoke.