dopus 0.2.3 → 0.2.4
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 +48 -2
- package/dist/cli.js +4 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +19 -2
- package/dist/{version-CwDWliFC.js → version-CNF5aJ7W.js} +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -204,9 +204,55 @@ defineAgent({
|
|
|
204
204
|
})
|
|
205
205
|
```
|
|
206
206
|
|
|
207
|
+
## Timeouts and long-running tools
|
|
208
|
+
|
|
209
|
+
An agent turn can take a while: several model calls plus your tools. Two kinds of limits apply, handled differently.
|
|
210
|
+
|
|
211
|
+
**Idle timeouts** (CloudFront, ALB, nginx: usually 60 s without a byte). Handled:
|
|
212
|
+
|
|
213
|
+
- The route streams tokens and tool states as they happen; it never waits for the whole answer.
|
|
214
|
+
- While a slow tool runs, an SSE comment (`: keep-alive`) goes out every 15 s (`keepAlive` option). Clients ignore it.
|
|
215
|
+
- Waiting for an approval holds nothing open. The request ends at the approval card, and the answer starts a new one.
|
|
216
|
+
|
|
217
|
+
**The platform's total limit** for one request (Vercel `maxDuration`, Lambda timeout, Cloud Run `--timeout`). It cannot be
|
|
218
|
+
streamed around, so dopus stays inside it:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
// app/api/agent/route.ts (as generated)
|
|
222
|
+
export const maxDuration = 60
|
|
223
|
+
export const POST = createAgentHandler(agent, { maxDuration })
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
With `maxDuration` passed, once 75% of it has passed the agent starts no more tool calls. It answers with what it has done
|
|
227
|
+
and says what is left, and the user can reply "continue". Without it, a long turn is cut off mid-stream and the chat shows
|
|
228
|
+
an error. A step already running (one model call or one tool) is not interrupted, so keep single tools well under the
|
|
229
|
+
limit. `maxSteps` (default 10) also bounds a turn.
|
|
230
|
+
|
|
231
|
+
**Tools that take longer than a request.** Exports, bulk updates, report generation and the like should not run inside a
|
|
232
|
+
tool call. Split them into two tools:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
startExport: defineTool((input) => startExportJob(input), { // enqueue, return { jobId } at once
|
|
236
|
+
description: 'Starts a CSV export and returns a job id.',
|
|
237
|
+
input: z.object({ from: z.string(), to: z.string() }),
|
|
238
|
+
confirm: true,
|
|
239
|
+
}),
|
|
240
|
+
exportStatus: defineTool((input) => getExportJob(input.jobId), { // { status, url? }
|
|
241
|
+
description: 'Returns the status of an export job and its download link when done.',
|
|
242
|
+
input: z.object({ jobId: z.string() }),
|
|
243
|
+
}),
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The job runs in your queue or background worker, outside the request, so it does not have the request's cookies. Check
|
|
247
|
+
permissions in `startExportJob` and pass the user to the job explicitly (see `identify` and `ctx.user`). Have `getExportJob`
|
|
248
|
+
check that the job belongs to the caller.
|
|
249
|
+
|
|
250
|
+
**If a turn is cut anyway**, tools that already ran stay done. "Retry" asks the model again and may call a tool again.
|
|
251
|
+
Tools behind `confirm: true` need a new approval; make other writes idempotent.
|
|
252
|
+
|
|
207
253
|
## Deploying
|
|
208
254
|
|
|
209
|
-
**Vercel / any Node server.** Nothing extra. The route streams. Raise `maxDuration` in `app/api/agent/route.ts` if your plan allows it.
|
|
255
|
+
**Vercel / any Node server.** Nothing extra. The route streams. Raise `maxDuration` in `app/api/agent/route.ts` if your plan allows it; the time budget follows it.
|
|
210
256
|
|
|
211
257
|
**AWS (OpenNext / SST).**
|
|
212
258
|
|
|
@@ -221,7 +267,7 @@ SST reads OpenNext's streaming flag and sets the Function URL to `RESPONSE_STREA
|
|
|
221
267
|
Things to know:
|
|
222
268
|
|
|
223
269
|
- **Keep-alive.** While a tool runs and nothing else is sent, dopus writes an SSE comment (`: keep-alive`) every 15 s (`keepAlive` option). Clients ignore it. CloudFront, ALB and nginx time out on gaps between packets, not on total time, so a slow tool does not cut the stream.
|
|
224
|
-
- **Turn length.** A turn still has to finish within the Lambda timeout. Behind CloudFront that is 60 s by default (up to 180 s with a quota increase). A bare Function URL allows up to 15 min.
|
|
270
|
+
- **Turn length.** A turn still has to finish within the Lambda timeout. Behind CloudFront that is 60 s by default (up to 180 s with a quota increase). A bare Function URL allows up to 15 min. Keep the route's `maxDuration` equal to the Lambda timeout so the time budget matches.
|
|
225
271
|
- **No API Gateway.** Don't put API Gateway HTTP APIs in front of `/api/agent`. They cap requests at 30 s and buffer the response.
|
|
226
272
|
- **Long-running work.** Work that outlives a request, such as queues and background jobs, loses the request's cookies. Use `identify` and pass `ctx.user` on explicitly.
|
|
227
273
|
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as PROVIDERS, t as VERSION } from "./version-
|
|
2
|
+
import { n as PROVIDERS, t as VERSION } from "./version-CNF5aJ7W.js";
|
|
3
3
|
import { execFileSync, spawn } from "node:child_process";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
import * as p from "@clack/prompts";
|
|
@@ -227,10 +227,11 @@ function routeTemplate(configImport) {
|
|
|
227
227
|
return `import { createAgentHandler } from 'dopus'
|
|
228
228
|
import agent from '${configImport}'
|
|
229
229
|
|
|
230
|
-
//
|
|
230
|
+
// Seconds this route may run (your platform's limit applies). Passing it lets the agent stop calling tools at
|
|
231
|
+
// 75% of it and answer with what it has, instead of being cut off mid-stream.
|
|
231
232
|
export const maxDuration = 60
|
|
232
233
|
|
|
233
|
-
export const POST = createAgentHandler(agent)
|
|
234
|
+
export const POST = createAgentHandler(agent, { maxDuration })
|
|
234
235
|
`;
|
|
235
236
|
}
|
|
236
237
|
// src/init/run.ts
|
package/dist/index.d.ts
CHANGED
|
@@ -131,12 +131,19 @@ interface ClientContext {
|
|
|
131
131
|
locale?: string;
|
|
132
132
|
page?: string;
|
|
133
133
|
}
|
|
134
|
+
interface HandlerOptions {
|
|
135
|
+
/**
|
|
136
|
+
* The route's `maxDuration` in seconds (pass the exported constant). Once 75% of it has passed, the agent
|
|
137
|
+
* starts no further tool calls and answers with what it has, instead of being cut off by the platform.
|
|
138
|
+
*/
|
|
139
|
+
maxDuration?: number;
|
|
140
|
+
}
|
|
134
141
|
/**
|
|
135
142
|
* Creates a `POST` route handler that speaks the AI SDK UI message stream protocol (`useChat`).
|
|
136
143
|
*
|
|
137
144
|
* Tools execute inside this request, so server functions they call see the caller's cookies.
|
|
138
145
|
*/
|
|
139
|
-
export declare function createAgentHandler(config: AgentConfig): (request: Request) => Promise<Response>;
|
|
146
|
+
export declare function createAgentHandler(config: AgentConfig, options?: HandlerOptions): (request: Request) => Promise<Response>;
|
|
140
147
|
/**
|
|
141
148
|
* An error whose message is safe to show to the model (and, through it, the user).
|
|
142
149
|
* Any other error thrown by a tool is reported via `onError` and replaced with a generic message,
|
|
@@ -159,5 +166,5 @@ interface ProviderInfo {
|
|
|
159
166
|
}
|
|
160
167
|
/** Verified against @ai-sdk/openai@4.0.111, @ai-sdk/anthropic@4.0.99, @ai-sdk/google@4.0.114. */
|
|
161
168
|
export declare const PROVIDERS: Record<ProviderId, ProviderInfo>;
|
|
162
|
-
export declare const VERSION = "0.2.
|
|
163
|
-
export type { AgentConfig, AgentUser, ClientContext, ConfirmOption, DopusTool, DopusToolSet, PromptLayers, ProviderId, ProviderInfo, RuntimeContext, ToolContext, ToolOptions };
|
|
169
|
+
export declare const VERSION = "0.2.4";
|
|
170
|
+
export type { AgentConfig, AgentUser, ClientContext, ConfirmOption, DopusTool, DopusToolSet, HandlerOptions, PromptLayers, ProviderId, ProviderInfo, RuntimeContext, ToolContext, ToolOptions };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as PROVIDERS, t as VERSION } from "./version-
|
|
1
|
+
import { n as PROVIDERS, t as VERSION } from "./version-CNF5aJ7W.js";
|
|
2
2
|
import { convertToModelMessages, createUIMessageStreamResponse, isStepCount, safeValidateUIMessages, streamText, toUIMessageStream, tool } from "ai";
|
|
3
3
|
// src/next-errors.ts
|
|
4
4
|
/**
|
|
@@ -220,15 +220,19 @@ function errorResponse(status, message) {
|
|
|
220
220
|
headers: JSON_HEADERS
|
|
221
221
|
});
|
|
222
222
|
}
|
|
223
|
+
/** Share of `maxDuration` after which no new tool calls start; the rest is left for the final answer. */
|
|
224
|
+
const TIME_BUDGET_SHARE = .75;
|
|
225
|
+
const TIME_UP = `Time for this request is almost up. Do not call any more tools. Answer now with what you have done and found so far. If work remains, say what is left so the user can ask you to continue.`;
|
|
223
226
|
/**
|
|
224
227
|
* Creates a `POST` route handler that speaks the AI SDK UI message stream protocol (`useChat`).
|
|
225
228
|
*
|
|
226
229
|
* Tools execute inside this request, so server functions they call see the caller's cookies.
|
|
227
230
|
*/
|
|
228
|
-
function createAgentHandler(config) {
|
|
231
|
+
function createAgentHandler(config, options = {}) {
|
|
229
232
|
const maxSteps = config.maxSteps ?? 10;
|
|
230
233
|
const report = config.onError ?? ((error) => console.error("[dopus]", error));
|
|
231
234
|
return async function handleAgentRequest(request) {
|
|
235
|
+
const deadline = options.maxDuration ? Date.now() + options.maxDuration * 1e3 * TIME_BUDGET_SHARE : void 0;
|
|
232
236
|
const maxBodyBytes = config.limits?.maxBodyBytes ?? 1e6;
|
|
233
237
|
const maxMessageChars = config.limits?.maxMessageChars ?? 8e3;
|
|
234
238
|
if (Number(request.headers.get("content-length") ?? 0) > maxBodyBytes) return errorResponse(413, "Conversation is too long");
|
|
@@ -266,6 +270,19 @@ function createAgentHandler(config) {
|
|
|
266
270
|
tools,
|
|
267
271
|
toolApproval,
|
|
268
272
|
stopWhen: isStepCount(maxSteps),
|
|
273
|
+
prepareStep: ({ stepNumber, instructions }) => {
|
|
274
|
+
if (deadline === void 0 || stepNumber === 0 || Date.now() < deadline) return {};
|
|
275
|
+
return {
|
|
276
|
+
toolChoice: "none",
|
|
277
|
+
instructions: [...(instructions === void 0 ? [] : [instructions].flat()).map((m) => typeof m === "string" ? {
|
|
278
|
+
role: "system",
|
|
279
|
+
content: m
|
|
280
|
+
} : m).filter((m) => m.content !== TIME_UP), {
|
|
281
|
+
role: "system",
|
|
282
|
+
content: TIME_UP
|
|
283
|
+
}]
|
|
284
|
+
};
|
|
285
|
+
},
|
|
269
286
|
abortSignal: request.signal,
|
|
270
287
|
...approvalSecret ? { experimental_toolApprovalSecret: approvalSecret } : {},
|
|
271
288
|
onError: ({ error }) => report(error)
|