@ai-sdk/workflow 1.0.0-canary.85 → 1.0.0-canary.87

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/workflow",
3
- "version": "1.0.0-canary.85",
3
+ "version": "1.0.0-canary.87",
4
4
  "description": "WorkflowAgent for building AI agents with AI SDK",
5
5
  "license": "Apache-2.0",
6
6
  "main": "./dist/index.js",
@@ -28,8 +28,8 @@
28
28
  "dependencies": {
29
29
  "ajv": "^8.20.0",
30
30
  "@ai-sdk/provider": "4.0.0-canary.18",
31
- "@ai-sdk/provider-utils": "5.0.0-canary.46",
32
- "ai": "7.0.0-canary.168"
31
+ "@ai-sdk/provider-utils": "5.0.0-canary.47",
32
+ "ai": "7.0.0-canary.170"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/node": "22.19.19",
@@ -35,10 +35,12 @@ import {
35
35
  } from 'ai';
36
36
  import {
37
37
  createRestrictedTelemetryDispatcher,
38
+ collectToolApprovals,
38
39
  convertToLanguageModelPrompt,
39
40
  mergeAbortSignals,
40
41
  mergeCallbacks,
41
42
  standardizePrompt,
43
+ validateApprovedToolApprovals,
42
44
  } from 'ai/internal';
43
45
  import { createLanguageModelToolResultOutput } from './create-language-model-tool-result-output.js';
44
46
  import { streamTextIterator } from './stream-text-iterator.js';
@@ -1351,8 +1353,30 @@ export class WorkflowAgent<
1351
1353
  // This mirrors how stream-text.ts handles tool-approval-response parts:
1352
1354
  // approved tools are executed, denied tools get denial results, and
1353
1355
  // approval parts are stripped from the messages.
1354
- const { approvedToolApprovals, deniedToolApprovals } =
1355
- collectToolApprovalsFromMessages(prompt.messages);
1356
+ // Use the AI SDK core collector so this path cannot drift from the
1357
+ // hardened generateText/streamText implementation. The collected approvals
1358
+ // are mapped to the flat shape used below; the original (nested) approval
1359
+ // is carried on `collected` for re-validation.
1360
+ const collectedApprovals = collectToolApprovals<ToolSet>({
1361
+ messages: prompt.messages,
1362
+ });
1363
+ const approvedToolApprovals = collectedApprovals.approvedToolApprovals.map(
1364
+ collected => ({
1365
+ toolCallId: collected.toolCall.toolCallId,
1366
+ toolName: collected.toolCall.toolName,
1367
+ input: collected.toolCall.input,
1368
+ reason: collected.approvalResponse.reason,
1369
+ collected,
1370
+ }),
1371
+ );
1372
+ const deniedToolApprovals = collectedApprovals.deniedToolApprovals.map(
1373
+ collected => ({
1374
+ toolCallId: collected.toolCall.toolCallId,
1375
+ toolName: collected.toolCall.toolName,
1376
+ input: collected.toolCall.input,
1377
+ reason: collected.approvalResponse.reason,
1378
+ }),
1379
+ );
1356
1380
 
1357
1381
  if (approvedToolApprovals.length > 0 || deniedToolApprovals.length > 0) {
1358
1382
  const _toolResultMessages: ModelMessage[] = [];
@@ -1368,6 +1392,70 @@ export class WorkflowAgent<
1368
1392
  for (const approval of approvedToolApprovals) {
1369
1393
  const tool = (this.tools as ToolSet)[approval.toolName];
1370
1394
  if (tool && typeof tool.execute === 'function') {
1395
+ if (!tool.needsApproval) {
1396
+ const reason = `Tool "${approval.toolName}" does not require approval`;
1397
+ toolResultContent.push({
1398
+ type: 'tool-result' as const,
1399
+ toolCallId: approval.toolCallId,
1400
+ toolName: approval.toolName,
1401
+ output: await createLanguageModelToolResultOutput({
1402
+ toolCallId: approval.toolCallId,
1403
+ toolName: approval.toolName,
1404
+ input: approval.input,
1405
+ output: reason,
1406
+ tool,
1407
+ errorMode: 'text',
1408
+ supportedUrls: {},
1409
+ download,
1410
+ }),
1411
+ });
1412
+ continue;
1413
+ }
1414
+
1415
+ // Re-validate through the shared core implementation: input schema,
1416
+ // HMAC signature (when configured), and approval policy. It throws on
1417
+ // invalid input/signature; convert that to a denial result so the
1418
+ // agent loop can continue gracefully.
1419
+ let revalidationReason: string | undefined;
1420
+ try {
1421
+ const { deniedToolApprovals: policyDenied } =
1422
+ await validateApprovedToolApprovals({
1423
+ approvedToolApprovals: [approval.collected],
1424
+ tools: this.tools as ToolSet,
1425
+ toolApproval: undefined,
1426
+ messages: prompt.messages,
1427
+ toolsContext:
1428
+ effectiveToolsContext as InferToolSetContext<ToolSet>,
1429
+ runtimeContext: effectiveRuntimeContext,
1430
+ });
1431
+ if (policyDenied.length > 0) {
1432
+ revalidationReason =
1433
+ policyDenied[0].approvalResponse.reason ??
1434
+ 'Tool approval denied';
1435
+ }
1436
+ } catch (error) {
1437
+ revalidationReason = getErrorMessage(error);
1438
+ }
1439
+
1440
+ if (revalidationReason != null) {
1441
+ toolResultContent.push({
1442
+ type: 'tool-result' as const,
1443
+ toolCallId: approval.toolCallId,
1444
+ toolName: approval.toolName,
1445
+ output: await createLanguageModelToolResultOutput({
1446
+ toolCallId: approval.toolCallId,
1447
+ toolName: approval.toolName,
1448
+ input: approval.input,
1449
+ output: revalidationReason,
1450
+ tool,
1451
+ errorMode: 'text',
1452
+ supportedUrls: {},
1453
+ download,
1454
+ }),
1455
+ });
1456
+ continue;
1457
+ }
1458
+
1371
1459
  try {
1372
1460
  const { execute } = tool;
1373
1461
  const resolvedContext = await resolveToolContext({
@@ -2689,113 +2777,3 @@ async function executeTool(
2689
2777
  isError: false,
2690
2778
  };
2691
2779
  }
2692
-
2693
- /**
2694
- * Collected tool approval information for a single tool call.
2695
- */
2696
- interface CollectedApproval {
2697
- toolCallId: string;
2698
- toolName: string;
2699
- input: unknown;
2700
- approvalId: string;
2701
- reason?: string;
2702
- }
2703
-
2704
- /**
2705
- * Collect tool approval responses from model messages.
2706
- * Mirrors the logic from `collectToolApprovals` in the AI SDK core
2707
- * (`packages/ai/src/generate-text/collect-tool-approvals.ts`).
2708
- *
2709
- * Scans the last tool message for `tool-approval-response` parts,
2710
- * matches them with `tool-approval-request` parts in assistant messages
2711
- * and the corresponding `tool-call` parts.
2712
- */
2713
- function collectToolApprovalsFromMessages(messages: ModelMessage[]): {
2714
- approvedToolApprovals: CollectedApproval[];
2715
- deniedToolApprovals: CollectedApproval[];
2716
- } {
2717
- const lastMessage = messages.at(-1);
2718
-
2719
- if (lastMessage?.role !== 'tool') {
2720
- return { approvedToolApprovals: [], deniedToolApprovals: [] };
2721
- }
2722
-
2723
- // Gather tool calls from assistant messages
2724
- const toolCallsByToolCallId: Record<
2725
- string,
2726
- { toolName: string; input: unknown }
2727
- > = {};
2728
- for (const message of messages) {
2729
- if (message.role === 'assistant' && Array.isArray(message.content)) {
2730
- for (const part of message.content as any[]) {
2731
- if (part.type === 'tool-call') {
2732
- toolCallsByToolCallId[part.toolCallId] = {
2733
- toolName: part.toolName,
2734
- input: part.input ?? part.args,
2735
- };
2736
- }
2737
- }
2738
- }
2739
- }
2740
-
2741
- // Gather approval requests from assistant messages
2742
- const approvalRequestsByApprovalId: Record<
2743
- string,
2744
- { approvalId: string; toolCallId: string }
2745
- > = {};
2746
- for (const message of messages) {
2747
- if (message.role === 'assistant' && Array.isArray(message.content)) {
2748
- for (const part of message.content as any[]) {
2749
- if (part.type === 'tool-approval-request') {
2750
- approvalRequestsByApprovalId[part.approvalId] = {
2751
- approvalId: part.approvalId,
2752
- toolCallId: part.toolCallId,
2753
- };
2754
- }
2755
- }
2756
- }
2757
- }
2758
-
2759
- // Gather existing tool results to avoid re-executing
2760
- const existingToolResults = new Set<string>();
2761
- for (const part of lastMessage.content as any[]) {
2762
- if (part.type === 'tool-result') {
2763
- existingToolResults.add(part.toolCallId);
2764
- }
2765
- }
2766
-
2767
- const approvedToolApprovals: CollectedApproval[] = [];
2768
- const deniedToolApprovals: CollectedApproval[] = [];
2769
-
2770
- // Collect approval responses from the last tool message
2771
- const approvalResponses = (lastMessage.content as any[]).filter(
2772
- (part: any) => part.type === 'tool-approval-response',
2773
- );
2774
-
2775
- for (const response of approvalResponses) {
2776
- const approvalRequest = approvalRequestsByApprovalId[response.approvalId];
2777
- if (approvalRequest == null) continue;
2778
-
2779
- // Skip if there's already a tool result for this tool call
2780
- if (existingToolResults.has(approvalRequest.toolCallId)) continue;
2781
-
2782
- const toolCall = toolCallsByToolCallId[approvalRequest.toolCallId];
2783
- if (toolCall == null) continue;
2784
-
2785
- const approval: CollectedApproval = {
2786
- toolCallId: approvalRequest.toolCallId,
2787
- toolName: toolCall.toolName,
2788
- input: toolCall.input,
2789
- approvalId: response.approvalId,
2790
- reason: response.reason,
2791
- };
2792
-
2793
- if (response.approved) {
2794
- approvedToolApprovals.push(approval);
2795
- } else {
2796
- deniedToolApprovals.push(approval);
2797
- }
2798
- }
2799
-
2800
- return { approvedToolApprovals, deniedToolApprovals };
2801
- }