thoughtgear 0.1.2 → 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 +112 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -247,6 +247,118 @@ Adapter status:
|
|
|
247
247
|
- `memory`, `files`, `s3` — fully implemented.
|
|
248
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
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.
|
|
361
|
+
|
|
250
362
|
## Switching providers
|
|
251
363
|
|
|
252
364
|
Just change `model.provider`:
|
package/package.json
CHANGED