agent-lattice 0.9.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,652 @@
1
+ # AgentLattice
2
+
3
+ AgentLattice is a TypeScript framework for building coordinated agent systems
4
+ with tools, skills, tracing, supervisor delegation, and mailbox-backed teams.
5
+
6
+ Install it from npm as `agent-lattice`. It works with Anthropic and
7
+ Anthropic-compatible providers such as DeepSeek, without installing the Claude
8
+ Code CLI runtime.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install agent-lattice zod
14
+ ```
15
+
16
+ ## Minimal Usage
17
+
18
+ The examples use DeepSeek's Anthropic-compatible endpoint.
19
+
20
+ ```ts
21
+ import { createAgent } from "agent-lattice";
22
+
23
+ const agent = createAgent({
24
+ apiKey: process.env.DEEPSEEK_API_KEY,
25
+ baseURL: "https://api.deepseek.com/anthropic",
26
+ model: "deepseek-v4-flash",
27
+ });
28
+
29
+ for await (const message of agent.query("Say hello")) {
30
+ console.log(message);
31
+ }
32
+ ```
33
+
34
+ Pass `{ stream: false }` to disable model streaming for a query:
35
+
36
+ ```ts
37
+ const result = await agent.prompt("Say hello", { stream: false });
38
+ ```
39
+
40
+ ## Multimodal Input
41
+
42
+ Pass Anthropic-compatible content blocks for image or document prompts:
43
+
44
+ ```ts
45
+ const result = await agent.prompt([
46
+ { type: "text", text: "Summarize this screenshot." },
47
+ {
48
+ type: "image",
49
+ source: {
50
+ type: "base64",
51
+ media_type: "image/png",
52
+ data: imageBase64,
53
+ },
54
+ },
55
+ ]);
56
+
57
+ console.log(result.result);
58
+ ```
59
+
60
+ ## JSONL Context Tracing
61
+
62
+ Pass a `ContextTracer` to observe an agent run without changing the agent loop.
63
+ The built-in JSONL tracer writes one structured event per line:
64
+
65
+ ```ts
66
+ import { createAgent, createJsonlContextTracer } from "agent-lattice";
67
+
68
+ const tracer = createJsonlContextTracer({
69
+ path: ".agent-runs/session.jsonl",
70
+ });
71
+
72
+ const agent = createAgent({
73
+ apiKey: process.env.DEEPSEEK_API_KEY,
74
+ baseURL: "https://api.deepseek.com/anthropic",
75
+ model: "deepseek-v4-flash",
76
+ tracer,
77
+ });
78
+
79
+ await agent.prompt("Remember that my name is Ada.");
80
+ ```
81
+
82
+ Each JSONL entry includes `session_id`, `run_id`, `seq`, `source`, `type`, and
83
+ `data`. Agent runs record transcript and context events such as `run_start`,
84
+ `user_message`, `model_request`, `assistant_message`, `tool_use`,
85
+ `tool_result`, and `result`. For team runners, pass the tracer per query to
86
+ propagate it into delegated agents:
87
+
88
+ ```ts
89
+ for await (const event of team.query("Ask engineering to investigate.", {
90
+ tracer,
91
+ })) {
92
+ console.log(event);
93
+ }
94
+ ```
95
+
96
+ ## LangSmith Context Tracing
97
+
98
+ Pass LangSmith's `RunTree` constructor to the SDK tracer. The SDK depends on
99
+ `langsmith` directly and uses its official `RunTree` / `RunTreeConfig` types for
100
+ this adapter.
101
+
102
+ Configure LangSmith with its standard environment variables:
103
+
104
+ ```bash
105
+ LANGSMITH_TRACING=true
106
+ LANGSMITH_ENDPOINT=https://api.smith.langchain.com
107
+ LANGSMITH_API_KEY=<your-langsmith-api-key>
108
+ LANGSMITH_PROJECT=<your-langsmith-project>
109
+ # Required only for org-scoped or multi-workspace API keys.
110
+ LANGSMITH_WORKSPACE_ID=<your-langsmith-workspace-id>
111
+ ```
112
+
113
+ ```ts
114
+ import { RunTree } from "langsmith/run_trees";
115
+ import {
116
+ createAgent,
117
+ createCompositeContextTracer,
118
+ createJsonlContextTracer,
119
+ createLangSmithContextTracer,
120
+ } from "agent-lattice";
121
+
122
+ const tracer = createCompositeContextTracer([
123
+ createJsonlContextTracer({ path: ".agent-runs/session.jsonl" }),
124
+ createLangSmithContextTracer({
125
+ RunTree,
126
+ projectName: process.env.LANGSMITH_PROJECT,
127
+ workspaceId: process.env.LANGSMITH_WORKSPACE_ID,
128
+ tags: ["local-debug"],
129
+ }),
130
+ ]);
131
+
132
+ const agent = createAgent({
133
+ apiKey: process.env.DEEPSEEK_API_KEY,
134
+ baseURL: "https://api.deepseek.com/anthropic",
135
+ model: "deepseek-v4-flash",
136
+ tracer,
137
+ });
138
+ ```
139
+
140
+ If you prefer explicit values over environment variables, pass them to the SDK
141
+ tracer. `workspaceId` is optional and only selects a LangSmith workspace; it is
142
+ not the tracing project name.
143
+
144
+ ```ts
145
+ import { RunTree } from "langsmith/run_trees";
146
+ import { createLangSmithContextTracer } from "agent-lattice";
147
+
148
+ const tracer = createLangSmithContextTracer({
149
+ RunTree,
150
+ apiKey: process.env.LANGSMITH_API_KEY,
151
+ apiUrl: process.env.LANGSMITH_ENDPOINT,
152
+ projectName: process.env.LANGSMITH_PROJECT,
153
+ // Optional: only when LangSmith requires an explicit workspace.
154
+ workspaceId: process.env.LANGSMITH_WORKSPACE_ID,
155
+ });
156
+ ```
157
+
158
+ LangSmith receives one root `chain` run per SDK query, child `llm` runs for
159
+ model turns, child `tool` runs for SDK tool calls, and run events for auxiliary
160
+ trace events.
161
+
162
+ Custom sinks can implement the same interface for SQLite, OpenTelemetry, object
163
+ storage, or host-specific observability. The functions below are application
164
+ code you provide, not SDK exports:
165
+
166
+ ```ts
167
+ const tracer = {
168
+ async onEvent(event) {
169
+ // TODO: Replace with your own storage/logging code.
170
+ },
171
+ };
172
+ ```
173
+
174
+ ## DeepSeek Anthropic-compatible API
175
+
176
+ DeepSeek exposes an Anthropic-compatible endpoint. Configure `baseURL` and use a
177
+ DeepSeek model name:
178
+
179
+ ```ts
180
+ const agent = createAgent({
181
+ apiKey: process.env.DEEPSEEK_API_KEY,
182
+ baseURL: "https://api.deepseek.com/anthropic",
183
+ model: "deepseek-v4-flash",
184
+ });
185
+ ```
186
+
187
+ ## Custom Tool
188
+
189
+ ```ts
190
+ import { createAgent, tool } from "agent-lattice";
191
+ import { z } from "zod/v4";
192
+
193
+ const agent = createAgent({
194
+ apiKey: process.env.DEEPSEEK_API_KEY,
195
+ baseURL: "https://api.deepseek.com/anthropic",
196
+ model: "deepseek-v4-flash",
197
+ tools: [
198
+ tool(
199
+ "calculator",
200
+ "Evaluate a simple arithmetic expression",
201
+ z.object({ expr: z.string() }),
202
+ async input => ({ content: String(Function(`return ${input.expr}`)()) }),
203
+ ),
204
+ ],
205
+ });
206
+
207
+ const result = await agent.prompt("What is 2+2?");
208
+ console.log(result.result);
209
+ ```
210
+
211
+ ## Permission Callback
212
+
213
+ ```ts
214
+ const agent = createAgent({
215
+ apiKey: process.env.DEEPSEEK_API_KEY,
216
+ baseURL: "https://api.deepseek.com/anthropic",
217
+ model: "deepseek-v4-flash",
218
+ tools: [dangerousTool],
219
+ permission: async request => {
220
+ if (request.toolName === "danger") {
221
+ return { behavior: "deny", message: "Blocked by policy" };
222
+ }
223
+ return { behavior: "allow" };
224
+ },
225
+ });
226
+ ```
227
+
228
+ Denied tools are returned to Claude as error `tool_result` blocks so the model
229
+ can explain or choose another path.
230
+
231
+ ## Skills
232
+
233
+ Skills are reusable instruction bundles. They are lighter than Claude Code
234
+ runtime plugins: the SDK reads skill instructions and injects matching skills
235
+ into the model request, but it does not depend on the Claude Code runtime.
236
+
237
+ ```ts
238
+ import { createAgent, loadSkill, skill } from "agent-lattice";
239
+
240
+ const codeReview = skill({
241
+ name: "code-review",
242
+ description: "Review code changes and pull requests",
243
+ instructions: "Always list bugs and risks before summaries.",
244
+ });
245
+
246
+ const pdf = await loadSkill("./skills/pdf");
247
+
248
+ const agent = createAgent({
249
+ apiKey: process.env.DEEPSEEK_API_KEY,
250
+ baseURL: "https://api.deepseek.com/anthropic",
251
+ model: "deepseek-v4-flash",
252
+ skills: [codeReview, pdf],
253
+ });
254
+ ```
255
+
256
+ `loadSkill(path)` expects a `SKILL.md` file:
257
+
258
+ ```md
259
+ ---
260
+ name: pdf
261
+ description: Read and inspect PDF documents
262
+ ---
263
+
264
+ Render pages before claiming layout is correct.
265
+ ```
266
+
267
+ ## MCP Tools
268
+
269
+ The SDK can expose MCP server tools as agent tools. The first version supports
270
+ stdio MCP servers, remote Streamable HTTP servers, OAuth providers, and a
271
+ generic `MCPClient` adapter.
272
+
273
+ ```ts
274
+ import {
275
+ connectMCPStdioServer,
276
+ createAgent,
277
+ } from "agent-lattice";
278
+
279
+ const mcp = await connectMCPStdioServer(
280
+ {
281
+ command: "node",
282
+ args: ["./mcp-server.js"],
283
+ },
284
+ {
285
+ namePrefix: "docs",
286
+ },
287
+ );
288
+
289
+ const agent = createAgent({
290
+ apiKey: process.env.DEEPSEEK_API_KEY,
291
+ baseURL: "https://api.deepseek.com/anthropic",
292
+ model: "deepseek-v4-flash",
293
+ tools: mcp.tools,
294
+ });
295
+
296
+ try {
297
+ const result = await agent.prompt("Search the docs for installation steps.");
298
+ console.log(result.result);
299
+ } finally {
300
+ await mcp.close();
301
+ }
302
+ ```
303
+
304
+ Use `createMCPTools(client)` if your host application already manages an MCP
305
+ client connection.
306
+
307
+ Connect a remote Streamable HTTP MCP server:
308
+
309
+ ```ts
310
+ import {
311
+ connectMCPStreamableHTTPServer,
312
+ createAgent,
313
+ } from "agent-lattice";
314
+
315
+ const mcp = await connectMCPStreamableHTTPServer("https://mcp.example.com/mcp", {
316
+ namePrefix: "remote",
317
+ requestInit: {
318
+ headers: {
319
+ "X-Workspace": "demo",
320
+ },
321
+ },
322
+ });
323
+
324
+ const agent = createAgent({
325
+ apiKey: process.env.DEEPSEEK_API_KEY,
326
+ baseURL: "https://api.deepseek.com/anthropic",
327
+ model: "deepseek-v4-flash",
328
+ tools: mcp.tools,
329
+ });
330
+ ```
331
+
332
+ Pass an official MCP `OAuthClientProvider` when the remote server requires OAuth:
333
+
334
+ ```ts
335
+ const mcp = await connectMCPStreamableHTTPServer("https://mcp.example.com/mcp", {
336
+ authProvider,
337
+ });
338
+ ```
339
+
340
+ ## AgentLike Composition
341
+
342
+ `Agent` and `Team` satisfy the same `AgentLike` shape:
343
+
344
+ ```ts
345
+ type AgentLike = {
346
+ query(prompt, options?): AsyncGenerator<SDKMessage | TeamRunnerMessage>;
347
+ prompt(prompt, options?): Promise<SDKResultMessage>;
348
+ };
349
+ ```
350
+
351
+ That means a team can be used anywhere a callable agent is expected. From the
352
+ outside, a team is an agent; inside, it can contain a whole organization.
353
+
354
+ ## Team Mailbox Collaboration
355
+
356
+ Use `createTeam()` when you want to talk to one `AgentLike` while it coordinates
357
+ with named members internally. The team automatically injects member
358
+ `agentTool()` tools and drives the mailbox runtime when you call `team.query()`
359
+ or `team.prompt()`.
360
+
361
+ ```ts
362
+ import {
363
+ createAgent,
364
+ createMemoryMailbox,
365
+ createTeam,
366
+ teamMember,
367
+ } from "agent-lattice";
368
+
369
+ const researcher = createAgent({
370
+ apiKey: process.env.DEEPSEEK_API_KEY,
371
+ baseURL: "https://api.deepseek.com/anthropic",
372
+ model: "deepseek-v4-flash",
373
+ systemPrompt: "You research agent SDK architecture and report concise findings.",
374
+ });
375
+
376
+ const team = createTeam({
377
+ name: "engineering",
378
+ lead: createAgent({
379
+ apiKey: process.env.DEEPSEEK_API_KEY,
380
+ baseURL: "https://api.deepseek.com/anthropic",
381
+ model: "deepseek-v4-flash",
382
+ systemPrompt: "You lead engineering work. Delegate research tasks to researcher.",
383
+ }),
384
+ members: [
385
+ teamMember({
386
+ name: "researcher",
387
+ role: "executor",
388
+ focus: "Research agent architecture",
389
+ agent: researcher,
390
+ }),
391
+ ],
392
+ mailbox: createMemoryMailbox(),
393
+ });
394
+
395
+ for await (const event of team.query("Ask the researcher to inspect the SDK design.")) {
396
+ console.log(event);
397
+ }
398
+ ```
399
+
400
+ `team.query()` streams both the lead agent's normal SDK messages and team
401
+ runtime events such as `team_message`, `team_agent`, and nested `agent_message`
402
+ events. `team.prompt()` consumes that stream and returns only the final result.
403
+
404
+ `createTeam()` injects member AgentLike tools into the lead. Those tools expose
405
+ an explicit action contract: `mode: "ask"` waits for the member result,
406
+ `mode: "handoff"` returns an acceptance receipt to the lead while the team
407
+ runtime continues the accepted mailbox work, and `mode: "observe"` reports
408
+ unsupported unless a host runtime provides observation support. In the default
409
+ `team.query()` and `team.prompt()` path, a handoff receipt is not the final
410
+ delivery: the runtime waits for the member's upstream reply, feeds it back to
411
+ the lead, and keeps going until the root lead returns the final result or the
412
+ run terminates.
413
+
414
+ Team member tools can also request explicit shared workspace write grants:
415
+
416
+ ```ts
417
+ for await (const event of team.query(
418
+ "Ask backend to implement the API in the shared repo.",
419
+ {
420
+ permissions: {
421
+ workspaceGrants: [{
422
+ root: "/work/shared/txt-notebook-app",
423
+ access: ["write"],
424
+ reason: "Project shared workspace",
425
+ }],
426
+ },
427
+ },
428
+ )) {
429
+ console.log(event);
430
+ }
431
+ ```
432
+
433
+ When the lead calls a member tool, it may include `workspaceGrants` scoped to
434
+ that member, for example `/work/shared/txt-notebook-app/backend`. The runtime
435
+ only accepts write grants that are covered by the caller's current permissions. The
436
+ accepted grants are written to mailbox metadata, included in the child agent's
437
+ task/system context, and enforced by the built-in write tools. Read-only tools
438
+ such as `Read`, `LS`, `Glob`, and `Grep` can inspect any path the host process
439
+ can read and do not require workspace grants. If a write tool is denied, the
440
+ model receives a structured `permission_denied` tool result with the requested
441
+ path, allowed roots, and a deterministic suggested next step.
442
+ Grant `access` values are operation categories, not tool names; `write` covers
443
+ `Write`, `Edit`, and obvious Bash writes.
444
+
445
+ Managers should choose one workspace strategy explicitly when delegating:
446
+ ask the member to write deliverables in its own private workspace and report
447
+ paths, or provide `workspaceGrants: [{ root, access: ["write"], reason }]` for
448
+ every shared or manager-owned root named as a write destination.
449
+
450
+ Advanced mailbox controls remain available through `team.send()`,
451
+ `team.drain()`, and `team.mailbox`. Member agents that can accept tools receive
452
+ `team_send`, `team_inbox`, `team_read`, `team_reply`, `team_followup`, and
453
+ `team_status` so they can process assigned mailbox work. The lead does not
454
+ receive raw mailbox tools by default; pass `exposeLeadMailboxTools: true` only
455
+ when the lead should manually operate the team mailbox.
456
+
457
+ For durable local storage, pass a SQLite-like database. `better-sqlite3` works
458
+ without the SDK taking a hard dependency on it:
459
+
460
+ ```ts
461
+ import Database from "better-sqlite3";
462
+ import {
463
+ createAgent,
464
+ createSQLiteMailbox,
465
+ createTeam,
466
+ } from "agent-lattice";
467
+
468
+ const mailbox = createSQLiteMailbox({
469
+ database: new Database("team-mailbox.db"),
470
+ });
471
+
472
+ const team = createTeam({
473
+ name: "engineering",
474
+ lead: createAgent({
475
+ apiKey: process.env.DEEPSEEK_API_KEY,
476
+ baseURL: "https://api.deepseek.com/anthropic",
477
+ model: "deepseek-v4-flash",
478
+ }),
479
+ members: [],
480
+ mailbox,
481
+ });
482
+ ```
483
+
484
+ Hosts can also provide their own `TeamMailbox` adapter for Redis, Cloudflare D1,
485
+ Durable Objects, or another queue/storage backend.
486
+
487
+ ### Nested teams
488
+
489
+ Because `teamMember().agent` accepts any `AgentLike`, a `Team` can be a member
490
+ of another `Team`:
491
+
492
+ ```ts
493
+ import {
494
+ createAgent,
495
+ createTeam,
496
+ teamMember,
497
+ } from "agent-lattice";
498
+
499
+ const createDeepSeekAgent = (systemPrompt: string) => createAgent({
500
+ apiKey: process.env.DEEPSEEK_API_KEY,
501
+ baseURL: "https://api.deepseek.com/anthropic",
502
+ model: "deepseek-v4-flash",
503
+ systemPrompt,
504
+ });
505
+
506
+ const ceoAgent = createDeepSeekAgent(
507
+ [
508
+ "You are the CEO agent.",
509
+ "Clarify product goals, decide which department owns the work, and ask for concise progress reports.",
510
+ "Do not implement engineering details yourself.",
511
+ ].join("\n"),
512
+ );
513
+ const engineeringHeadAgent = createDeepSeekAgent(
514
+ [
515
+ "You are the engineering head agent.",
516
+ "Break engineering goals into backend and frontend work, route tasks to the right executor, and report outcomes upstream.",
517
+ "Keep architecture decisions explicit.",
518
+ ].join("\n"),
519
+ );
520
+ const backendAgent = createDeepSeekAgent(
521
+ [
522
+ "You are the backend executor agent.",
523
+ "Handle APIs, data models, storage, integrations, and server-side correctness.",
524
+ "Escalate product or UI decisions instead of guessing.",
525
+ ].join("\n"),
526
+ );
527
+ const frontendAgent = createDeepSeekAgent(
528
+ [
529
+ "You are the frontend executor agent.",
530
+ "Handle UI flows, client state, accessibility, and browser behavior.",
531
+ "Escalate API contract questions instead of inventing them.",
532
+ ].join("\n"),
533
+ );
534
+
535
+ const engineeringTeam = createTeam({
536
+ name: "engineering",
537
+ lead: engineeringHeadAgent,
538
+ members: [
539
+ teamMember({ name: "backend", role: "executor", agent: backendAgent }),
540
+ teamMember({ name: "frontend", role: "executor", agent: frontendAgent }),
541
+ ],
542
+ });
543
+
544
+ const companyTeam = createTeam({
545
+ name: "company",
546
+ lead: ceoAgent,
547
+ members: [
548
+ teamMember({
549
+ name: "engineering",
550
+ role: "head",
551
+ focus: "Own engineering delivery",
552
+ agent: engineeringTeam,
553
+ }),
554
+ ],
555
+ });
556
+ ```
557
+
558
+ Use this pattern to model CEO -> Head Team -> Executor Agent without hard-coding
559
+ that hierarchy into the SDK.
560
+
561
+ ### Routing loops
562
+
563
+ The SDK does not block routing loops by default. A task can move from a manager
564
+ to a member, back to the manager for context, and then back to the same member.
565
+ That is normal organizational flow, not necessarily a runtime error.
566
+
567
+ Use `maxTurns`, permission callbacks, mailbox status, and host-level monitoring
568
+ to control cost and risk. If your application needs a strict hierarchy, expose
569
+ only the allowed members at each layer and enforce routing with permission
570
+ callbacks or a host-level policy.
571
+
572
+ ### Team runtime drain
573
+
574
+ Mailbox routing is explicit: a pending message belongs to its `to` mailbox and
575
+ must be handled by that member's agent. `claimNext(mailboxId)` only claims one
576
+ pending message for that mailbox and marks it `processing`.
577
+
578
+ ```ts
579
+ const message = await team.mailbox.claimNext("engineering::researcher");
580
+ ```
581
+
582
+ Use `team.drain()` to let the runtime advance already-routed work:
583
+
584
+ ```ts
585
+ const result = await team.drain({
586
+ maxRounds: 5,
587
+ maxMessages: 20,
588
+ });
589
+ ```
590
+
591
+ `drain()` iterates members, claims pending messages from each member's own
592
+ mailbox, and prompts that member agent. It does not re-route work. The member
593
+ must call `team_reply` for a final result or `team_followup` for progress. If a
594
+ member ends without either, the runtime marks the original message `failed` and
595
+ sends a diagnostic follow-up to the upstream mailbox.
596
+
597
+ ## Agent Workspace Tools
598
+
599
+ AgentLattice includes an opt-in set of workspace tools:
600
+
601
+ - `Read`
602
+ - `Write`
603
+ - `Edit`
604
+ - `LS`
605
+ - `Glob`
606
+ - `Grep`
607
+ - `Bash`
608
+
609
+ ```ts
610
+ import { createAgent, createAgentWorkspaceTools } from "agent-lattice";
611
+
612
+ const agent = createAgent({
613
+ apiKey: process.env.DEEPSEEK_API_KEY,
614
+ baseURL: "https://api.deepseek.com/anthropic",
615
+ model: "deepseek-v4-flash",
616
+ tools: createAgentWorkspaceTools({
617
+ cwd: process.cwd(),
618
+ allowedDirectories: [process.cwd()],
619
+ }),
620
+ permission: async request => {
621
+ if (request.toolName === "Bash" || request.toolName === "Write" || request.toolName === "Edit") {
622
+ return { behavior: "deny", message: "This host did not approve write or shell access." };
623
+ }
624
+ return { behavior: "allow" };
625
+ },
626
+ });
627
+ ```
628
+
629
+ These tools are not enabled by default. `Read`, `LS`, `Glob`, and `Grep` are
630
+ read-only observation tools and are not gated by workspace grants. `Write`,
631
+ `Edit`, and obvious Bash writes are gated to the configured workspace roots and
632
+ task-scoped shared workspace grants, so production hosts should pair write and
633
+ shell access with a permission callback. Shell redirects to `/dev/null` are
634
+ treated as discard targets, not workspace writes.
635
+
636
+ ## Multi-turn Session
637
+
638
+ ```ts
639
+ const agent = createAgent({
640
+ apiKey: process.env.DEEPSEEK_API_KEY,
641
+ baseURL: "https://api.deepseek.com/anthropic",
642
+ model: "deepseek-v4-flash",
643
+ });
644
+
645
+ await agent.prompt("My name is Ada.");
646
+ const result = await agent.prompt("What is my name?");
647
+ console.log(result.result);
648
+ ```
649
+
650
+ The SDK stores conversation state in memory for the lifetime of the `Agent`
651
+ instance. Persistent transcripts and resume support are intentionally out of
652
+ scope for the first release.