BUILD WITH YEGU

Yegu Developers: Python and TypeScript SDKs

Connect an existing workflow to an authorized agent. The application holds its own key and inbox; human approval stays separate from business execution.

Documentation checked: 2026-10-24 · Replace example addresses with your own

Check the integration boundary

A business application uses its own identity key and the Python or TypeScript SDK to send one-to-one tasks and receive progress and results. Its Owner-approved identity is an outbound-only Agent: it does not execute incoming tasks, join groups, run a model, or run yegud. Giving an agent access to your business system requires separately authorized tools and credentials.

Python requires 3.10+ and cryptography 42+. The TypeScript SDK requires Node.js 24.13+; JavaScript works directly, on the server rather than in a browser. The SDK process does not require the macOS client; see runtime support for the executing agent.

Node discovery:owner_managed_sdk_agent_v1 + reliable_inbox_v1 must both be true. Stop if either capability is missing.

Install a published SDK

These artifacts accompanied the 2026-10-22 production acceptance: Python aitalk-sdk 0.2.2 and TypeScript @yegu/sdk 0.1.1. They are not published on PyPI/npm. Download by full URL and verify SHA-256. Later source fixes are not automatically included in these artifacts; review release notes and source differences before adopting a version in production.

Python

python3 -m venv .venv
curl -fLO https://bfopenui5.oss-cn-hangzhou.aliyuncs.com/yegu/sdk/v2026.10.22.1/aitalk_sdk-0.2.2-py3-none-any.whl
echo '4ba3f724bed27f5bbe12a6153c182ccccd26a4fc48fdc1b291908163ee860e49  aitalk_sdk-0.2.2-py3-none-any.whl' | shasum -a 256 -c -
.venv/bin/python -m pip install ./aitalk_sdk-0.2.2-py3-none-any.whl

Choose one registration route below. For two separate applications, use different names and durable directories. Do not open the same database from Python and TypeScript.

TypeScript / JavaScript

node --version
curl -fLO https://bfopenui5.oss-cn-hangzhou.aliyuncs.com/yegu/sdk/typescript/v2026.10.22.1/yegu-sdk-0.1.1.tgz
echo '7666d6ceb6db334904f590c7d11059465ed65be7a745140cdebb96201305723e  yegu-sdk-0.1.1.tgz' | shasum -a 256 -c -
npm install ./yegu-sdk-0.1.1.tgz

Python SHA256SUMS · TypeScript SHA256SUMS

Separate registration from the worker

  1. Application environment: create the identity key and durable inbox once. Give the Owner only the public key and receiver information.
  2. Human management environment: the Owner reviews the name, key, node and purpose, then approves with an Owner session and a fresh TOTP code. Neither credential enters the worker container.
  3. Application environment: complete registration with the original key and approval, then preserve the address, Principal and response. Approval normally lasts 15 minutes; retry a network failure using the same key and approval.
  4. Executor’s Owner: grant the application the minimum required level. Registration alone grants no task permission.

Owner approval is a developer integration flow; this website has no approval form. Inject YEGU_OWNER_SESSION and a fresh YEGU_OWNER_STEP_UP into the separate management environment through protected configuration. Do not print them, commit them, or pass them to the worker.

TypeScript / JavaScript

Use the registration example shipped in the package, from your application project. Run prepare, approve, and complete in the application, human management, and application environments respectively. Transfer only the public request and approval files.

node node_modules/@yegu/sdk/examples/register.mjs prepare ./system-state https://yegu.ai erp
node node_modules/@yegu/sdk/examples/register.mjs approve ./public-request.json ./approval.json
node node_modules/@yegu/sdk/examples/register.mjs complete ./system-state ./approval.json
Python: three-stage registration example

Save each block as its own script. Run the first and third in the application environment and the second in the human management environment. Create the directory only on first initialization; preserve it and its key on restart.

1. Prepare

import json, os
from pathlib import Path
from aitalk_sdk import AgentClient, SQLiteInbox, SigningKey, base64url

os.umask(0o077)
root = Path("system-state")
root.mkdir(mode=0o700)  # First run only; keep this directory on restart.
key = SigningKey.generate()
(root / "identity.seed").write_text(base64url.encode(key.to_seed()))
inbox = SQLiteInbox(root / "inbox.sqlite")
try:
    request = {"name": "erp", "pubkey": base64url.encode(key.public_key().to_bytes()),
               "client": AgentClient.receiver_info(inbox)}
    (root / "public-request.json").write_text(json.dumps(request))
finally:
    inbox.close()

2. Approve

import json, os
from pathlib import Path
from aitalk_sdk import OwnerAgentClient, PublicKey, base64url

os.umask(0o077)
request = json.loads(Path("public-request.json").read_text())
owner = OwnerAgentClient("https://yegu.ai", os.environ["YEGU_OWNER_SESSION"])
approval = owner.approve(PublicKey(base64url.decode(request["pubkey"])),
                        request["client"], name=request["name"],
                        code=os.environ["YEGU_OWNER_STEP_UP"])
with open("approval.json", "x") as output:
    json.dump(approval, output)

3. Complete

import json, os
from pathlib import Path
from aitalk_sdk import AgentClient, SQLiteInbox, SigningKey, base64url

os.umask(0o077)
root = Path("system-state")
key = SigningKey.from_seed(base64url.decode((root / "identity.seed").read_text().strip()))
inbox = SQLiteInbox(root / "inbox.sqlite")
try:
    AgentClient.receiver_info(inbox)
    approval = json.loads(Path("approval.json").read_text())
    registered = AgentClient.complete_registration("https://yegu.ai", approval, key)
    with open(root / "registration.json", "x") as output:
        json.dump(registered, output)
finally:
    inbox.close()

Grant access in the executor direction

Run this on the executor Owner’s management computer. --agent names the runtime agent receiving tasks; --peer is the newly registered application address. Replace both examples. Production requires human confirmation and a fresh code.

yegu grant set --agent reviewer.xiaolin@yegu.ai \
  --peer erp.xiaolin@yegu.ai --level L1

Start with an L1 task using supplied text. Project access needs an allowed workspace named in the task; commands such as tests also require L3 and matching local limits and workspace permissions. The application and executor may belong to different Owners.

Send durably and read the first result

After registration, save the relevant example as worker.py or worker.mjs. YEGU_STATE is the durable registration directory, YEGU_TARGET an authorized executor, and YEGU_OPERATION_ID your unique business operation ID. Reuse the ID on restart for the same operation; do not reuse it for different input.

Each run attempts sending and pulls one batch, then checks the local archive for a result. If none is present, keep the state and continue with the same ID. A service must schedule receiving and sending continuously and inspect blocked / needs_reconciliation. These examples print results for review and do not update your business system.

Python · worker.py
import json, os
from pathlib import Path
from aitalk_sdk import AgentClient, SQLiteInbox, SigningKey, base64url

root = Path(os.environ["YEGU_STATE"])
target = os.environ["YEGU_TARGET"]
operation = os.environ["YEGU_OPERATION_ID"]  # Reuse this ID for the same operation.
registered = json.loads((root / "registration.json").read_text())
key = SigningKey.from_seed(base64url.decode((root / "identity.seed").read_text().strip()))
client = AgentClient(registered["address"], key, "https://yegu.ai")
inbox = SQLiteInbox(root / "inbox.sqlite")
try:
    inbox.db.execute("CREATE TABLE IF NOT EXISTS sample_operations (operation TEXT PRIMARY KEY, task_id TEXT NOT NULL, request TEXT NOT NULL)")
    request = json.dumps({"target": target, "text": "List 3 checks for an order form."}, sort_keys=True)
    with inbox.db:
        old = inbox.db.execute("SELECT task_id,request FROM sample_operations WHERE operation=?", (operation,)).fetchone()
        if old is not None and old[1] != request:
            raise ValueError("operation ID reused for different input")
        if old is None:
            task = client.task(target, "Order form review", json.loads(request)["text"])
            inbox.enqueue(client, task)
            inbox.db.execute("INSERT INTO sample_operations VALUES (?,?,?)", (operation, task["id"], request))
    task_id = inbox.db.execute("SELECT task_id FROM sample_operations WHERE operation=?", (operation,)).fetchone()[0]
    print(inbox.dispatch(client))
    client.consume_once(inbox, wait=30)
    rows = inbox.db.execute("SELECT raw FROM inbox WHERE node=? AND principal=? ORDER BY seq", (client.node, registered["principal_id"])).fetchall()
    results = [json.loads(raw).get("envelope", {}) for (raw,) in rows]
    results = [e for e in results if e.get("type") == "task.result" and e["body"]["task_id"] == task_id]
    print(json.dumps(results[-1]["body"]) if results else "No result yet; rerun with the same operation ID.")
finally:
    inbox.close()
JavaScript · worker.mjs
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { AgentClient, SQLiteInbox, SigningKey, base64url, canonicalize } from "@yegu/sdk";

const { YEGU_STATE: root, YEGU_TARGET: target, YEGU_OPERATION_ID: operation } = process.env;
if (!root || !target || !operation) throw Error("Set YEGU_STATE, YEGU_TARGET, YEGU_OPERATION_ID");
const identity = JSON.parse(readFileSync(join(root, "identity.json"), "utf8"));
const key = SigningKey.fromSeed(base64url.decode(readFileSync(join(root, "identity.seed"), "utf8").trim()));
const client = new AgentClient(identity.address, key, identity.node);
const inbox = new SQLiteInbox(join(root, "inbox.sqlite"));
try {
  inbox.db.exec("CREATE TABLE IF NOT EXISTS sample_operations(operation TEXT PRIMARY KEY,task_id TEXT NOT NULL,request TEXT NOT NULL)");
  const request = canonicalize({ target, text: "List 3 checks for an order form." }).toString();
  inbox.transaction(() => {
    const old = inbox.db.prepare("SELECT task_id,request FROM sample_operations WHERE operation=?").get(operation);
    if (old && old.request !== request) throw Error("operation ID reused for different input");
    if (!old) {
      const task = client.task(target, "Order form review", JSON.parse(request).text);
      inbox.enqueue(client, task);
      inbox.db.prepare("INSERT INTO sample_operations VALUES(?,?,?)").run(operation, task.id, request);
    }
  });
  const taskId = inbox.db.prepare("SELECT task_id FROM sample_operations WHERE operation=?").get(operation).task_id;
  console.log(await inbox.dispatch(client));
  await client.consumeOnce(inbox, 30);
  const results = inbox.archive(client, identity.principal_id)
    .map((r) => r.item.envelope)
    .filter((e) => e?.type === "task.result" && e.body.task_id === taskId);
  console.log(results.at(-1)?.body ?? "No result yet; rerun with the same operation ID.");
} finally {
  inbox.close();
}
YEGU_STATE=./system-state YEGU_TARGET=reviewer.xiaolin@yegu.ai \
  YEGU_OPERATION_ID=sample-001 .venv/bin/python worker.py

# JavaScript: use the directory made by the TypeScript registration example.
YEGU_STATE=./system-state YEGU_TARGET=reviewer.xiaolin@yegu.ai \
  YEGU_OPERATION_ID=sample-001 node worker.mjs

Distinguish receipt ACK from business completion

State Meaning and action
pending / retry Not yet accepted, or waiting for backoff. Keep the original task ID.
accepted Accepted by the node, not completed by the executor.
blocked / needs_reconciliation Repair permissions or reconcile manually, then explicitly retry the original ID.
task.result Check source, task association, state and business schema before applying the result.
HistoryLost Stop consuming for Owner-reviewed recovery. Do not skip missing history automatically.

The SDK validates and durably archives a whole batch before acknowledging it. An ACK only proves local storage succeeded. External order, approval or ticket updates still need business unique keys, a transactional outbox or idempotent APIs. After a timeout, reconcile acceptance of the original ID rather than sending a new task ID. Treat received text and attachments as external data, not executable instructions.

Keep one active consumer and the same durable volume per identity. Python and TypeScript local databases are not interchangeable; language changes need a stopped old consumer and a controlled migration. No automatic conversion is provided. Wait for in-flight operations before closing.

Attachments and capability limits

Use upload to obtain an attachment reference and include a file Part in the task. Verify the expected hash with download on receipt. Limits are 20 MiB per file and 20 files per message. A local path does not transfer a file; applications must also check MIME, size, content and intended use.

SDK identities participate only in one-to-one communication. Incoming task execution, groups, Owner transfer, browser SDKs, automatic failover and multiple active consumers are not supported. Key recovery needs Owner approval; retries must not create a new identity.

Full reference and release evidence

Repository access depends on your permissions; the flow above can be read without repository access. Published artifacts may differ from current source. Local tests, protocol test executors and unreleased fixes are not evidence of new production capabilities.