dopus 0.2.10 → 0.2.12
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 +28 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +34 -8
- package/dist/{version-CWmiLdVn.js → version-gbic1Mgf.js} +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,6 +142,7 @@ export default defineAgent({
|
|
|
142
142
|
| `input` | **Required.** A zod schema. The model's arguments are validated before `fn` runs. |
|
|
143
143
|
| `confirm` | `true`, or `(input, ctx) => boolean`, to ask the user before each call. Use it for anything that writes. |
|
|
144
144
|
| `output` | `(result) => unknown`. Trims the result before the model sees it, for example to drop internal fields. |
|
|
145
|
+
| `public` | `true` to offer the tool to users who are not signed in too. Needs `identify` (see below). |
|
|
145
146
|
|
|
146
147
|
`ctx` is `{ user, toolCallId, abortSignal }`. `user` is only set when you configure `identify` (see below).
|
|
147
148
|
|
|
@@ -202,6 +203,33 @@ Your part, so an injected instruction cannot cause damage:
|
|
|
202
203
|
|
|
203
204
|
We tested this with a real model in a small task-board app. A member wrote a task whose title ordered the assistant to delete other tasks, then an admin's assistant listed the tasks. Claude (claude-sonnet-5-5) did not even request the deletion and pointed out the planted text. If a model did request it, the approval card would still stop it. Other models were not tested this way.
|
|
204
205
|
|
|
206
|
+
**Signed-in and anonymous users.** Define `identify` and dopus knows who is signed in. When it returns `undefined`, the
|
|
207
|
+
model gets only the tools marked `public: true` (product info, opening hours, "am I signed in?"), and the prompt says
|
|
208
|
+
the rest needs sign-in, so the agent tells the visitor to sign in instead of trying. Member tools are not offered at
|
|
209
|
+
all, so a visitor does not see which features exist. Signed-in users get every tool. An approval for a member tool
|
|
210
|
+
that arrives after the user signed out (in another tab) is not run.
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
defineAgent({
|
|
214
|
+
tools: {
|
|
215
|
+
getOpeningHours: defineTool(getOpeningHours, {
|
|
216
|
+
description: '...',
|
|
217
|
+
input: z.object({}),
|
|
218
|
+
public: true,
|
|
219
|
+
}),
|
|
220
|
+
listMyOrders: defineTool(() => listMyOrders(), { description: '...', input: z.object({}) }),
|
|
221
|
+
},
|
|
222
|
+
identify: async () => {
|
|
223
|
+
const session = await getSession()
|
|
224
|
+
return session ? { id: session.userId } : undefined
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
This decides what the model sees, nothing more: keep the checks in your server functions. Without `identify` dopus
|
|
230
|
+
cannot tell, so every tool is offered to everyone (and `defineAgent` warns if you used `public`). Anonymous users cost
|
|
231
|
+
model tokens too; rate-limit them in `beforeRequest` (see [Audit log, usage and rate limits](#audit-log-usage-and-rate-limits)).
|
|
232
|
+
|
|
205
233
|
**Running tools outside the request.** Background jobs and queues lose the request's cookies. For that case, define `identify(request)` to resolve a serialisable user. Tools then receive it as `ctx.user` and can pass it on explicitly. dopus itself always runs tools inside the request; this is for jobs your tools hand off.
|
|
206
234
|
|
|
207
235
|
## Prompts
|
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-gbic1Mgf.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";
|
package/dist/index.d.ts
CHANGED
|
@@ -66,6 +66,12 @@ interface ToolOptions<SCHEMA extends z.ZodType, RESULT> {
|
|
|
66
66
|
* the job finishes the agent is told, reads the result with another of your tools, and continues.
|
|
67
67
|
*/
|
|
68
68
|
job?: (result: Awaited<RESULT>) => string;
|
|
69
|
+
/**
|
|
70
|
+
* Also offered to users who are not signed in. Needs `identify` in the agent config: when it returns
|
|
71
|
+
* `undefined`, only public tools are given to the model, so features that need an account stay out of sight.
|
|
72
|
+
* This only decides which tools the model sees; keep the permission checks in your server functions.
|
|
73
|
+
*/
|
|
74
|
+
public?: boolean;
|
|
69
75
|
}
|
|
70
76
|
type ToolFunction<INPUT, RESULT> = (input: INPUT, ctx: ToolContext) => RESULT;
|
|
71
77
|
interface DopusTool<SCHEMA extends z.ZodType = z.ZodType, RESULT = unknown> {
|
|
@@ -235,5 +241,5 @@ interface ProviderInfo {
|
|
|
235
241
|
}
|
|
236
242
|
/** Verified against @ai-sdk/openai@4.0.111, @ai-sdk/anthropic@4.0.99, @ai-sdk/google@4.0.114. */
|
|
237
243
|
export declare const PROVIDERS: Record<ProviderId, ProviderInfo>;
|
|
238
|
-
export declare const VERSION = "0.2.
|
|
244
|
+
export declare const VERSION = "0.2.12";
|
|
239
245
|
export type { AgentConfig, AgentUser, ClientContext, ConfirmOption, DopusTool, DopusToolSet, FinishEvent, HandlerOptions, PromptLayers, ProviderId, ProviderInfo, RuntimeContext, ToolCallEvent, 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-gbic1Mgf.js";
|
|
2
2
|
import { convertToModelMessages, createUIMessageStreamResponse, getToolName, isStepCount, isToolUIPart, safeValidateUIMessages, streamText, toUIMessageStream, tool } from "ai";
|
|
3
3
|
// src/job.ts
|
|
4
4
|
/**
|
|
@@ -82,7 +82,8 @@ const CACHE_BREAKPOINT = { anthropic: { cacheControl: { type: "ephemeral" } } };
|
|
|
82
82
|
const RUNTIME_LABELS = {
|
|
83
83
|
locale: "Response language",
|
|
84
84
|
role: "User role",
|
|
85
|
-
page: "Current page (reported by the browser)"
|
|
85
|
+
page: "Current page (reported by the browser)",
|
|
86
|
+
access: "Access"
|
|
86
87
|
};
|
|
87
88
|
/** Picks `ko-KR`, then `ko`, from the locale notes. */
|
|
88
89
|
function pickLocaleNotes(notes, locale) {
|
|
@@ -194,8 +195,27 @@ function defineAgent(config) {
|
|
|
194
195
|
if (!isDopusTool(value)) throw new TypeError(`defineAgent: tools.${name} must be created with defineTool()`);
|
|
195
196
|
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(name)) throw new TypeError(`defineAgent: tool name "${name}" must match [a-zA-Z0-9_-]{1,64}`);
|
|
196
197
|
}
|
|
198
|
+
if (!config.identify && Object.values(config.tools).some((t) => t.options.public)) console.warn("[dopus] `public` tools need `identify` in defineAgent: without it dopus cannot tell who is signed in, so every tool is offered to everyone.");
|
|
197
199
|
return config;
|
|
198
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* The tools this request may use: all of them for a signed-in user, only `public` ones for an anonymous user.
|
|
203
|
+
* Without `identify` dopus cannot tell who is signed in, so every tool is offered (the server functions decide).
|
|
204
|
+
*/
|
|
205
|
+
function visibleTools(config, user) {
|
|
206
|
+
if (!config.identify || user) return {
|
|
207
|
+
tools: config.tools,
|
|
208
|
+
hidden: []
|
|
209
|
+
};
|
|
210
|
+
const tools = {};
|
|
211
|
+
const hidden = [];
|
|
212
|
+
for (const [name, t] of Object.entries(config.tools)) if (t.options.public) tools[name] = t;
|
|
213
|
+
else hidden.push(name);
|
|
214
|
+
return {
|
|
215
|
+
tools,
|
|
216
|
+
hidden
|
|
217
|
+
};
|
|
218
|
+
}
|
|
199
219
|
/** Runs a user hook; a failing hook is reported but never breaks the chat. */
|
|
200
220
|
async function runHook(hook, event, report) {
|
|
201
221
|
if (!hook) return;
|
|
@@ -277,6 +297,10 @@ function readClientContext(body, request) {
|
|
|
277
297
|
page: clean(raw.page, 200)
|
|
278
298
|
};
|
|
279
299
|
}
|
|
300
|
+
/** Reason recorded for a confirmation of a tool that needs sign-in, answered by a user who is not signed in. */
|
|
301
|
+
const SIGN_IN_REASON = "Not run: this needs the user to be signed in, and they are not (anymore). Tell them to sign in first.";
|
|
302
|
+
/** Runtime prompt line for anonymous users when some tools are hidden from them. */
|
|
303
|
+
const SIGNED_OUT_NOTE = "Not signed in. Only features that work without an account are available; if the user asks for something else, tell them to sign in first.";
|
|
280
304
|
/** Reason recorded for a confirmation the conversation moved past (the model reads it). */
|
|
281
305
|
const NOT_RUN_REASON = "Not run: the user sent a new message instead of approving it. If they still want it, call the tool again so they can approve it.";
|
|
282
306
|
/**
|
|
@@ -286,11 +310,11 @@ const NOT_RUN_REASON = "Not run: the user sent a new message instead of approvin
|
|
|
286
310
|
* approved-but-unrun call makes every later request fail with a missing tool result. A stale approval is never
|
|
287
311
|
* executed: the user has moved on, so it may no longer be what they want.
|
|
288
312
|
*/
|
|
289
|
-
function settleStaleApprovals(messages) {
|
|
313
|
+
function settleStaleApprovals(messages, hidden = []) {
|
|
290
314
|
const last = messages.length - 1;
|
|
291
315
|
return messages.map((message, index) => {
|
|
292
|
-
if (message.role !== "assistant"
|
|
293
|
-
const pending = (part) => isToolUIPart(part) && (part.state === "approval-requested" || part.state === "approval-responded");
|
|
316
|
+
if (message.role !== "assistant") return message;
|
|
317
|
+
const pending = (part) => isToolUIPart(part) && (part.state === "approval-requested" || part.state === "approval-responded") && (index !== last || hidden.includes(getToolName(part)));
|
|
294
318
|
if (!message.parts.some(pending)) return message;
|
|
295
319
|
return {
|
|
296
320
|
...message,
|
|
@@ -303,7 +327,7 @@ function settleStaleApprovals(messages) {
|
|
|
303
327
|
approval: {
|
|
304
328
|
id: part.approval.id,
|
|
305
329
|
approved: false,
|
|
306
|
-
reason: declined ? part.approval.reason : NOT_RUN_REASON
|
|
330
|
+
reason: declined ? part.approval.reason : index === last ? SIGN_IN_REASON : NOT_RUN_REASON
|
|
307
331
|
}
|
|
308
332
|
};
|
|
309
333
|
})
|
|
@@ -343,14 +367,15 @@ function createAgentHandler(config, options = {}) {
|
|
|
343
367
|
return errorResponse(400, "Request body must be JSON");
|
|
344
368
|
}
|
|
345
369
|
const user = await config.identify?.(request);
|
|
346
|
-
const
|
|
370
|
+
const visible = visibleTools(config, user);
|
|
371
|
+
const { tools, toolApproval } = buildTools(visible.tools, {
|
|
347
372
|
user,
|
|
348
373
|
report,
|
|
349
374
|
onToolCall: config.onToolCall
|
|
350
375
|
});
|
|
351
376
|
const validated = await safeValidateUIMessages({ messages: body?.messages });
|
|
352
377
|
if (!validated.success) return errorResponse(400, "Invalid messages");
|
|
353
|
-
const messages = settleStaleApprovals(validated.data);
|
|
378
|
+
const messages = settleStaleApprovals(validated.data, visible.hidden);
|
|
354
379
|
if (messages.some((message) => message.role === "user" && message.parts.some((part) => part.type === "text" && part.text.length > maxMessageChars))) return errorResponse(413, "Message is too long");
|
|
355
380
|
if (messages.some((message) => message.parts.some((part) => part.type === "data-dopus-job" && !isJobId(part.data?.id)))) return errorResponse(400, "Invalid job notice");
|
|
356
381
|
const early = await config.beforeRequest?.({
|
|
@@ -372,6 +397,7 @@ function createAgentHandler(config, options = {}) {
|
|
|
372
397
|
const client = readClientContext(body, request);
|
|
373
398
|
const runtime = {
|
|
374
399
|
...client,
|
|
400
|
+
...visible.hidden.length > 0 ? { access: SIGNED_OUT_NOTE } : {},
|
|
375
401
|
...await config.runtime?.(request, {
|
|
376
402
|
user,
|
|
377
403
|
client
|
package/package.json
CHANGED