Skip to content

Commit 0467001

Browse files
Refactor AgentPond architecture and CLI workflows
1 parent a7853bc commit 0467001

70 files changed

Lines changed: 1480 additions & 2936 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/add-files-sdk-storage.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
"@agentpond/files-sdk": minor
33
"@agentpond/core": minor
44
"agentpond": minor
5+
"@agentpond/firebase": minor
6+
"@agentpond/ingest-service": minor
57
---
68

7-
Add Files SDK span export and persistent bucket-backed CLI sync environments.
9+
Consolidate manual remote storage and container ingestion on Files SDK, remove legacy local, S3, and GCS environment formats, and keep platform-native Firebase, Supabase, and Vercel storage.

README.md

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<a href="https://www.npmjs.com/package/agentpond"><img src="https://img.shields.io/node/v/agentpond.svg" alt="Node.js version"></a>
1515
</p>
1616

17-
AgentPond is a lightweight trace backend and CLI for AI applications. It keeps raw traces in object storage you control and syncs them into a local DuckDB cache for fast analysis by your coding agent. Use it with Firebase Storage, Supabase Storage, Amazon S3, Google Cloud Storage, Vercel Blob, or custom infrastructure. Firebase, Supabase, and Vercel projects include automated setup; other deployments use a manual setup path.
17+
AgentPond is a lightweight trace backend and CLI for AI applications. It keeps raw traces in object storage you control and syncs them into a local DuckDB cache for fast analysis by your coding agent. It supports all bucket providers from [Files SDK](https://github.com/haydenbleasel/files-sdk) such as Amazon S3, Google Cloud Storage, R2, and MinIO; For platforms like Firebase, Supabase, and Vercel there are explicit one-command quick starts.
1818

1919
## How it works
2020

@@ -24,21 +24,15 @@ Object storage is the durable source of truth. The local DuckDB database is a re
2424

2525
## Getting started
2626

27-
Start with the [Manual deployment setup](./docs/getting-started/manual-setup.md) to configure object storage, instrument the application, and sync its traces into AgentPond.
27+
Start with the [Manual deployment setup](./docs/getting-started/manual-setup.md) to configure object storage, instrument your application, and sync its traces into AgentPond.
2828

29-
For Firebase, Supabase, and Vercel projects, AgentPond provides automated quick starts:
30-
31-
- [Firebase quick start](./docs/getting-started/firebase.md)
32-
- [Supabase quick start](./docs/getting-started/supabase.md)
33-
- [Vercel quick start](./docs/getting-started/vercel.md)
34-
35-
All require Node.js 22 or newer for the CLI. From your project folder, run:
29+
For [Firebase](./docs/getting-started/firebase.md), [Supabase](./docs/getting-started/supabase.md), and [Vercel](./docs/getting-started/vercel.md) projects, to get started, just run:
3630

3731
```sh
3832
npx agentpond init
3933
```
4034

41-
The command detects the platform, installs AgentPond's instrumentation and analytics skills, and prints a install prompt for your coding agent. Your agent inspects then the application, proposes a setup, implements it after confirmation, and verifies a real trace end to end.
35+
The command installs AgentPond's instrumentation and analytics skills, and prints a install prompt for your coding agent. Your coding agent inspects then the application, proposes a setup, implements it after confirmation, and verifies a real trace end to end.
4236

4337
Once the application has emitted a trace:
4438

@@ -47,26 +41,9 @@ npx agentpond sync
4741
npx agentpond traces list --limit 10
4842
```
4943

50-
## Analyze traces
51-
52-
Use focused commands for individual traces and sessions:
53-
54-
```sh
55-
npx agentpond traces get <trace-id>
56-
npx agentpond observations list --traceId <trace-id>
57-
npx agentpond sessions get <session-id>
58-
npx agentpond scores list --traceId <trace-id>
59-
```
60-
61-
Use DuckDB SQL for aggregation, joins, time windows, or cost analysis:
62-
63-
```sh
64-
npx agentpond sql "select id, name, session_id, total_cost from traces order by start_time desc limit 10"
65-
```
66-
6744
## Features
6845

69-
- Direct OpenTelemetry export to Firebase Storage, Supabase Storage, S3, GCS, and Vercel Blob
46+
- Direct OpenTelemetry export through Files SDK, Firebase Storage, Supabase Storage, and Vercel Blob
7047
- Langfuse-compatible and OTLP HTTP ingestion adapters
7148
- Incremental object-store synchronization
7249
- Local DuckDB projections for traces, observations, sessions, and scores

apps/cli/package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,25 @@
2525
"directory": "apps/cli"
2626
},
2727
"dependencies": {
28-
"@agentpond/aws": "workspace:*",
2928
"@agentpond/core": "workspace:*",
3029
"@agentpond/duckdb": "workspace:*",
3130
"@agentpond/fastify-ingest": "workspace:*",
3231
"@agentpond/firebase": "workspace:*",
3332
"@agentpond/files-sdk": "workspace:*",
34-
"@agentpond/google": "workspace:*",
3533
"@agentpond/supabase": "workspace:*",
3634
"@agentpond/vercel": "workspace:*",
35+
"@aws-sdk/client-s3": "3.1068.0",
36+
"@aws-sdk/s3-presigned-post": "3.1068.0",
37+
"@aws-sdk/s3-request-presigner": "3.1068.0",
3738
"@duckdb/node-api": "^1.5.4-r.1",
39+
"@google-cloud/storage": "^7.21.0",
3840
"@inquirer/prompts": "8.5.2",
41+
"@supabase/storage-js": "2.110.6",
3942
"commander": "15.0.0",
43+
"disk": "^0.8.18",
4044
"fastify": "^5.6.2",
4145
"files-sdk": "2.2.1",
46+
"node-appwrite": "^26.0.0",
4247
"protobufjs": "^7.6.5",
4348
"skills": "1.5.9",
4449
"zod": "^4.3.6"

apps/cli/src/commands/env.ts

Lines changed: 87 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { existsSync } from "node:fs";
22
import {
3-
type AgentPondStoreType,
43
type FilesSdkEnvironmentConfig,
54
initAgentPondEnvironment,
65
listAgentPondEnvironments,
@@ -23,6 +22,7 @@ import {
2322
import {
2423
environmentContextForCommand,
2524
manualEnvironmentContextForCommand,
25+
validateManualEnvironment,
2626
} from "../environment-context.js";
2727
import { providerForCommand } from "../providers.js";
2828

@@ -32,8 +32,6 @@ export type SelectPrompt<T extends string> = (config: {
3232
}) => Promise<T>;
3333

3434
export type SelectEnvironmentPrompt = SelectPrompt<string>;
35-
export type AgentPondInitStore = AgentPondStoreType;
36-
export type SelectStorePrompt = SelectPrompt<AgentPondInitStore>;
3735
export type SelectFilesProviderPrompt = SelectPrompt<string>;
3836
export type InputPrompt = (config: {
3937
message: string;
@@ -42,25 +40,28 @@ export type InputPrompt = (config: {
4240

4341
type EnvOptions = GlobalOptions & {
4442
bucket?: string;
43+
endpoint?: string;
4544
langfuse?: boolean;
4645
otel?: boolean;
4746
provider?: string;
48-
store?: string;
47+
region?: string;
4948
};
5049

5150
export function registerEnvCommand(
5251
program: Command,
5352
options: {
5453
selectEnvironment?: SelectEnvironmentPrompt;
5554
selectFilesProvider?: SelectFilesProviderPrompt;
56-
selectStore?: SelectStorePrompt;
5755
inputBucket?: InputPrompt;
56+
inputEndpoint?: InputPrompt;
57+
inputRegion?: InputPrompt;
5858
} = {},
5959
): void {
6060
const promptSelect = options.selectEnvironment ?? select<string>;
61-
const promptStore = options.selectStore ?? select<AgentPondInitStore>;
6261
const promptFilesProvider = options.selectFilesProvider ?? select<string>;
6362
const promptBucket = options.inputBucket ?? input;
63+
const promptEndpoint = options.inputEndpoint ?? input;
64+
const promptRegion = options.inputRegion ?? input;
6465
const env = addGlobalOptions(
6566
program.command("env").description("select and manage environments"),
6667
);
@@ -108,44 +109,51 @@ export function registerEnvCommand(
108109
name,
109110
selected: name === selected,
110111
}));
112+
for (const name of new Set([selected, ...rows.map(({ name }) => name)])) {
113+
validateManualEnvironment(cwd, name);
114+
}
111115
return print(rows, Boolean(globalOptions.json));
112116
});
113117

114118
addGlobalOptions(env.command("init <name>"))
115119
.description("initialize a manual environment")
116-
.option("--store <store>", "object store: files-sdk, s3, gcs, or local")
117120
.option("--provider <provider>", "Files SDK bucket provider")
118121
.option("--bucket <bucket>", "Files SDK bucket name")
122+
.option("--endpoint <endpoint>", "Files SDK provider endpoint")
123+
.option("--region <region>", "Files SDK provider region")
119124
.action(
120125
async (name: string, commandOptions: EnvOptions, command: Command) => {
126+
if (name === "dev") {
127+
throw new CliError(
128+
'The "dev" environment is managed by `npx agentpond dev`',
129+
);
130+
}
121131
const globalOptions = command.optsWithGlobals<GlobalOptions>();
122132
const context = manualEnvironmentContextForCommand("init", {
123133
envName: name,
124134
platform: globalOptions.platform,
125135
});
126-
const store =
127-
storeFromValue(commandOptions.store) ??
128-
(await promptForStore(promptStore));
129-
const filesSdk = await filesSdkConfigForStore(
130-
store,
136+
const filesSdk = await filesSdkConfigForOptions(
131137
commandOptions,
132138
promptFilesProvider,
133139
promptBucket,
140+
promptEndpoint,
141+
promptRegion,
134142
);
135143
const environment = initAgentPondEnvironment(name, {
136144
cwd: context.rootDir,
137-
storeType: store,
138-
filesSdk: filesSdk?.config,
145+
filesSdk,
139146
});
147+
validateManualEnvironment(context.rootDir, environment.name);
140148
return print(
141149
{
142-
bucket: filesSdk?.config.bucket,
150+
bucket: filesSdk.bucket,
143151
name: environment.name,
144152
envFile: environment.envFilePath,
145153
dbPath: environment.dbPath,
146-
peerDependencies: filesSdk?.peerDependencies,
147-
provider: filesSdk?.config.provider,
148-
store,
154+
provider: filesSdk.provider,
155+
...(filesSdk.endpoint ? { endpoint: filesSdk.endpoint } : {}),
156+
...(filesSdk.region ? { region: filesSdk.region } : {}),
149157
},
150158
Boolean(globalOptions.json),
151159
);
@@ -194,64 +202,19 @@ async function selectEnvironmentForCommand(
194202
});
195203
const selectedName =
196204
name ?? (await promptForEnvironmentName(promptSelect, context.rootDir));
205+
validateManualEnvironment(context.rootDir, selectedName);
197206
return selectAgentPondEnvironment(selectedName, {
198207
cwd: context.rootDir,
199208
}).name;
200209
}
201210

202-
function storeFromValue(
203-
value: string | undefined,
204-
): AgentPondInitStore | undefined {
205-
if (value === undefined) return undefined;
206-
if (
207-
value === "files-sdk" ||
208-
value === "s3" ||
209-
value === "gcs" ||
210-
value === "local"
211-
) {
212-
return value;
213-
}
214-
throw new CliError(
215-
`--store must be files-sdk, s3, gcs, or local, got "${value}"`,
216-
);
217-
}
218-
219-
async function promptForStore(
220-
promptSelect: SelectStorePrompt,
221-
): Promise<AgentPondInitStore> {
222-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
223-
throw new CliError("Missing --store");
224-
}
225-
return promptSelect({
226-
message: "Select AgentPond object store",
227-
choices: [
228-
{ name: "Files SDK bucket provider", value: "files-sdk" },
229-
{ name: "AWS S3 (or compatible)", value: "s3" },
230-
{ name: "Google Cloud Storage (GCS)", value: "gcs" },
231-
{ name: "Local filesystem", value: "local" },
232-
],
233-
});
234-
}
235-
236-
async function filesSdkConfigForStore(
237-
store: AgentPondInitStore,
238-
options: Pick<EnvOptions, "bucket" | "provider">,
211+
async function filesSdkConfigForOptions(
212+
options: Pick<EnvOptions, "bucket" | "endpoint" | "provider" | "region">,
239213
promptProvider: SelectFilesProviderPrompt,
240214
promptBucket: InputPrompt,
241-
): Promise<
242-
| {
243-
config: FilesSdkEnvironmentConfig;
244-
peerDependencies: readonly string[];
245-
}
246-
| undefined
247-
> {
248-
if (store !== "files-sdk") {
249-
if (options.provider !== undefined || options.bucket !== undefined) {
250-
throw new CliError("--provider and --bucket require --store files-sdk");
251-
}
252-
return undefined;
253-
}
254-
215+
promptEndpoint: InputPrompt,
216+
promptRegion: InputPrompt,
217+
): Promise<FilesSdkEnvironmentConfig> {
255218
const provider =
256219
options.provider ?? (await promptForFilesProvider(promptProvider));
257220
const definition = bucketProvider(provider);
@@ -260,9 +223,23 @@ async function filesSdkConfigForStore(
260223
).trim();
261224
if (!bucket) throw new CliError("Missing --bucket");
262225

226+
const endpoint = await configuredProviderValue(
227+
"endpoint",
228+
definition.env.config?.includes("endpoint") ?? false,
229+
options.endpoint,
230+
promptEndpoint,
231+
);
232+
const region = await configuredProviderValue(
233+
"region",
234+
definition.env.config?.includes("region") ?? false,
235+
options.region,
236+
promptRegion,
237+
);
263238
return {
264-
config: { provider, bucket },
265-
peerDependencies: definition.peerDeps,
239+
provider,
240+
bucket,
241+
...(endpoint ? { endpoint } : {}),
242+
...(region ? { region } : {}),
266243
};
267244
}
268245

@@ -305,16 +282,54 @@ function bucketProvider(provider: string) {
305282
`Files SDK provider "${provider}" is not bucket-backed and is not supported by AgentPond`,
306283
);
307284
}
285+
if (provider === "bun-s3") {
286+
throw new CliError(
287+
'Files SDK provider "bun-s3" is not supported by AgentPond\'s Node.js runtime',
288+
);
289+
}
290+
for (const field of definition.env.config) {
291+
if (field !== "bucket" && field !== "endpoint" && field !== "region") {
292+
throw new CliError(
293+
`Files SDK provider "${provider}" requires unsupported configuration field "${field}"`,
294+
);
295+
}
296+
}
308297
return definition;
309298
}
310299

311300
function bucketProviders() {
312301
return PROVIDER_NAMES.flatMap((provider) => {
313302
const definition = getProvider(provider);
314-
return definition?.env.config?.includes("bucket") ? [definition] : [];
303+
return provider !== "bun-s3" &&
304+
definition?.env.config?.includes("bucket") &&
305+
definition.env.config.every(
306+
(field) =>
307+
field === "bucket" || field === "endpoint" || field === "region",
308+
)
309+
? [definition]
310+
: [];
315311
});
316312
}
317313

314+
async function configuredProviderValue(
315+
field: "endpoint" | "region",
316+
required: boolean,
317+
value: string | undefined,
318+
promptInput: InputPrompt,
319+
): Promise<string | undefined> {
320+
const configured = value?.trim();
321+
if (configured) return configured;
322+
if (!required) return undefined;
323+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
324+
throw new CliError(`Missing --${field}`);
325+
}
326+
const prompted = (
327+
await promptInput({ message: `Files SDK ${field}` })
328+
).trim();
329+
if (!prompted) throw new CliError(`Missing --${field}`);
330+
return prompted;
331+
}
332+
318333
async function promptForEnvironmentName(
319334
promptSelect: SelectEnvironmentPrompt,
320335
cwd: string,

apps/cli/src/commands/scores.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export async function createScore(
140140
},
141141
};
142142

143-
if (context.usesAgentPondDevServer) {
143+
if (context.kind === "dev") {
144144
const result = await new DuckDbIngestionSink(config.dbPath).writeEvents({
145145
projectId: config.projectId,
146146
events: [event],

apps/cli/src/commands/sync.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function registerSyncCommand(program: Command): void {
1414
const { context, json } = commandContext(
1515
command.optsWithGlobals<GlobalOptions>(),
1616
);
17-
if (context.usesAgentPondDevServer) {
17+
if (context.kind === "dev") {
1818
return print(
1919
{
2020
skipped: true,

apps/cli/src/commands/traces.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export async function createTrace(
113113
const now = new Date().toISOString();
114114
const traceId = stringFlag(options, "id") ?? createOtelTraceId();
115115
const resourceSpans = manualTraceResourceSpans(options, traceId, now);
116-
if (context.usesAgentPondDevServer) {
116+
if (context.kind === "dev") {
117117
const result = await new DuckDbIngestionSink(
118118
config.dbPath,
119119
).writeOtelResourceSpans({

0 commit comments

Comments
 (0)