@bastani/atomic 0.9.28-alpha.1 → 0.9.28-alpha.2
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/CHANGELOG.md +33 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +23 -0
- package/dist/builtin/workflows/builtin/{chunk-wz8v67bn.js → chunk-aqv8ny38.js} +257 -259
- package/dist/builtin/workflows/builtin/{chunk-f61w0hpm.js → chunk-br4ac9a3.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-tecnpe8j.js → chunk-p3xmajmj.js} +438 -39
- package/dist/builtin/workflows/builtin/chunk-p6jagvyf.js +8 -0
- package/dist/builtin/workflows/builtin/goal.js +2 -2
- package/dist/builtin/workflows/builtin/index.js +4 -4
- package/dist/builtin/workflows/builtin/ralph.js +2 -2
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/authoring.d.ts +14 -0
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +107577 -106722
- package/dist/builtin/workflows/src/index.js +42284 -41141
- package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +16 -11
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +16 -1
- package/dist/cli/credential-print.d.ts +1 -1
- package/dist/cli/credential-print.d.ts.map +1 -1
- package/dist/cli/credential-print.js +2 -56
- package/dist/cli/credential-print.js.map +1 -1
- package/dist/core/agent-session-accessors.d.ts.map +1 -1
- package/dist/core/agent-session-accessors.js +6 -1
- package/dist/core/agent-session-accessors.js.map +1 -1
- package/dist/core/agent-session-compaction.d.ts.map +1 -1
- package/dist/core/agent-session-compaction.js +20 -3
- package/dist/core/agent-session-compaction.js.map +1 -1
- package/dist/core/agent-session-events.d.ts.map +1 -1
- package/dist/core/agent-session-events.js +2 -1
- package/dist/core/agent-session-events.js.map +1 -1
- package/dist/core/agent-session-extension-bindings.d.ts +6 -0
- package/dist/core/agent-session-extension-bindings.d.ts.map +1 -1
- package/dist/core/agent-session-extension-bindings.js +87 -7
- package/dist/core/agent-session-extension-bindings.js.map +1 -1
- package/dist/core/agent-session-methods.d.ts +2 -1
- package/dist/core/agent-session-methods.d.ts.map +1 -1
- package/dist/core/agent-session-methods.js.map +1 -1
- package/dist/core/agent-session-prompt.d.ts.map +1 -1
- package/dist/core/agent-session-prompt.js +14 -1
- package/dist/core/agent-session-prompt.js.map +1 -1
- package/dist/core/agent-session-tool-registry.d.ts.map +1 -1
- package/dist/core/agent-session-tool-registry.js +1 -0
- package/dist/core/agent-session-tool-registry.js.map +1 -1
- package/dist/core/agent-session-types.d.ts +1 -0
- package/dist/core/agent-session-types.d.ts.map +1 -1
- package/dist/core/agent-session-types.js.map +1 -1
- package/dist/core/compaction/classifier-compaction.d.ts +13 -0
- package/dist/core/compaction/classifier-compaction.d.ts.map +1 -0
- package/dist/core/compaction/classifier-compaction.js +110 -0
- package/dist/core/compaction/classifier-compaction.js.map +1 -0
- package/dist/core/compaction/compaction-boundary.d.ts.map +1 -1
- package/dist/core/compaction/compaction-boundary.js +2 -6
- package/dist/core/compaction/compaction-boundary.js.map +1 -1
- package/dist/core/compaction/compaction-runner.d.ts +16 -1
- package/dist/core/compaction/compaction-runner.d.ts.map +1 -1
- package/dist/core/compaction/compaction-runner.js +97 -12
- package/dist/core/compaction/compaction-runner.js.map +1 -1
- package/dist/core/compaction/compaction-types.d.ts +18 -1
- package/dist/core/compaction/compaction-types.d.ts.map +1 -1
- package/dist/core/compaction/compaction-types.js +1 -1
- package/dist/core/compaction/compaction-types.js.map +1 -1
- package/dist/core/compaction/fallback-planner.d.ts +1 -0
- package/dist/core/compaction/fallback-planner.d.ts.map +1 -1
- package/dist/core/compaction/fallback-planner.js +2 -0
- package/dist/core/compaction/fallback-planner.js.map +1 -1
- package/dist/core/compaction/index.d.ts +3 -0
- package/dist/core/compaction/index.d.ts.map +1 -1
- package/dist/core/compaction/index.js +3 -0
- package/dist/core/compaction/index.js.map +1 -1
- package/dist/core/compaction/model-resolver.d.ts +16 -0
- package/dist/core/compaction/model-resolver.d.ts.map +1 -0
- package/dist/core/compaction/model-resolver.js +18 -0
- package/dist/core/compaction/model-resolver.js.map +1 -0
- package/dist/core/compaction/morph-compaction.d.ts +14 -0
- package/dist/core/compaction/morph-compaction.d.ts.map +1 -0
- package/dist/core/compaction/morph-compaction.js +99 -0
- package/dist/core/compaction/morph-compaction.js.map +1 -0
- package/dist/core/compaction/pi-summary-fallback.d.ts +34 -0
- package/dist/core/compaction/pi-summary-fallback.d.ts.map +1 -0
- package/dist/core/compaction/pi-summary-fallback.js +357 -0
- package/dist/core/compaction/pi-summary-fallback.js.map +1 -0
- package/dist/core/compaction/planner-outcome.d.ts +10 -6
- package/dist/core/compaction/planner-outcome.d.ts.map +1 -1
- package/dist/core/compaction/planner-outcome.js +11 -4
- package/dist/core/compaction/planner-outcome.js.map +1 -1
- package/dist/core/compaction/range-planner-diagnostics.d.ts +3 -1
- package/dist/core/compaction/range-planner-diagnostics.d.ts.map +1 -1
- package/dist/core/compaction/range-planner-diagnostics.js +11 -0
- package/dist/core/compaction/range-planner-diagnostics.js.map +1 -1
- package/dist/core/compaction/range-planner.d.ts +3 -3
- package/dist/core/compaction/range-planner.d.ts.map +1 -1
- package/dist/core/compaction/range-planner.js +59 -28
- package/dist/core/compaction/range-planner.js.map +1 -1
- package/dist/core/compaction/region-trimming.d.ts.map +1 -1
- package/dist/core/compaction/region-trimming.js +7 -0
- package/dist/core/compaction/region-trimming.js.map +1 -1
- package/dist/core/compaction/structured-compaction-input.d.ts +19 -0
- package/dist/core/compaction/structured-compaction-input.d.ts.map +1 -0
- package/dist/core/compaction/structured-compaction-input.js +70 -0
- package/dist/core/compaction/structured-compaction-input.js.map +1 -0
- package/dist/core/compaction/transcript-serialization.d.ts +1 -0
- package/dist/core/compaction/transcript-serialization.d.ts.map +1 -1
- package/dist/core/compaction/transcript-serialization.js +26 -0
- package/dist/core/compaction/transcript-serialization.js.map +1 -1
- package/dist/core/extensions/api-types.d.ts +2 -2
- package/dist/core/extensions/api-types.d.ts.map +1 -1
- package/dist/core/extensions/api-types.js.map +1 -1
- package/dist/core/extensions/context-types.d.ts +1 -0
- package/dist/core/extensions/context-types.d.ts.map +1 -1
- package/dist/core/extensions/context-types.js.map +1 -1
- package/dist/core/extensions/loader-api.js.map +1 -1
- package/dist/core/extensions/runner-context.d.ts +10 -4
- package/dist/core/extensions/runner-context.d.ts.map +1 -1
- package/dist/core/extensions/runner-context.js +47 -3
- package/dist/core/extensions/runner-context.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +10 -3
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +31 -7
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/workflow-events.d.ts +49 -0
- package/dist/core/extensions/workflow-events.d.ts.map +1 -1
- package/dist/core/extensions/workflow-events.js.map +1 -1
- package/dist/core/extensions/workflow-run-control-hub.d.ts +16 -4
- package/dist/core/extensions/workflow-run-control-hub.d.ts.map +1 -1
- package/dist/core/extensions/workflow-run-control-hub.js +72 -1
- package/dist/core/extensions/workflow-run-control-hub.js.map +1 -1
- package/dist/core/extensions/workflow-run-control.d.ts +11 -3
- package/dist/core/extensions/workflow-run-control.d.ts.map +1 -1
- package/dist/core/extensions/workflow-run-control.js.map +1 -1
- package/dist/core/messages.d.ts +1 -0
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +5 -1
- package/dist/core/messages.js.map +1 -1
- package/dist/core/remote-catalog-provider.d.ts.map +1 -1
- package/dist/core/remote-catalog-provider.js +3 -0
- package/dist/core/remote-catalog-provider.js.map +1 -1
- package/dist/core/session-manager-history.d.ts.map +1 -1
- package/dist/core/session-manager-history.js +4 -2
- package/dist/core/session-manager-history.js.map +1 -1
- package/dist/core/settings-manager-basic-accessors.d.ts +2 -0
- package/dist/core/settings-manager-basic-accessors.d.ts.map +1 -1
- package/dist/core/settings-manager-basic-accessors.js +29 -0
- package/dist/core/settings-manager-basic-accessors.js.map +1 -1
- package/dist/core/settings-types.d.ts +1 -0
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/transcript.d.ts +58 -0
- package/dist/core/transcript.d.ts.map +1 -0
- package/dist/core/transcript.js +185 -0
- package/dist/core/transcript.js.map +1 -0
- package/dist/extensions/herdr/index.d.ts.map +1 -1
- package/dist/extensions/herdr/index.js +15 -10
- package/dist/extensions/herdr/index.js.map +1 -1
- package/dist/extensions/herdr/pane-owner.d.ts +6 -2
- package/dist/extensions/herdr/pane-owner.d.ts.map +1 -1
- package/dist/extensions/herdr/pane-owner.js +19 -4
- package/dist/extensions/herdr/pane-owner.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/components/compaction-boundary-message.d.ts +2 -0
- package/dist/modes/interactive/components/compaction-boundary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/compaction-boundary-message.js +15 -3
- package/dist/modes/interactive/components/compaction-boundary-message.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector-items.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector-items.js +43 -0
- package/dist/modes/interactive/components/settings-selector-items.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector-types.d.ts +6 -0
- package/dist/modes/interactive/components/settings-selector-types.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector-types.js.map +1 -1
- package/dist/modes/interactive/interactive-prompt-turn.js +6 -4
- package/dist/modes/interactive/interactive-prompt-turn.js.map +1 -1
- package/dist/modes/interactive/interactive-render-chat.js +13 -1
- package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
- package/dist/modes/interactive/interactive-selectors.js +8 -0
- package/dist/modes/interactive/interactive-selectors.js.map +1 -1
- package/dist/modes/interactive/interactive-session-runtime.js +2 -0
- package/dist/modes/interactive/interactive-session-runtime.js.map +1 -1
- package/dist/modes/interactive/interactive-startup.js +1 -1
- package/dist/modes/interactive/interactive-startup.js.map +1 -1
- package/dist/utils/credential-redaction.d.ts +2 -0
- package/dist/utils/credential-redaction.d.ts.map +1 -0
- package/dist/utils/credential-redaction.js +20 -0
- package/dist/utils/credential-redaction.js.map +1 -0
- package/docs/compaction/reference.md +10 -4
- package/docs/compaction.md +40 -8
- package/docs/docs.json +1 -0
- package/docs/extensions/api-reference.md +4 -0
- package/docs/extensions/events.md +2 -2
- package/docs/providers.md +12 -1
- package/docs/sdk/reference.md +16 -1
- package/docs/sdk/web-server.md +325 -0
- package/docs/sdk.md +83 -1
- package/docs/security.md +1 -1
- package/docs/workflows/api-reference.md +71 -3
- package/docs/workflows/builtins.md +4 -0
- package/docs/workflows/operations.md +12 -4
- package/npm-shrinkwrap.json +32 -32
- package/package.json +3 -3
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Embedding Atomic in a web server"
|
|
3
|
+
sidebarTitle: "Web server"
|
|
4
|
+
description: "Run Atomic sessions and workflows behind a Next.js App Router route and stream their output to a browser."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Embedding Atomic in a web server
|
|
8
|
+
|
|
9
|
+
This guide puts an Atomic agent behind an HTTP endpoint, using a Next.js App Router project as the example. The same pattern works in any long-running Node.js server: keep one session per conversation, start a prompt per request, and stream the session's events back as newline-delimited JSON (NDJSON).
|
|
10
|
+
|
|
11
|
+
Read the [SDK](/sdk) page first for `createAgentSession()` and session events.
|
|
12
|
+
|
|
13
|
+
The examples use `createTranscript()`, `createAgentSessionAdapter()`, and `RunOpts.onStageSessionEvent`, which require an Atomic release newer than 0.9.28-alpha.1. Check the [changelog](/changelog) for your installed version.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @bastani/atomic
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Atomic needs Node.js 22.19 or newer and does not require dependency install scripts.
|
|
22
|
+
|
|
23
|
+
Bun blocks the install scripts of some dependencies, such as `@embedded-postgres/<platform>`, `protobufjs`, and `@google/genai`, and reports them after `bun add @bastani/atomic`. You don't need to add them to `trustedDependencies` in `package.json`: Atomic prepares the embedded Postgres runtime itself the first time it starts it, and it doesn't depend on the other scripts.
|
|
24
|
+
|
|
25
|
+
## Keep Atomic out of the server bundle
|
|
26
|
+
|
|
27
|
+
Atomic finds its bundled resources and native modules relative to its installed package, so it must load from `node_modules` rather than from Next.js's server bundle. List it in `serverExternalPackages` (Next.js 15 and later):
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// next.config.ts
|
|
31
|
+
import type { NextConfig } from "next";
|
|
32
|
+
|
|
33
|
+
const nextConfig: NextConfig = {
|
|
34
|
+
serverExternalPackages: ["@bastani/atomic", "@bastani/atomic-natives", "@bastani/pi-ai", "typebox"],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default nextConfig;
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Atomic's own dependencies load from `node_modules` once `@bastani/atomic` is external. The other entries cover packages your app may import directly, such as `typebox` for workflow schemas, so your code and Atomic share one copy. Add any other Atomic dependency you import yourself.
|
|
41
|
+
|
|
42
|
+
Every route that imports Atomic must use the Node.js runtime:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
export const runtime = "nodejs";
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Import Atomic only from server code. Client components may use `import type` from `@bastani/atomic`, which adds nothing to the browser bundle.
|
|
49
|
+
|
|
50
|
+
## Keep one session per conversation
|
|
51
|
+
|
|
52
|
+
An `AgentSession` holds the conversation's history, so create one per conversation and reuse it across requests. Keep sessions on `globalThis`: in `next dev`, module-level variables reset on every hot reload, but `globalThis` survives, so open conversations don't vanish and old sessions aren't leaked. Restart the dev server after changing session options; existing sessions keep the options they were created with.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
// lib/atomic.ts
|
|
56
|
+
import "server-only";
|
|
57
|
+
import { type AgentSession, createAgentSession, ModelRuntime, SessionManager } from "@bastani/atomic";
|
|
58
|
+
|
|
59
|
+
export const WORKSPACE = process.env.AGENT_WORKSPACE ?? "/srv/agent-workspace";
|
|
60
|
+
|
|
61
|
+
export interface Conversation {
|
|
62
|
+
session: AgentSession;
|
|
63
|
+
busy: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface AtomicState {
|
|
67
|
+
modelRuntime: Promise<ModelRuntime>;
|
|
68
|
+
conversations: Map<string, Promise<Conversation>>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const shared = globalThis as typeof globalThis & { atomicState?: AtomicState };
|
|
72
|
+
const state = (shared.atomicState ??= {
|
|
73
|
+
modelRuntime: ModelRuntime.create(),
|
|
74
|
+
conversations: new Map(),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export function getConversation(id: string): Promise<Conversation> {
|
|
78
|
+
let conversation = state.conversations.get(id);
|
|
79
|
+
if (!conversation) {
|
|
80
|
+
conversation = state.modelRuntime.then(async (modelRuntime) => {
|
|
81
|
+
const { session } = await createAgentSession({
|
|
82
|
+
cwd: WORKSPACE,
|
|
83
|
+
sessionManager: SessionManager.inMemory(WORKSPACE),
|
|
84
|
+
modelRuntime,
|
|
85
|
+
tools: ["read", "find", "search"],
|
|
86
|
+
builtins: { workflows: false, subagents: false, mcp: false, "web-access": false, intercom: false },
|
|
87
|
+
});
|
|
88
|
+
return { session, busy: false };
|
|
89
|
+
});
|
|
90
|
+
conversation.catch(() => state.conversations.delete(id));
|
|
91
|
+
state.conversations.set(id, conversation);
|
|
92
|
+
}
|
|
93
|
+
return conversation;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function closeConversation(id: string): Promise<void> {
|
|
97
|
+
const conversation = state.conversations.get(id);
|
|
98
|
+
state.conversations.delete(id);
|
|
99
|
+
if (conversation) await (await conversation).session.dispose();
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`SessionManager.inMemory()` keeps history in process memory. To keep conversations across restarts, pass a file-backed `SessionManager` instead; see [Sessions](/sessions). Call `closeConversation()` when a conversation ends or has been idle for a while, because each open session holds resources until it is disposed.
|
|
104
|
+
|
|
105
|
+
Sessions on `globalThis` live in one server process. Run this on a long-lived Node.js server, and route each conversation to the same instance if you run several. Short-lived serverless functions lose the session between requests.
|
|
106
|
+
|
|
107
|
+
## Stream a prompt
|
|
108
|
+
|
|
109
|
+
The route below starts one prompt and streams the assistant's output as NDJSON. `createTranscript()` turns session events into ordered, JSON-serializable parts (text, thinking, and tool calls joined to their results), so each line carries the complete output so far and the client simply replaces what it shows.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// app/api/chat/route.ts
|
|
113
|
+
import { createTranscript, type TranscriptPart } from "@bastani/atomic";
|
|
114
|
+
import { getConversation } from "@/lib/atomic";
|
|
115
|
+
import { authenticate } from "@/lib/auth";
|
|
116
|
+
|
|
117
|
+
export const runtime = "nodejs";
|
|
118
|
+
|
|
119
|
+
export type ChatLine =
|
|
120
|
+
| { type: "parts"; parts: TranscriptPart[] }
|
|
121
|
+
| { type: "done" }
|
|
122
|
+
| { type: "error"; message: string };
|
|
123
|
+
|
|
124
|
+
export async function POST(request: Request): Promise<Response> {
|
|
125
|
+
const user = await authenticate(request);
|
|
126
|
+
if (!user) return new Response("Unauthorized", { status: 401 });
|
|
127
|
+
|
|
128
|
+
const { conversationId, message } = (await request.json()) as { conversationId: string; message: string };
|
|
129
|
+
const conversation = await getConversation(`${user.id}:${conversationId}`);
|
|
130
|
+
const { session } = conversation;
|
|
131
|
+
if (conversation.busy || session.isStreaming) {
|
|
132
|
+
return new Response("This conversation is already answering a prompt.", { status: 409 });
|
|
133
|
+
}
|
|
134
|
+
if (request.signal.aborted) return new Response(null, { status: 499 });
|
|
135
|
+
conversation.busy = true;
|
|
136
|
+
|
|
137
|
+
const encoder = new TextEncoder();
|
|
138
|
+
const transcript = createTranscript();
|
|
139
|
+
let open = true;
|
|
140
|
+
|
|
141
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
142
|
+
start(controller) {
|
|
143
|
+
const send = (line: ChatLine) => {
|
|
144
|
+
if (open) controller.enqueue(encoder.encode(`${JSON.stringify(line)}\n`));
|
|
145
|
+
};
|
|
146
|
+
const stop = () => void session.abort();
|
|
147
|
+
request.signal.addEventListener("abort", stop, { once: true });
|
|
148
|
+
|
|
149
|
+
const unsubscribe = session.subscribe((event) => {
|
|
150
|
+
transcript.apply(event);
|
|
151
|
+
if (
|
|
152
|
+
event.type === "message_update" ||
|
|
153
|
+
event.type === "message_end" ||
|
|
154
|
+
event.type === "tool_execution_update" ||
|
|
155
|
+
event.type === "tool_execution_end"
|
|
156
|
+
) {
|
|
157
|
+
send({ type: "parts", parts: transcript.parts() });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
session
|
|
162
|
+
.prompt(message)
|
|
163
|
+
.then(() => send({ type: "done" }))
|
|
164
|
+
.catch((error: Error) => send({ type: "error", message: error.message }))
|
|
165
|
+
.finally(() => {
|
|
166
|
+
unsubscribe();
|
|
167
|
+
request.signal.removeEventListener("abort", stop);
|
|
168
|
+
conversation.busy = false;
|
|
169
|
+
if (open) {
|
|
170
|
+
open = false;
|
|
171
|
+
controller.close();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
cancel() {
|
|
176
|
+
open = false;
|
|
177
|
+
void session.abort();
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return new Response(stream, {
|
|
182
|
+
headers: { "Content-Type": "application/x-ndjson; charset=utf-8", "Cache-Control": "no-cache" },
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
A few details matter here:
|
|
188
|
+
|
|
189
|
+
- **One prompt at a time.** `session.prompt()` rejects while the session is already streaming. The route answers `409` instead. `busy` is set synchronously, so two requests that arrive together can't both start a prompt. To queue a message instead of rejecting it, call `session.prompt(message, { streamingBehavior: "followUp" })`.
|
|
190
|
+
- **Stop on disconnect.** When the browser disconnects or aborts its `fetch`, `request.signal` aborts and the stream is cancelled. Both call `session.abort()`, which stops the current turn and its tools but keeps the session usable.
|
|
191
|
+
- **Payload size.** Each line repeats the whole output so far. For long answers, send lines on a timer instead of on every event, or send only the parts that changed.
|
|
192
|
+
|
|
193
|
+
`createTranscript()` reports a tool result's text by default. To send a tool's structured `details` instead, pass `createTranscript({ toolResult: (toolName) => (toolName === "todo" ? "details" : "content") })`.
|
|
194
|
+
|
|
195
|
+
## Read the stream in the browser
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
// app/chat/send-message.ts
|
|
199
|
+
import type { TranscriptPart } from "@bastani/atomic";
|
|
200
|
+
import type { ChatLine } from "@/app/api/chat/route";
|
|
201
|
+
|
|
202
|
+
export async function sendMessage(
|
|
203
|
+
conversationId: string,
|
|
204
|
+
message: string,
|
|
205
|
+
onParts: (parts: TranscriptPart[]) => void,
|
|
206
|
+
signal?: AbortSignal,
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
const response = await fetch("/api/chat", {
|
|
209
|
+
method: "POST",
|
|
210
|
+
headers: { "Content-Type": "application/json" },
|
|
211
|
+
body: JSON.stringify({ conversationId, message }),
|
|
212
|
+
signal,
|
|
213
|
+
});
|
|
214
|
+
if (!response.ok || !response.body) throw new Error(await response.text());
|
|
215
|
+
|
|
216
|
+
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
217
|
+
let buffered = "";
|
|
218
|
+
for (;;) {
|
|
219
|
+
const { value, done } = await reader.read();
|
|
220
|
+
if (done) return;
|
|
221
|
+
buffered += value;
|
|
222
|
+
const lines = buffered.split("\n");
|
|
223
|
+
buffered = lines.pop() ?? "";
|
|
224
|
+
for (const text of lines) {
|
|
225
|
+
if (!text) continue;
|
|
226
|
+
const line = JSON.parse(text) as ChatLine;
|
|
227
|
+
if (line.type === "parts") onParts(line.parts);
|
|
228
|
+
if (line.type === "error") throw new Error(line.message);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Render each part by `type`: `text` and `thinking` are strings, and a `toolCall` part has `name`, `arguments`, and, once the tool reports progress or finishes, a `result` with `content`, `isError`, and `isPartial`. Pass an `AbortController`'s signal to stop the agent from the page.
|
|
235
|
+
|
|
236
|
+
## Run a workflow from a route
|
|
237
|
+
|
|
238
|
+
`run()` from `@bastani/atomic/workflows` runs a workflow definition in the server process. Each stage runs as an in-process Atomic session, so `run()` needs no adapters. Use `createAgentSessionAdapter()` when every stage should share session options, such as the same tool restrictions as your chat sessions:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
// app/api/review/route.ts
|
|
242
|
+
import { createTranscript, type Transcript } from "@bastani/atomic";
|
|
243
|
+
import { createAgentSessionAdapter, run } from "@bastani/atomic/workflows";
|
|
244
|
+
import { WORKSPACE } from "@/lib/atomic";
|
|
245
|
+
import { authenticate } from "@/lib/auth";
|
|
246
|
+
import { reviewWorkflow } from "@/workflows/review";
|
|
247
|
+
|
|
248
|
+
export const runtime = "nodejs";
|
|
249
|
+
|
|
250
|
+
export async function POST(request: Request): Promise<Response> {
|
|
251
|
+
if (!(await authenticate(request))) return new Response("Unauthorized", { status: 401 });
|
|
252
|
+
const { topic } = (await request.json()) as { topic: string };
|
|
253
|
+
if (request.signal.aborted) return new Response(null, { status: 499 });
|
|
254
|
+
|
|
255
|
+
const encoder = new TextEncoder();
|
|
256
|
+
const abort = new AbortController();
|
|
257
|
+
request.signal.addEventListener("abort", () => abort.abort(), { once: true });
|
|
258
|
+
|
|
259
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
260
|
+
async start(controller) {
|
|
261
|
+
const send = (line: object) => {
|
|
262
|
+
if (!abort.signal.aborted) controller.enqueue(encoder.encode(`${JSON.stringify(line)}\n`));
|
|
263
|
+
};
|
|
264
|
+
const stages = new Map<string, Transcript>();
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const result = await run(reviewWorkflow, { topic }, {
|
|
268
|
+
cwd: WORKSPACE,
|
|
269
|
+
signal: abort.signal,
|
|
270
|
+
durability: { mode: "memory" },
|
|
271
|
+
adapters: {
|
|
272
|
+
agentSession: createAgentSessionAdapter({ cwd: WORKSPACE, tools: ["read", "find", "search"] }),
|
|
273
|
+
},
|
|
274
|
+
onStageSessionEvent: (runId, stageId, event) => {
|
|
275
|
+
let transcript = stages.get(stageId);
|
|
276
|
+
if (!transcript) stages.set(stageId, (transcript = createTranscript()));
|
|
277
|
+
transcript.apply(event);
|
|
278
|
+
if (event.type === "message_update" || event.type === "tool_execution_end") {
|
|
279
|
+
send({ type: "stage", runId, stageId, parts: transcript.parts() });
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
send({ type: "done", status: result.status, result: result.result, error: result.error });
|
|
284
|
+
} catch (error) {
|
|
285
|
+
send({ type: "error", message: error instanceof Error ? error.message : String(error) });
|
|
286
|
+
} finally {
|
|
287
|
+
if (!abort.signal.aborted) controller.close();
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
cancel() {
|
|
291
|
+
abort.abort();
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
return new Response(stream, { headers: { "Content-Type": "application/x-ndjson; charset=utf-8" } });
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
`onStageSessionEvent` receives every stage's session events tagged with its run and stage ids, including stages that switch to a fallback model and stages of nested workflows. Aborting `signal`, here when the client disconnects, cancels the run.
|
|
300
|
+
|
|
301
|
+
### Choose workflow durability
|
|
302
|
+
|
|
303
|
+
By default, `run()` makes workflow state durable by starting a managed local Postgres, so a run can resume after the process exits. Request-scoped servers rarely want that:
|
|
304
|
+
|
|
305
|
+
- **`durability: { mode: "memory" }`** keeps state in memory for this call only. No Postgres starts, and the run can't resume after the process exits. Use it when the request owns the run.
|
|
306
|
+
- **`durability: { mode: "durable", systemDatabaseUrl }`** stores state in a Postgres database you run, such as a hosted one, and Atomic doesn't start its own. Use it in deployments that need runs to survive restarts. `DBOS_SYSTEM_DATABASE_URL`, when set, overrides the URL.
|
|
307
|
+
|
|
308
|
+
One process uses one workflow database. See [Choosing the durable backend](/workflows/api-reference#choosing-the-durable-backend) for the full rules, and [Workflows](/workflows) for writing workflow definitions.
|
|
309
|
+
|
|
310
|
+
## Secure the endpoint
|
|
311
|
+
|
|
312
|
+
A route that drives an agent session lets whoever calls it run tools on your server with the server process's permissions. Atomic has [no built-in sandbox](/security#no-built-in-sandbox).
|
|
313
|
+
|
|
314
|
+
- **Require authentication** on every route that creates sessions, sends prompts, or starts workflows, and scope conversation ids to the authenticated user, as `getConversation()` does above.
|
|
315
|
+
- **Allow only the tools you need.** `tools` is an allowlist; the examples allow only the read-only `read`, `find`, and `search`. Leave out `bash`, `edit`, and `write` unless the agent must change files or run commands. `excludedTools` removes tools, and `noTools: "all"` exposes none. `ask_user_question` needs a person to answer, so leave it out of headless sessions. See [Tools](/sdk/reference#tools).
|
|
316
|
+
- **Disable builtins you don't use.** `builtins` turns off the shipped workflows, subagents, MCP, web access, and Intercom packages, along with their tools.
|
|
317
|
+
- **Choose the working directory deliberately.** `cwd` sets the starting directory for tool paths and controls which project settings, context files, and extensions Atomic discovers. It does not restrict file access: tools can use absolute paths and paths outside `cwd`. Point it at a directory you control, never at an upload or a user-supplied path.
|
|
318
|
+
- **Isolate the process** in a container, VM, or restricted OS account when callers must not access other server files, even with read-only tools, or when the agent can write files or run commands. See [Containerization](/containerization).
|
|
319
|
+
- **Keep credentials on the server.** Provider keys come from the server's `ModelRuntime`. Never send them to the browser or accept them from requests.
|
|
320
|
+
|
|
321
|
+
## Next steps
|
|
322
|
+
|
|
323
|
+
- [SDK](/sdk) for sessions, prompting, and events.
|
|
324
|
+
- [SDK API reference](/sdk/reference) for every `createAgentSession()` option.
|
|
325
|
+
- [Workflow API reference](/workflows/api-reference) for `run()` and `RunOpts`.
|
package/docs/sdk.md
CHANGED
|
@@ -26,6 +26,8 @@ For a custom host that runs background work, see [Owner-bound task supervisor](/
|
|
|
26
26
|
|
|
27
27
|
Not sure the SDK is the right integration mode? Compare it with RPC and JSON mode on [Programmatic use](/programmatic).
|
|
28
28
|
|
|
29
|
+
To serve an agent over HTTP, see [Embedding Atomic in a web server](/sdk/web-server), which walks through a Next.js App Router route that streams session output to a browser.
|
|
30
|
+
|
|
29
31
|
## Quick Start
|
|
30
32
|
|
|
31
33
|
```typescript
|
|
@@ -241,6 +243,7 @@ interface SessionWorkflows {
|
|
|
241
243
|
pause(target: { all: true }): Promise<WorkflowRunControlOutcome>;
|
|
242
244
|
quit(target: string | { all: true }): Promise<WorkflowRunControlOutcome>;
|
|
243
245
|
resume(runId: string, options?: { stageId?: string; message?: string }): Promise<WorkflowRunControlOutcome>;
|
|
246
|
+
observe(observer: WorkflowActivityObserver): WorkflowActivitySubscription;
|
|
244
247
|
}
|
|
245
248
|
```
|
|
246
249
|
|
|
@@ -313,7 +316,38 @@ async function onResumeClicked() {
|
|
|
313
316
|
}
|
|
314
317
|
```
|
|
315
318
|
|
|
316
|
-
`pause` holds the live run and keeps it resumable in this process; `quit` retires the executor at a durability boundary so a later process can resume it. After `quit`, or when resuming a run from a previous process, `resume` follows the durable resume path: keep the workflow definition and durable storage available. See [pausing, quitting, and resuming](/workflows/operations#pausing-quitting-and-resuming) for what each action guarantees.
|
|
319
|
+
`pause` holds the live run and keeps it resumable in this process; `quit` retires the executor at a durability boundary so a later process can resume it. After `quit`, or when resuming a run from a previous process, `resume` follows the durable resume path: keep the workflow definition and durable storage available. See [pausing, quitting, and resuming](/workflows/operations#pausing-quitting-and-resuming) for what each action guarantees.
|
|
320
|
+
|
|
321
|
+
#### Observing workflow runs
|
|
322
|
+
|
|
323
|
+
To follow runs instead of polling `listRuns()`, call `session.workflows.observe(observer)`. It covers runs that the agent launches with the `workflow` tool, and needs no extension. The first frame is a `snapshot`. After it come `changed` and `removed` frames, delivered in order. Each callback finishes before the next one starts. Call `dispose()` on the returned subscription to stop. The frames and cursors follow the [workflow activity contract](/extensions/events#workflow-activity-and-lifecycle-hooks).
|
|
324
|
+
|
|
325
|
+
Each root summary carries a `graph` with the nodes of that root's workflow, including the nodes of nested child workflows. Nodes are `stage` nodes and `ctx.tool` nodes. Each one has an `id`, `kind`, `name`, `status`, `parentIds`, `runId`, `nodeId` (its run-local id), `depth`, and `executionOrder` when it is known. Tool nodes add their `ordinal`. A stage that is waiting on a prompt adds `pendingPrompt` with the prompt's `id`, `kind`, `message`, `choices`, and `createdAt`. A prompt that a run raises outside any stage appears in `graph.runPrompts` with the same fields and the `runId` that owns it. A node status change produces a `changed` frame. Frames are plain JSON-serializable objects.
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
import type { WorkflowRootActivity } from "@bastani/atomic";
|
|
329
|
+
|
|
330
|
+
const roots = new Map<string, WorkflowRootActivity>();
|
|
331
|
+
const subscription = session.workflows.observe((frame) => {
|
|
332
|
+
if (frame.kind === "snapshot") {
|
|
333
|
+
roots.clear();
|
|
334
|
+
// `recovering` or `unavailable` means activity is unknown, not idle.
|
|
335
|
+
if (frame.availability === "ready") for (const root of frame.roots) roots.set(root.rootRunId, root);
|
|
336
|
+
} else if (frame.kind === "changed") {
|
|
337
|
+
roots.set(frame.root.rootRunId, frame.root);
|
|
338
|
+
for (const node of frame.root.graph?.nodes ?? []) {
|
|
339
|
+
renderNode(node.id, node.name, node.status, node.parentIds);
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
roots.delete(frame.rootRunId);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Later:
|
|
347
|
+
subscription.dispose();
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Observation can start before the workflows package has finished loading; the first snapshot then reports `availability: "unavailable"`, and a `ready` snapshot follows. The subscription survives `session.reload()`. After a reload, the observer receives a new snapshot with a new cursor `epoch`, so replace everything you knew. When the session is disposed, each observer receives a final `snapshot` with `availability: "unavailable"` before `dispose()` resolves; after that, `observe()` throws `WorkflowRunControlUnavailableError`. This stream reports root activity and graph state. It does not carry the events inside each stage.
|
|
317
351
|
|
|
318
352
|
### Workflow and subagent children
|
|
319
353
|
|
|
@@ -403,6 +437,8 @@ A `ShutdownFailed` error contains component failures in `errors`; cleanup still
|
|
|
403
437
|
|
|
404
438
|
Captured extension APIs also refuse new execution, mutations and registrations as soon as close or reload begins. Already-admitted `pi.exec()` calls remain part of the awaited drain; provide `signal` or `timeout` when invoking subprocesses that might not finish on their own. `abort()` alone does not retire the API. A rejected transactional reload restores the surviving generation's action admission.
|
|
405
439
|
|
|
440
|
+
Custom messages queued with `persistWhenStreaming: true` survive a successful reload and receive their model turn through the replacement extensions. An explicitly paused queue remains paused; call `resumeQueuedMessages()` before sending the next prompt. A rejected transactional reload retains the notices for the next prompt on the surviving generation.
|
|
441
|
+
|
|
406
442
|
#### Finishing admitted work
|
|
407
443
|
|
|
408
444
|
Disposal and reload wait for admitted callbacks, resource refreshes, and subprocesses. Ensure callbacks can settle independently of disposal, including work started by cleanup handlers. Give `pi.exec()` calls a timeout or cancellation signal, and await them in the handler so you can inspect failures. Never make cleanup wait for disposal itself.
|
|
@@ -415,6 +451,24 @@ Strict transactional reload failure leaves the original generation usable. Ordin
|
|
|
415
451
|
|
|
416
452
|
Prefer acquiring extension resources in `session_start`. If a factory acquires them earlier, register `session_shutdown` immediately, even if `extensionsOverride` may later omit that factory. Capture cleanup handles when acquiring resources rather than rereading loader getters during cleanup.
|
|
417
453
|
|
|
454
|
+
Transactional reload dispatches candidate `session_start` before commit. Use `publishExtensionContextEffect(ctx, effect)` to publish shared lifecycle state only after commit and retiring-generation cleanup; rollback discards the effect. Outside a transactional candidate, it runs immediately. Awaiting the helper queues the effect, not its eventual execution, so startup must not depend on the effect's result.
|
|
455
|
+
|
|
456
|
+
For synchronous identity handoff, use `publishExtensionContextEffect(ctx, effect, "commit")`. This phase runs after the transaction commits and before predecessor authority is revoked or any retirement work is awaited. It must not return a promise or start asynchronous work; asynchronous callbacks are rejected and reported. The default `"start"` phase still runs after retirement, when reporter and task-host startup is safe.
|
|
457
|
+
|
|
458
|
+
Register generation-owned observer disposal with `registerExtensionContextRetirementEffect(ctx, dispose)` during `session_start`. Disposal is synchronous, runs before authority revocation and before rollback invalidation, and is independent of `session_shutdown` handler order. The returned function unregisters the effect. Keep awaited resource cleanup in `session_shutdown`; observer disposal must not quit or checkpoint preserved workflows.
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
import { publishExtensionContextEffect, type ExtensionFactory } from "@bastani/atomic";
|
|
462
|
+
|
|
463
|
+
const extension: ExtensionFactory = (pi) => {
|
|
464
|
+
pi.on("session_start", (_event, ctx) =>
|
|
465
|
+
publishExtensionContextEffect(ctx, () => {
|
|
466
|
+
pi.events.emit("example:session-ready", { sessionId: ctx.sessionManager.getSessionId() });
|
|
467
|
+
}),
|
|
468
|
+
);
|
|
469
|
+
};
|
|
470
|
+
```
|
|
471
|
+
|
|
418
472
|
Do not cache dialog functions across reload attempts. Retiring functions refuse new questions; after a rejected transaction, use the surviving session's current `ctx.ui`.
|
|
419
473
|
|
|
420
474
|
Shared loaders, event buses, settings, and `SessionManager` instances do not share live task ownership. Closing one session leaves another session's commands and borrowed discovery resources alone.
|
|
@@ -678,6 +732,34 @@ To rebuild an assistant message from deltas, accumulate them in your own message
|
|
|
678
732
|
|
|
679
733
|
If you subscribe part-way through a turn, seed your object from `session.agent.state.streamingMessage`, if present, to include the deltas you missed.
|
|
680
734
|
|
|
735
|
+
### Collecting assistant output
|
|
736
|
+
|
|
737
|
+
`createTranscript()` does that bookkeeping for you. It turns session events into the assistant's output as ordered, JSON-serializable parts:
|
|
738
|
+
|
|
739
|
+
```typescript
|
|
740
|
+
import { createTranscript } from "@bastani/atomic";
|
|
741
|
+
|
|
742
|
+
const transcript = createTranscript();
|
|
743
|
+
session.subscribe((event) => transcript.apply(event));
|
|
744
|
+
|
|
745
|
+
await session.prompt("List the files here");
|
|
746
|
+
console.log(JSON.stringify(transcript.parts()));
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
`parts()` returns a fresh copy of the parts of every assistant message seen so far, including one still streaming:
|
|
750
|
+
|
|
751
|
+
- `{ type: "text", text }`
|
|
752
|
+
- `{ type: "thinking", thinking }`. Redacted thinking is left out.
|
|
753
|
+
- `{ type: "toolCall", id, name, arguments, result? }`. `result` appears once the tool reports progress or finishes. It holds `isError` and `isPartial`, which stays `true` until the tool finishes. If the message is aborted or fails, tool calls without a final result get an error result carrying the error message.
|
|
754
|
+
|
|
755
|
+
Events from tools called inside other tools (those with `parentToolCallId`) are ignored. By default, `result.content` holds the result's text content. To get a tool's structured `details` instead, pass `toolResult`:
|
|
756
|
+
|
|
757
|
+
```typescript
|
|
758
|
+
const transcript = createTranscript({
|
|
759
|
+
toolResult: (toolName) => (toolName === "todo" ? "details" : "content"),
|
|
760
|
+
});
|
|
761
|
+
```
|
|
762
|
+
|
|
681
763
|
## Options Reference
|
|
682
764
|
|
|
683
765
|
Moved to [SDK API reference](/sdk/reference#options-reference).
|
package/docs/security.md
CHANGED
|
@@ -21,7 +21,7 @@ Trusting a project allows Atomic to load trust-gated project inputs, including:
|
|
|
21
21
|
- missing project packages configured through project settings
|
|
22
22
|
- project-local extensions and project package-managed extensions
|
|
23
23
|
|
|
24
|
-
Declining trust skips protected resources, including project-local discovery of `AGENTS.override.md`, `AGENTS.md`, and `CLAUDE.md`. Global context and explicitly supplied CLI resources remain available.
|
|
24
|
+
Declining trust skips protected resources, including project-local discovery of `AGENTS.override.md`, `AGENTS.md`, and `CLAUDE.md`. Global context and explicitly supplied CLI resources remain available. Interactive sessions in an untrusted project show a warning that project resources are ignored; use `/trust` to save a decision, then restart Atomic.
|
|
25
25
|
|
|
26
26
|
Before resolving trust, Atomic loads only user/global extensions and explicit CLI `-e` package-level extensions. Those trusted extensions can handle the `project_trust` event; the first to return a yes/no decision owns it.
|
|
27
27
|
|
|
@@ -1163,13 +1163,22 @@ interface StageSnapshot extends WorkflowSerializableObject {
|
|
|
1163
1163
|
readonly id: string;
|
|
1164
1164
|
readonly name: string;
|
|
1165
1165
|
readonly status: StageStatus;
|
|
1166
|
+
readonly parentIds: readonly string[];
|
|
1167
|
+
readonly executionOrder?: number;
|
|
1168
|
+
readonly model?: string;
|
|
1169
|
+
readonly startedAt?: number;
|
|
1170
|
+
readonly endedAt?: number;
|
|
1171
|
+
readonly durationMs?: number;
|
|
1166
1172
|
readonly result?: WorkflowSerializableValue;
|
|
1167
1173
|
readonly error?: string;
|
|
1174
|
+
readonly routerSelection?: ModelRouterOutput;
|
|
1168
1175
|
}
|
|
1169
1176
|
```
|
|
1170
1177
|
|
|
1171
1178
|
Programmatic `run(...)` returns this type. `exited` identifies `ctx.exit(...)` termination, and `stages` contains the final stage snapshots.
|
|
1172
1179
|
|
|
1180
|
+
Each stage snapshot carries its graph position: `parentIds` lists the stages it depends on, and `executionOrder` is its admission order among the run's stages and tool nodes. `model` is the effective model after fallback resolution; `routerSelection` is the automatic routing choice. Timestamps are epoch milliseconds and are absent until the stage starts or ends. The runtime may replace `parentIds` before a stage starts, so read it from the latest snapshot rather than caching it.
|
|
1181
|
+
|
|
1173
1182
|
## Programmatic usage
|
|
1174
1183
|
|
|
1175
1184
|
`@bastani/atomic/workflows` is Atomic's published workflow SDK. Import `workflow` from that specifier, import `Type` from `typebox`, and export the definition returned by `workflow({...})`. Workflow helpers may also import the host TypeBox runtime subpaths `typebox/compile` and `typebox/value`; the legacy `@sinclair/typebox` root and matching subpaths remain supported. Keep runtime helpers such as widget factories and shared utilities in a subdirectory outside the top-level discovery scan, such as `.atomic/workflows/lib/`; see [Workflow Locations](/workflows/operations#workflow-locations).
|
|
@@ -1275,6 +1284,52 @@ function run<
|
|
|
1275
1284
|
|
|
1276
1285
|
Executes a compiled definition programmatically with validated inputs. Empty-input workflows accept an empty readonly record.
|
|
1277
1286
|
|
|
1287
|
+
#### Stage sessions and adapters
|
|
1288
|
+
|
|
1289
|
+
When `opts.adapters` supplies neither `agentSession` nor `prompt`, `run()` uses `createAgentSessionAdapter({ cwd: opts.cwd })`. Each stage then runs its own in-process Atomic `createAgentSession` session with an in-memory `SessionManager`, unless the stage sets `sessionDir` or resumes or forks a session. Stage sessions resolve models, credentials, settings, and resources the same way `createAgentSession` does. A `prompt` adapter on its own answers stage prompts itself, so `run()` adds no default session adapter beside it.
|
|
1290
|
+
|
|
1291
|
+
```typescript
|
|
1292
|
+
function createAgentSessionAdapter(
|
|
1293
|
+
baseOptions?: Omit<CreateAgentSessionOptions, "sessionManager">,
|
|
1294
|
+
): AgentSessionAdapter;
|
|
1295
|
+
|
|
1296
|
+
interface AgentSessionAdapter {
|
|
1297
|
+
create(
|
|
1298
|
+
options: StageSessionCreateOptions,
|
|
1299
|
+
meta?: StageExecutionMeta,
|
|
1300
|
+
): Promise<StageSessionRuntime | StageSessionCreateResult>;
|
|
1301
|
+
}
|
|
1302
|
+
```
|
|
1303
|
+
|
|
1304
|
+
Call `createAgentSessionAdapter(baseOptions)` yourself to apply the same `createAgentSession` options, such as `agentDir`, `modelRuntime`, or `settingsManager`, to every stage. Options set on an individual stage override `baseOptions`. `baseOptions` cannot set `sessionManager`, because every stage needs its own. A `cwd` in `baseOptions` applies to every stage that doesn't set its own, and session files saved through `sessionDir` record that directory.
|
|
1305
|
+
|
|
1306
|
+
A custom adapter's `create(options, meta)` receives the stage's session options, including any session manager Atomic opened for `sessionDir`, resume, or fork. It must resolve to `{ session }` or to the session itself, where `session` implements `StageSessionRuntime`. To change options per stage while keeping the stage policy that `createAgentSessionAdapter` applies, such as hiding the `workflow` tool from stage sessions, delegate to it:
|
|
1307
|
+
|
|
1308
|
+
```ts
|
|
1309
|
+
import { createAgentSessionAdapter, run } from "@bastani/atomic/workflows";
|
|
1310
|
+
|
|
1311
|
+
// Default: in-process stage sessions with in-memory session managers.
|
|
1312
|
+
await run(helloWorld, { name: "Atomic" }, { cwd: process.cwd() });
|
|
1313
|
+
|
|
1314
|
+
// Shared options for every stage.
|
|
1315
|
+
await run(helloWorld, { name: "Atomic" }, {
|
|
1316
|
+
adapters: { agentSession: createAgentSessionAdapter({ agentDir: "/srv/atomic-agent" }) },
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
// Per-stage options on top of the default adapter.
|
|
1320
|
+
const stageSessions = createAgentSessionAdapter();
|
|
1321
|
+
await run(helloWorld, { name: "Atomic" }, {
|
|
1322
|
+
adapters: {
|
|
1323
|
+
agentSession: {
|
|
1324
|
+
create: (options, meta) =>
|
|
1325
|
+
stageSessions.create(meta?.stageName === "review" ? { ...options, cwd: "/srv/review" } : options, meta),
|
|
1326
|
+
},
|
|
1327
|
+
},
|
|
1328
|
+
});
|
|
1329
|
+
```
|
|
1330
|
+
|
|
1331
|
+
Without an attached workflow UI, a stage that calls `ask_user_question` waits for an answer until the run is aborted through `opts.signal`. For unattended runs, pass `executionMode: "non_interactive"`; stages then run without `ask_user_question`.
|
|
1332
|
+
|
|
1278
1333
|
### `RunOpts`
|
|
1279
1334
|
|
|
1280
1335
|
```typescript
|
|
@@ -1309,6 +1364,7 @@ interface RunOpts {
|
|
|
1309
1364
|
readonly onRunStart?: (snapshot: RunSnapshot) => void;
|
|
1310
1365
|
readonly onStageStart?: (runId: string, snapshot: StageSnapshot) => void;
|
|
1311
1366
|
readonly onStageEnd?: (runId: string, snapshot: StageSnapshot) => unknown;
|
|
1367
|
+
readonly onStageSessionEvent?: (runId: string, stageId: string, event: AgentSessionEvent) => void;
|
|
1312
1368
|
readonly onRunEnd?: (
|
|
1313
1369
|
runId: string,
|
|
1314
1370
|
status: RunStatus,
|
|
@@ -1321,6 +1377,8 @@ interface RunOpts {
|
|
|
1321
1377
|
|
|
1322
1378
|
Supplies runtime adapters, execution policy, persistence, MCP, cancellation, graph/store integration, continuation metadata, and lifecycle callbacks to `run(...)`. Every field is optional.
|
|
1323
1379
|
|
|
1380
|
+
`onStageSessionEvent` streams every agent-session event from each stage (for example `message_update`, `tool_execution_start`, and `tool_execution_end`), tagged with the stage's `runId` and `stageId`. `AgentSessionEvent` is exported by `@bastani/atomic`. The stream continues when a fallback model replaces a stage's session. Stages in nested `ctx.workflow(...)` runs report the child run's `runId`. Exceptions thrown by the listener, and rejections from an async listener, are ignored and do not affect the stage.
|
|
1381
|
+
|
|
1324
1382
|
The public authoring declaration intentionally excludes runtime-only executor fields such as `defaultSessionDir`, `gitWorktreeSetupCache`, `durableBackend`, `durableScope`, and `onStageSession`.
|
|
1325
1383
|
|
|
1326
1384
|
#### Choosing the durable backend
|
|
@@ -1343,11 +1401,10 @@ One process uses one workflow database: DBOS runs a single executor per process,
|
|
|
1343
1401
|
```ts
|
|
1344
1402
|
import { run, WorkflowDurabilityRequiredError } from "@bastani/atomic/workflows";
|
|
1345
1403
|
|
|
1346
|
-
await run(helloWorld, { name: "Atomic" }, {
|
|
1404
|
+
await run(helloWorld, { name: "Atomic" }, { durability: { mode: "memory" } });
|
|
1347
1405
|
|
|
1348
1406
|
try {
|
|
1349
1407
|
await run(helloWorld, { name: "Atomic" }, {
|
|
1350
|
-
adapters,
|
|
1351
1408
|
durability: { mode: "durable", systemDatabaseUrl: process.env.WORKFLOW_DATABASE_URL },
|
|
1352
1409
|
});
|
|
1353
1410
|
} catch (error) {
|
|
@@ -1453,12 +1510,23 @@ interface Store {
|
|
|
1453
1510
|
removeRun(runId: string): boolean;
|
|
1454
1511
|
recordNotice(notice: WorkflowNotice): void;
|
|
1455
1512
|
ackNotice(id: string): boolean;
|
|
1513
|
+
snapshot(): StoreSnapshot;
|
|
1514
|
+
graphSnapshot(): StoreSnapshot;
|
|
1515
|
+
subscribe(listener: (snapshot: StoreSnapshot) => void): () => void;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
interface StoreSnapshot {
|
|
1519
|
+
readonly runs: readonly RunSnapshot[];
|
|
1520
|
+
readonly notices: readonly WorkflowNotice[];
|
|
1521
|
+
readonly version: number;
|
|
1456
1522
|
}
|
|
1457
1523
|
```
|
|
1458
1524
|
|
|
1459
1525
|
`createStore()` returns an isolated workflow state store. `store` is the default singleton exported by the SDK authoring surface.
|
|
1460
1526
|
|
|
1461
|
-
|
|
1527
|
+
`snapshot()` returns a JSON copy of every run and notice; `version` increases on every store change. `graphSnapshot()` returns a cached, size-bounded copy for graph rendering that stays the same object until the store changes. `subscribe(listener)` calls `listener` with a fresh `snapshot()` after each change and returns an unsubscribe function.
|
|
1528
|
+
|
|
1529
|
+
Atomic's runtime store also has prompt, session, and pause/resume methods used by embedded integrations; those controls are not part of the published `Store` contract shown here.
|
|
1462
1530
|
|
|
1463
1531
|
### `createCancellationRegistry()` / `cancellationRegistry`
|
|
1464
1532
|
|
|
@@ -144,6 +144,10 @@ All six can run by name or as nested definitions. Prefer composition over copyin
|
|
|
144
144
|
|
|
145
145
|
Goal keeps the literal objective and immutable acceptance criteria in a durable ledger, delegates implementation, and asks independent reviewers to inspect current changes. Its decision is `complete`, `blocked`, or `needs_human`. Resume retains the run identity and checkpointed progress without repeating completed effects.
|
|
146
146
|
|
|
147
|
+
An orchestrator or reviewer infrastructure failure, including a provider outage or durable checkpoint timeout, leaves the goal ledger `active`. Resolve the reported problem, then use `/workflow resume <run-id>` in the same Atomic session to retry the interrupted stage and finish its turn. You do not need to quit or restart Atomic. Completed stages replay their checkpoints. `needs_human` remains reserved for review outcomes, malformed reviewer decisions, exhausted schema corrections, and exhausted turn budgets. Resume also recovers older synthetic failure verdicts when their diagnostics identify a database or checkpoint interruption; ambiguous old failures and genuine human escalations remain unchanged.
|
|
148
|
+
|
|
149
|
+
Reviewer schema corrections also cover invalid `structured_output` inputs, such as whitespace-only instructions or empty named state. If the reviewer exhausts those corrections, Goal requires human attention rather than retrying the malformed review on resume. If a provider failure occurs during those corrections and no correction succeeds, the goal stays resumable even when a later call has invalid inputs. Provider and database failures remain resumable.
|
|
150
|
+
|
|
147
151
|
Goal reviewers derive checks from the literal objective before consulting implementation receipts, inspect the actual checkout delta, and report commands, observed output, and file:line evidence rather than internal reasoning. Shared contracts cover acceptance-matrix traceability, contract-fidelity risks, end-to-end and QA-video evidence, and independent verification. `stop_review_loop` is the authoritative convergence signal: it remains `false` for P0–P2 findings, any `required_by_objective` finding, or unproven implementation/validation requirements; it becomes `true` only when independent evidence proves the objective and only non-blocking or authorized post-approval work remains. The deterministic reducer consumes that signal without reinterpreting free-form prose, but a `stop_review_loop=true` vote does not count toward the reviewer quorum when the same review reports `overall_correctness: "patch is incorrect"`, `goal_oracle_satisfied: false`, or a blocking finding (P0–P2, or any `required_by_objective` finding). Ralph applies the same check to its reviews.
|
|
148
152
|
Goal and Ralph worker, reviewer and final handoff prompts share [verification and evidence guidance](/workflows/verification): browser, terminal and desktop/simulator routing; environment-aware setup and truthful fallback; qlty initialization or manual offline configuration selected from user/repository priorities; and authorized native GitHub media attachment with hosted-link confirmation. Existing config, read-only tasks and authoritative project checks remain protected. Explicit task-scoped inline/no-workflow steering overrides an existing execution plan and requires safe reconciliation of active work before continuing without duplicate execution.
|
|
149
153
|
Both workflows also share repository-intent mining guidance: implementers and reviewers infer maintainer and requesting-user conventions from repository behavior — git history (including `git log --show-signature`), merged PRs, issues and their comments, review comments, commit subjects and trailers, and CI/branch-protection config — covering norms written docs rarely state, such as commit signing, message style and issue linking, changelog discipline, and review etiquette. The dominant, recent, intentional pattern wins over accidental drift, the requesting user's own activity weighs highest, implementers match the inferred conventions (an unsigned commit in a signed history is a miss, not a preference), and reviewers report deviations as convention findings. Behavioral evidence fills contract gaps; it never overrides the literal objective, acceptance criteria, or explicit `AGENTS.md`/`CLAUDE.md` guidance.
|