@thesis-ai/sdk
v0.1.1
Published
Thesis SDK for building durable AI workflows on Temporal: agents, ontology, companion API, and Python sandbox tooling.
Maintainers
Readme
@thesis-ai/sdk
@thesis-ai/sdk is a TypeScript SDK for building durable, production-grade AI workflows on Temporal, aimed at backoffice and corporate automation. It packages the patterns proven across the Thesis fleet into one toolkit:
- Durable agent workflows on
@temporalio/ai-sdk— every model call is an activity (retried, replayable, never re-billed on replay), with direct Anthropic/OpenAI providers, cost ledgers with hard budgets that degrade gracefully, and an LLM-aware retry taxonomy. - Multi-agent primitives — sub-agents as child workflows, bounded fan-out with continue-as-new, handoffs, supervisors, and human-in-the-loop approval gates built on Workflow Updates.
- Companion API — a Fastify server that boots beside your worker: workflow catalog with JSON-Schema forms, run kickoff/status/events/usage, versioned config catalogs, uploads. Contracts are declared once in zod; the server routes, the OpenAPI spec, and the worker's typed HTTP client all derive from the same objects, so they cannot drift.
- Ontology — domain objects/relations/actions authored in zod, serialized byte-for-byte to the thesis-ontology IR, with a prepare/execute action protocol (tokens, idempotency, explicit approval) and a fixed four-tool agent surface.
- Declarative workflows — a JSON
WorkflowDef(the ontology IR) executed by a generic interpreter workflow against a registry of typed handlers: new workflow = new document, no deploy. Code-first workflows remain for complex control flow — declarative for the graph, code for the nodes. - Python sandbox toolkit — battle-tested
wbhelper/docxhelpermodules (Excel style-safe editing, Word tracked-changes) injected into a sandbox (local process or Azure Dynamic Sessions) behind an agentrunPythontool.
Quick start
pnpm add @thesis-ai/sdk zod ai @temporalio/workflow @temporalio/worker @temporalio/client @temporalio/activity @temporalio/common @temporalio/ai-sdk @ai-sdk/anthropic1. Define a workflow manifest (pure data — imported by worker and API):
// src/workflows/memo-triage.manifest.ts
import { z } from 'zod';
import { defineWorkflow, ui } from '@thesis-ai/sdk';
export const memoTriage = defineWorkflow({
name: 'memo_triage', // = Temporal workflow type = module export name
displayName: 'Memo triage',
category: 'investment',
description: 'Classifies an investment memo and routes it for review.',
input: z.object({
memo: z.string().meta(ui({ widget: 'textarea', label: 'Memo text' })),
}),
output: z.object({ decision: z.string() }),
steps: [
{ key: 'classify', displayName: 'Classify', kind: 'agent', defaultModel: 'claude-sonnet-5' },
{ key: 'route', displayName: 'Route', kind: 'deterministic', dependsOn: ['classify'] },
],
maxCostUsd: 2,
});2. Implement it (the envelope validates I/O, tracks cost, records terminal_exit on every path, and reports to the companion API):
// src/workflows/memo-triage.workflow.ts
import { z } from 'zod';
import { defineAgent } from '@thesis-ai/sdk';
import { implementWorkflow } from '@thesis-ai/sdk/workflow';
import { runAgent } from '@thesis-ai/sdk/workflow/agents';
import { memoTriage } from './memo-triage.manifest.js';
const classifier = defineAgent({
name: 'memo_classifier',
model: 'claude-sonnet-5',
system: 'Classify investment memos as approve/deny/escalate.',
output: z.object({ decision: z.enum(['approve', 'deny', 'escalate']) }),
});
export const memo_triage = implementWorkflow(memoTriage, async (ctx) => {
const { output } = await ctx.step('classify', () =>
runAgent(ctx, classifier, { prompt: ctx.input.memo }),
);
if (output.decision === 'escalate') {
const gate = await ctx.approval.request({
key: 'escalation',
summary: 'Memo needs human review',
payload: { memo: ctx.input.memo },
});
return { decision: gate.approved ? 'approve' : 'deny' };
}
return await ctx.step('route', async () => ({ decision: output.decision }));
});3. Run the worker and the companion API:
// src/worker.ts
import { createRegistry } from '@thesis-ai/sdk';
import { createThesisWorker, defaultModelCatalog } from '@thesis-ai/sdk/worker';
import { parseEnv } from '@thesis-ai/sdk';
import { memoTriage } from './workflows/memo-triage.manifest.js';
const env = parseEnv(process.env);
const worker = await createThesisWorker({
registry: createRegistry([memoTriage]),
workflowsPath: new URL('./workflows/memo-triage.workflow.ts', import.meta.url).pathname,
models: await defaultModelCatalog(env), // ANTHROPIC_API_KEY / OPENAI_API_KEY; LLM_BASE_URL for a proxy
});
await worker.run();// src/api.ts
import { createRegistry } from '@thesis-ai/sdk';
import { createCompanionApi } from '@thesis-ai/sdk/api';
import { memoTriage } from './workflows/memo-triage.manifest.js';
const api = await createCompanionApi({
databaseUrl: process.env.DATABASE_URL!,
registry: createRegistry([memoTriage]),
});
await api.start({ port: 8080 });
// POST /v1/workflows/memo_triage/runs → starts the workflow, returns the run
// GET /v1/runs/:id → live status/state/output
// GET /openapi.json, /docs → the whole contractLocal dev: temporal server start-dev, then run both processes — with no TEMPORAL_API_KEY set the SDK connects to localhost:7233 in plaintext. Register search attributes once per namespace with pnpm exec thesis-ai bootstrap.
Subpath map
| Import | Runtime | Contents |
|---|---|---|
| @thesis-ai/sdk | universal | defineWorkflow, defineAgent, defineHandler, registries, contracts, errors, retry/pricing tables, env schema |
| @thesis-ai/sdk/workflow | workflow sandbox | implementWorkflow, approvals, mapBounded, startAgentChild, thesisWorkflowRunner, API report proxies |
| @thesis-ai/sdk/workflow/agents | workflow sandbox (needs models on the worker) | runAgent, thesisAgentRunner, handoffs, supervisors |
| @thesis-ai/sdk/worker | Node | createThesisWorker, ModelCatalog, LLM error classifier |
| @thesis-ai/sdk/activities | Node | contract-derived CompanionApiClient, createSdkActivities |
| @thesis-ai/sdk/client | Node | startWorkflowRun, connectThesisClient, ensureSearchAttributes |
| @thesis-ai/sdk/api | Node (only place fastify/drizzle/postgres are allowed) | createCompanionApi |
| @thesis-ai/sdk/ir, /ontology | universal | IR schemas/validators/canonicalization, defineDomain, ontology agent tools |
| @thesis-ai/sdk/sandbox | Node | sandbox backends, harness, createSandboxTools |
| @thesis-ai/sdk/testing | Node | createTestEnv, mockModel/mockModelBy, findInHistory |
The worker surface is DB-free by construction: activities reach data only through the companion API's typed client, and a bundler tripwire test fails CI if an HTTP-server or database package ever leaks in.
Environment
TEMPORAL_HOST · TEMPORAL_NAMESPACE · TEMPORAL_TASK_QUEUE · TEMPORAL_API_KEY (unset ⇒ local plaintext) · THESIS_API_URL/THESIS_API_KEY (worker→API reporting) · ANTHROPIC_API_KEY / OPENAI_API_KEY · LLM_BASE_URL (optional proxy, e.g. LiteLLM) · MAX_RUN_COST_USD (fleet-wide budget ceiling) · DATABASE_URL (API only).
Testing
createTestEnv() boots a Temporal dev server with the SDK's search attributes registered. mockModel([...turns]) and mockModelBy(promptJson => turn) script models at the provider level — run them behind the real AiSdkPlugin for full-path tests, or pass modelOverride to runAgent for hermetic unit tests with exact cost assertions. findInHistory filters workflow histories for assertions like "the budget stop forced a tool-free final call". The repo's own suites (envelope, plugin, multi-agent, interpreter, full-loop, API) are working examples.
CLI
thesis-ai bootstrap # ensure search attributes: create missing, skip existing
thesis-ai bootstrap --check # CI gate: report the diff, exit 1 if anything is missing
thesis-ai bootstrap --extra deal_id=Keyword,score=Int --json
thesis-ai schemas --registry src/registry.ts # dump every manifest's JSON-Schema form DSL
thesis-ai replay-check --workflows src/workflows/index.ts # determinism gate over recent historiesbootstrap is idempotent and CI-safe: it lists the namespace's search attributes, creates only the missing ones, skips the rest, and exits non-zero on type conflicts (or, with --check, when anything is missing). Run it as a deploy step, or call ensureSearchAttributes(connection, namespace, { extra, dryRun }) from @thesis-ai/sdk/client for the same behavior programmatically. On Temporal Cloud, where custom attributes are managed via tcld/the Cloud UI, use --check to verify.
Source
Developed at LM-Software-Labs-LLC/thesis-sdk. Apache-2.0.
