thoughtgear 0.1.1 → 0.1.3
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 +177 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -116,8 +116,8 @@ Notes:
|
|
|
116
116
|
|
|
117
117
|
- The `sessionId` is just a string you choose (e.g. a user ID, a chat thread ID, a UUID).
|
|
118
118
|
- Without `sessionId`, each `handlePrompt` is isolated — the model only sees that single prompt and the loop's own tool calls.
|
|
119
|
-
- Session history is loaded via `orm.getSessionHistory(sessionId)`. The in-memory
|
|
120
|
-
- Resume a session in a different process by passing the same `sessionId` and pointing at the same persistent DB (e.g. `db: { type: "
|
|
119
|
+
- Session history is loaded via `orm.getSessionHistory(sessionId)`. The in-memory, files, and S3 adapters fully implement this; the Mongo / SQL adapters are stubs (one-liner query you fill in).
|
|
120
|
+
- Resume a session in a different process by passing the same `sessionId` and pointing at the same persistent DB (e.g. `db: { type: "files", path: "./.thoughtgear" }` or `db: { type: "s3", bucket: "my-bucket" }`).
|
|
121
121
|
|
|
122
122
|
## Streaming callbacks
|
|
123
123
|
|
|
@@ -134,7 +134,7 @@ callbacks: {
|
|
|
134
134
|
|
|
135
135
|
## Persistence: `orm` or `db`
|
|
136
136
|
|
|
137
|
-
`PromptHandler` accepts **either** a pre-built `orm` **or** raw `db` settings. The `db` form is just a shortcut — internally the handler constructs an `ORM` from your `DbConfig` and picks the right adapter (`memory` / `mongodb` / `sql`).
|
|
137
|
+
`PromptHandler` accepts **either** a pre-built `orm` **or** raw `db` settings. The `db` form is just a shortcut — internally the handler constructs an `ORM` from your `DbConfig` and picks the right adapter (`memory` / `files` / `s3` / `mongodb` / `sql`).
|
|
138
138
|
|
|
139
139
|
### Shortcut: pass `db` settings
|
|
140
140
|
|
|
@@ -153,10 +153,69 @@ Other supported `db` shapes:
|
|
|
153
153
|
|
|
154
154
|
```ts
|
|
155
155
|
db: { type: "memory" }
|
|
156
|
+
db: { type: "files", path: "./.thoughtgear" }
|
|
157
|
+
db: { type: "s3", bucket: "my-bucket", path: "thoughtgear/prod", region: "us-east-1" }
|
|
156
158
|
db: { type: "mongodb", uri: "...", database: "..." }
|
|
157
159
|
db: { type: "sql", dialect: "postgres", uri: "..." }
|
|
158
160
|
```
|
|
159
161
|
|
|
162
|
+
### Files adapter (`type: "files"`)
|
|
163
|
+
|
|
164
|
+
Zero-dependency, on-disk JSON persistence — ideal for local development, CLIs, and single-process apps that don't want to stand up a database. Pass a directory and the adapter writes:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
{path}/
|
|
168
|
+
sessions/{sessionId}.json # all messages + run states for the session
|
|
169
|
+
runs/{runId}.json # for runs without a sessionId
|
|
170
|
+
cache.json
|
|
171
|
+
memory.json
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
const handler = new PromptHandler({
|
|
176
|
+
sessionId: "user-42",
|
|
177
|
+
context: "You are a helpful assistant.",
|
|
178
|
+
tools: [],
|
|
179
|
+
model: { name: "gpt-4o-mini", provider: "openai", apiKey: process.env.OPENAI_API_KEY! },
|
|
180
|
+
db: { type: "files", path: "./.thoughtgear" },
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Writes are atomic (write-temp-then-rename) but there is no cross-process locking — concurrent writers to the same session file can race. That's fine for single-process use; reach for `s3` / `mongodb` if you need a multi-writer story.
|
|
185
|
+
|
|
186
|
+
### S3 adapter (`type: "s3"`)
|
|
187
|
+
|
|
188
|
+
Same layout as the files adapter, but keys live in an S3 bucket under an optional prefix:
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
{bucket}/{path}/
|
|
192
|
+
sessions/{sessionId}.json
|
|
193
|
+
runs/{runId}.json
|
|
194
|
+
cache.json
|
|
195
|
+
memory.json
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
const handler = new PromptHandler({
|
|
200
|
+
sessionId: "user-42",
|
|
201
|
+
context: "You are a helpful assistant.",
|
|
202
|
+
tools: [],
|
|
203
|
+
model: { name: "gpt-4o-mini", provider: "openai", apiKey: process.env.OPENAI_API_KEY! },
|
|
204
|
+
db: {
|
|
205
|
+
type: "s3",
|
|
206
|
+
bucket: "my-bucket",
|
|
207
|
+
path: "thoughtgear/prod", // optional key prefix
|
|
208
|
+
region: "us-east-1", // optional — falls back to AWS_REGION env
|
|
209
|
+
credentials: { // optional — omit to use the default credential chain
|
|
210
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
211
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`region` and `credentials` are both optional. When omitted, the AWS SDK's default credential chain (env vars, shared config file, IAM role) is used — typical for apps running on EC2 / ECS / Lambda. Same race caveat as the files adapter applies (S3 has no atomic compare-and-swap).
|
|
218
|
+
|
|
160
219
|
### Bring your own ORM
|
|
161
220
|
|
|
162
221
|
Use this when you want to share one ORM across multiple handlers, or you need to read the transcript back yourself:
|
|
@@ -184,7 +243,121 @@ const history = await orm.getHistory(runId);
|
|
|
184
243
|
const state = await orm.getRunState(runId);
|
|
185
244
|
```
|
|
186
245
|
|
|
187
|
-
|
|
246
|
+
Adapter status:
|
|
247
|
+
- `memory`, `files`, `s3` — fully implemented.
|
|
248
|
+
- `mongodb`, `sql` — stubbed in `src/classes/PromptHandler.ts`; fill in the eight `OrmAdapter` methods using the `mongodb` / `pg` / `kysely` drivers to make them live. Mongo collections used: `messages`, `run_states`, `cache`, `memory`.
|
|
249
|
+
|
|
250
|
+
## Executors
|
|
251
|
+
|
|
252
|
+
Each iteration of the agent loop is **stateless against the ORM** — the run, transcript, and tool results are persisted before the iteration returns. An **Executor** decides *how* the next iteration gets driven. Same loop semantics either way; the choice is operational.
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
interface Executor {
|
|
256
|
+
scheduleNextIteration(runId: string): Promise<void>;
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Two are built in. You pass one via `executor` on the constructor; the default is `LocalExecutor`.
|
|
261
|
+
|
|
262
|
+
### `LocalExecutor` (default)
|
|
263
|
+
|
|
264
|
+
Drives the next iteration in the same process by awaiting `handler.continueRun(runId)`. This is what you want for any single-process app — a script, a server handling a request end-to-end, a CLI, tests.
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
import { PromptHandler, LocalExecutor } from "thoughtgear";
|
|
268
|
+
|
|
269
|
+
const handler = new PromptHandler({
|
|
270
|
+
context: "...",
|
|
271
|
+
tools: [...],
|
|
272
|
+
model: { ... },
|
|
273
|
+
db: { type: "memory" },
|
|
274
|
+
// executor: new LocalExecutor(), // implicit — this is the default
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
await handler.handlePrompt({ text: "..." }); // resolves when the whole run finishes
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
`handlePrompt` / `continueRun` resolve only once the model is done iterating, so callers can `await` the full run.
|
|
281
|
+
|
|
282
|
+
### `LambdaExecutor`
|
|
283
|
+
|
|
284
|
+
Persists state, fires a **fresh invocation** of your Lambda with `{ runId, action: "continue" }`, and returns immediately. The next tick of the loop runs in a new invocation that loads state from the shared ORM.
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
import { PromptHandler, LambdaExecutor, makeLambdaHandler } from "thoughtgear";
|
|
288
|
+
import { LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda";
|
|
289
|
+
|
|
290
|
+
const lambda = new LambdaClient({});
|
|
291
|
+
const executor = new LambdaExecutor(async (payload) => {
|
|
292
|
+
await lambda.send(new InvokeCommand({
|
|
293
|
+
FunctionName: process.env.SELF_FUNCTION_NAME!, // this function's own ARN/name
|
|
294
|
+
InvocationType: "Event", // fire-and-forget
|
|
295
|
+
Payload: Buffer.from(JSON.stringify(payload)),
|
|
296
|
+
}));
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const handler = new PromptHandler({
|
|
300
|
+
context: "...",
|
|
301
|
+
tools: [...],
|
|
302
|
+
model: { ... },
|
|
303
|
+
db: { type: "s3", bucket: "my-bucket", path: "thoughtgear/prod" },
|
|
304
|
+
executor,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
export const lambdaHandler = makeLambdaHandler(handler);
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`makeLambdaHandler` routes events for you:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
type LambdaEvent =
|
|
314
|
+
| { action: "start"; text: string; files?: FileAttachment[] }
|
|
315
|
+
| { action: "continue"; runId: string };
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
So one Lambda function serves both the initial prompt (`action: "start"`) and every continuation tick (`action: "continue"`).
|
|
319
|
+
|
|
320
|
+
Requirements:
|
|
321
|
+
- **Shared persistence.** Use `s3`, `mongodb`, or `sql` — `memory` won't survive an invocation boundary and `files` is single-host.
|
|
322
|
+
- **Self-invoke permission.** The Lambda's IAM role needs `lambda:InvokeFunction` on its own ARN, plus whatever the persistence adapter needs.
|
|
323
|
+
- **Idempotency.** With fire-and-forget invocations, an upstream retry could in theory schedule the same `runId` twice; the ORM has no atomic compare-and-swap on `files`/`s3`. In practice this is rare, but worth knowing if you're at high volume.
|
|
324
|
+
|
|
325
|
+
### When to pick which
|
|
326
|
+
|
|
327
|
+
| Scenario | Executor | Why |
|
|
328
|
+
| --- | --- | --- |
|
|
329
|
+
| Local script, CLI, single-process server | `LocalExecutor` | No infra needed; awaitable end-to-end. |
|
|
330
|
+
| HTTP server returning the final answer in one response | `LocalExecutor` | The request handler awaits the whole loop. |
|
|
331
|
+
| HTTP server returning `runId` immediately, client polls | either | Use `Local` with a background worker, or `Lambda` for serverless. |
|
|
332
|
+
| Long-running agent runs (many tool calls, big chains) | `LambdaExecutor` | Each iteration fits inside one invocation — no 15-min Lambda cap risk. |
|
|
333
|
+
| Bursty workloads, scale-to-zero | `LambdaExecutor` | Pay only for active iterations; no idle worker. |
|
|
334
|
+
| Same code in dev and prod | both | Swap the executor at construction time; everything else stays identical. |
|
|
335
|
+
|
|
336
|
+
### Custom executors
|
|
337
|
+
|
|
338
|
+
Anything that implements `scheduleNextIteration(runId)` works. Useful scenarios:
|
|
339
|
+
|
|
340
|
+
- **Queue-backed worker** — push `{ runId, action: "continue" }` to SQS / Redis / Cloud Tasks; a separate worker pool dequeues and calls `continueRun(runId)`. Buys you backpressure and retries the framework doesn't give you natively.
|
|
341
|
+
- **Cron / scheduled continuation** — schedule the next tick instead of firing it immediately (e.g. to throttle, or wait on an external event).
|
|
342
|
+
- **Cross-region failover** — invoke a Lambda in a different region when the primary is degraded.
|
|
343
|
+
|
|
344
|
+
Skeleton:
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
import { Executor, PromptHandler } from "thoughtgear";
|
|
348
|
+
|
|
349
|
+
class SqsExecutor implements Executor {
|
|
350
|
+
constructor(private queueUrl: string, private sqs: SQSClient) {}
|
|
351
|
+
async scheduleNextIteration(runId: string) {
|
|
352
|
+
await this.sqs.send(new SendMessageCommand({
|
|
353
|
+
QueueUrl: this.queueUrl,
|
|
354
|
+
MessageBody: JSON.stringify({ runId, action: "continue" }),
|
|
355
|
+
}));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Your worker then reads the queue and calls `handler.continueRun(runId)` per message.
|
|
188
361
|
|
|
189
362
|
## Switching providers
|
|
190
363
|
|
package/package.json
CHANGED