@letta-ai/letta-code 0.21.8 → 0.21.9

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.
Files changed (2) hide show
  1. package/letta.js +273 -115
  2. package/package.json +1 -1
package/letta.js CHANGED
@@ -3269,7 +3269,7 @@ var package_default;
3269
3269
  var init_package = __esm(() => {
3270
3270
  package_default = {
3271
3271
  name: "@letta-ai/letta-code",
3272
- version: "0.21.8",
3272
+ version: "0.21.9",
3273
3273
  description: "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
3274
3274
  type: "module",
3275
3275
  bin: {
@@ -77004,8 +77004,8 @@ function isValidApprovalResponseBody(value) {
77004
77004
  if (decision.behavior === "allow") {
77005
77005
  const hasMessage = decision.message === undefined || typeof decision.message === "string";
77006
77006
  const hasUpdatedInput = decision.updated_input === undefined || decision.updated_input === null || typeof decision.updated_input === "object";
77007
- const hasUpdatedPermissions = decision.updated_permissions === undefined || Array.isArray(decision.updated_permissions) && decision.updated_permissions.every((entry) => typeof entry === "string");
77008
- return hasMessage && hasUpdatedInput && hasUpdatedPermissions;
77007
+ const hasSelectedPermissionSuggestionIds = decision.selected_permission_suggestion_ids === undefined || Array.isArray(decision.selected_permission_suggestion_ids) && decision.selected_permission_suggestion_ids.every((entry) => typeof entry === "string");
77008
+ return hasMessage && hasUpdatedInput && hasSelectedPermissionSuggestionIds;
77009
77009
  }
77010
77010
  if (decision.behavior === "deny") {
77011
77011
  return typeof decision.message === "string";
@@ -77565,99 +77565,6 @@ var init_permissionMode = __esm(() => {
77565
77565
  init_remote_settings();
77566
77566
  });
77567
77567
 
77568
- // src/cli/helpers/safeJsonParse.ts
77569
- function safeJsonParse(json) {
77570
- try {
77571
- const data = JSON.parse(json);
77572
- return { success: true, data };
77573
- } catch (error) {
77574
- return {
77575
- success: false,
77576
- error: error instanceof Error ? error.message : String(error)
77577
- };
77578
- }
77579
- }
77580
- function safeJsonParseOr(json, defaultValue) {
77581
- const result = safeJsonParse(json);
77582
- return result.success ? result.data : defaultValue;
77583
- }
77584
-
77585
- // src/cli/helpers/approvalClassification.ts
77586
- async function getMissingRequiredArgs(toolName, parsedArgs) {
77587
- const schema = getToolSchema(toolName);
77588
- const required = schema?.input_schema?.required || [];
77589
- return required.filter((key) => !(key in parsedArgs) || parsedArgs[key] == null);
77590
- }
77591
- async function classifyApprovals(approvals, opts = {}) {
77592
- const needsUserInput = [];
77593
- const autoAllowed = [];
77594
- const autoDenied = [];
77595
- const denyReasonForAsk = opts.denyReasonForAsk ?? "Tool requires approval (headless mode)";
77596
- const missingNameReason = opts.missingNameReason ?? "Tool call incomplete - missing name";
77597
- for (const approval of approvals) {
77598
- const toolName = approval.toolName;
77599
- if (!toolName) {
77600
- autoDenied.push({
77601
- approval,
77602
- permission: { decision: "deny", reason: missingNameReason },
77603
- context: null,
77604
- parsedArgs: {},
77605
- denyReason: missingNameReason
77606
- });
77607
- continue;
77608
- }
77609
- const parsedArgs = safeJsonParseOr(approval.toolArgs || "{}", {});
77610
- const permission = await checkToolPermission(toolName, parsedArgs, opts.workingDirectory, opts.permissionModeState);
77611
- const context3 = opts.getContext ? await opts.getContext(toolName, parsedArgs, opts.workingDirectory) : null;
77612
- let decision = permission.decision;
77613
- if (opts.alwaysRequiresUserInput?.(toolName) && decision === "allow") {
77614
- decision = "ask";
77615
- }
77616
- if (decision === "ask" && opts.treatAskAsDeny) {
77617
- autoDenied.push({
77618
- approval,
77619
- permission,
77620
- context: context3,
77621
- parsedArgs,
77622
- denyReason: denyReasonForAsk
77623
- });
77624
- continue;
77625
- }
77626
- if (decision === "allow" && opts.requireArgsForAutoApprove) {
77627
- const missingRequiredArgs = await getMissingRequiredArgs(toolName, parsedArgs);
77628
- if (missingRequiredArgs.length > 0) {
77629
- const denyReason = opts.missingArgsReason ? opts.missingArgsReason(missingRequiredArgs) : `Missing required parameter${missingRequiredArgs.length > 1 ? "s" : ""}: ${missingRequiredArgs.join(", ")}`;
77630
- autoDenied.push({
77631
- approval,
77632
- permission,
77633
- context: context3,
77634
- parsedArgs,
77635
- missingRequiredArgs,
77636
- denyReason
77637
- });
77638
- continue;
77639
- }
77640
- }
77641
- const entry = {
77642
- approval,
77643
- permission,
77644
- context: context3,
77645
- parsedArgs
77646
- };
77647
- if (decision === "ask") {
77648
- needsUserInput.push(entry);
77649
- } else if (decision === "deny") {
77650
- autoDenied.push(entry);
77651
- } else {
77652
- autoAllowed.push(entry);
77653
- }
77654
- }
77655
- return { needsUserInput, autoAllowed, autoDenied };
77656
- }
77657
- var init_approvalClassification = __esm(async () => {
77658
- await init_manager3();
77659
- });
77660
-
77661
77568
  // node_modules/diff/libesm/diff/base.js
77662
77569
  class Diff {
77663
77570
  diff(oldStr, newStr, options = {}) {
@@ -79017,6 +78924,162 @@ async function computeDiffPreviews(toolName, toolArgs, workingDirectory = proces
79017
78924
  var cachedDiffDeps = null;
79018
78925
  var init_diffPreview = () => {};
79019
78926
 
78927
+ // src/cli/helpers/safeJsonParse.ts
78928
+ function safeJsonParse(json) {
78929
+ try {
78930
+ const data = JSON.parse(json);
78931
+ return { success: true, data };
78932
+ } catch (error) {
78933
+ return {
78934
+ success: false,
78935
+ error: error instanceof Error ? error.message : String(error)
78936
+ };
78937
+ }
78938
+ }
78939
+ function safeJsonParseOr(json, defaultValue) {
78940
+ const result = safeJsonParse(json);
78941
+ return result.success ? result.data : defaultValue;
78942
+ }
78943
+
78944
+ // src/cli/helpers/approvalClassification.ts
78945
+ async function getMissingRequiredArgs(toolName, parsedArgs) {
78946
+ const schema = getToolSchema(toolName);
78947
+ const required = schema?.input_schema?.required || [];
78948
+ return required.filter((key) => !(key in parsedArgs) || parsedArgs[key] == null);
78949
+ }
78950
+ async function classifyApprovals(approvals, opts = {}) {
78951
+ const needsUserInput = [];
78952
+ const autoAllowed = [];
78953
+ const autoDenied = [];
78954
+ const denyReasonForAsk = opts.denyReasonForAsk ?? "Tool requires approval (headless mode)";
78955
+ const missingNameReason = opts.missingNameReason ?? "Tool call incomplete - missing name";
78956
+ for (const approval of approvals) {
78957
+ const toolName = approval.toolName;
78958
+ if (!toolName) {
78959
+ autoDenied.push({
78960
+ approval,
78961
+ permission: { decision: "deny", reason: missingNameReason },
78962
+ context: null,
78963
+ parsedArgs: {},
78964
+ denyReason: missingNameReason
78965
+ });
78966
+ continue;
78967
+ }
78968
+ const parsedArgs = safeJsonParseOr(approval.toolArgs || "{}", {});
78969
+ const permission = await checkToolPermission(toolName, parsedArgs, opts.workingDirectory, opts.permissionModeState);
78970
+ const context3 = opts.getContext ? await opts.getContext(toolName, parsedArgs, opts.workingDirectory) : null;
78971
+ let decision = permission.decision;
78972
+ if (opts.alwaysRequiresUserInput?.(toolName) && decision === "allow") {
78973
+ decision = "ask";
78974
+ }
78975
+ if (decision === "ask" && opts.treatAskAsDeny) {
78976
+ autoDenied.push({
78977
+ approval,
78978
+ permission,
78979
+ context: context3,
78980
+ parsedArgs,
78981
+ denyReason: denyReasonForAsk
78982
+ });
78983
+ continue;
78984
+ }
78985
+ if (decision === "allow" && opts.requireArgsForAutoApprove) {
78986
+ const missingRequiredArgs = await getMissingRequiredArgs(toolName, parsedArgs);
78987
+ if (missingRequiredArgs.length > 0) {
78988
+ const denyReason = opts.missingArgsReason ? opts.missingArgsReason(missingRequiredArgs) : `Missing required parameter${missingRequiredArgs.length > 1 ? "s" : ""}: ${missingRequiredArgs.join(", ")}`;
78989
+ autoDenied.push({
78990
+ approval,
78991
+ permission,
78992
+ context: context3,
78993
+ parsedArgs,
78994
+ missingRequiredArgs,
78995
+ denyReason
78996
+ });
78997
+ continue;
78998
+ }
78999
+ }
79000
+ const entry = {
79001
+ approval,
79002
+ permission,
79003
+ context: context3,
79004
+ parsedArgs
79005
+ };
79006
+ if (decision === "ask") {
79007
+ needsUserInput.push(entry);
79008
+ } else if (decision === "deny") {
79009
+ autoDenied.push(entry);
79010
+ } else {
79011
+ autoAllowed.push(entry);
79012
+ }
79013
+ }
79014
+ return { needsUserInput, autoAllowed, autoDenied };
79015
+ }
79016
+ var init_approvalClassification = __esm(async () => {
79017
+ await init_manager3();
79018
+ });
79019
+
79020
+ // src/websocket/listener/approval-suggestions.ts
79021
+ function getSuggestedPermissionRule(context3) {
79022
+ if (!context3?.allowPersistence || context3.recommendedRule.trim().length === 0) {
79023
+ return null;
79024
+ }
79025
+ return context3.recommendedRule;
79026
+ }
79027
+ function getApprovalPermissionSuggestions(context3) {
79028
+ const suggestedRule = getSuggestedPermissionRule(context3);
79029
+ if (suggestedRule === null || !context3) {
79030
+ return [];
79031
+ }
79032
+ const text = context3.approveAlwaysText.trim();
79033
+ if (text.length === 0) {
79034
+ return [];
79035
+ }
79036
+ return [
79037
+ {
79038
+ suggestion: {
79039
+ id: "save-default",
79040
+ text
79041
+ },
79042
+ rule: suggestedRule,
79043
+ scope: context3.defaultScope
79044
+ }
79045
+ ];
79046
+ }
79047
+ function buildApprovalSuggestionPayload(context3) {
79048
+ return {
79049
+ permission_suggestions: getApprovalPermissionSuggestions(context3).map(({ suggestion }) => suggestion)
79050
+ };
79051
+ }
79052
+ async function classifyApprovalsWithSuggestions(approvals, opts = {}) {
79053
+ return classifyApprovals(approvals, {
79054
+ ...opts,
79055
+ getContext: async (toolName, parsedArgs, workingDirectory) => analyzeToolApproval(toolName, parsedArgs, workingDirectory)
79056
+ });
79057
+ }
79058
+ async function applySuggestedPermissionsForApproval(params) {
79059
+ const { decision, context: context3, workingDirectory } = params;
79060
+ if (!context3?.allowPersistence || context3.defaultScope === undefined) {
79061
+ return false;
79062
+ }
79063
+ const selectedIds = decision.selected_permission_suggestion_ids ?? [];
79064
+ if (selectedIds.length === 0) {
79065
+ return false;
79066
+ }
79067
+ const matchedSuggestions = getApprovalPermissionSuggestions(context3).filter(({ suggestion }) => selectedIds.includes(suggestion.id));
79068
+ if (matchedSuggestions.length === 0) {
79069
+ return false;
79070
+ }
79071
+ for (const matchedSuggestion of matchedSuggestions) {
79072
+ await savePermissionRule2(matchedSuggestion.rule, "allow", matchedSuggestion.scope, workingDirectory);
79073
+ }
79074
+ return true;
79075
+ }
79076
+ var init_approval_suggestions = __esm(async () => {
79077
+ await __promiseAll([
79078
+ init_approvalClassification(),
79079
+ init_manager3()
79080
+ ]);
79081
+ });
79082
+
79020
79083
  // src/websocket/listener/recovery.ts
79021
79084
  function isApprovalToolCallDesyncError(detail) {
79022
79085
  if (isInvalidToolCallIdsError(detail) || isApprovalPendingError(detail)) {
@@ -79232,12 +79295,13 @@ async function recoverApprovalStateForSync(runtime, scope) {
79232
79295
  return;
79233
79296
  }
79234
79297
  const workingDirectory = getConversationWorkingDirectory(runtime.listener, scope.agent_id, scope.conversation_id);
79235
- const { needsUserInput, autoAllowed, autoDenied } = await classifyApprovals(pendingApprovals, {
79298
+ const permissionModeState = getOrCreateConversationPermissionModeStateRef(runtime.listener, scope.agent_id, scope.conversation_id);
79299
+ const { needsUserInput, autoAllowed, autoDenied } = await classifyApprovalsWithSuggestions(pendingApprovals, {
79236
79300
  alwaysRequiresUserInput: isInteractiveApprovalTool,
79237
79301
  requireArgsForAutoApprove: true,
79238
79302
  missingNameReason: "Tool call incomplete - missing name",
79239
79303
  workingDirectory,
79240
- permissionModeState: getOrCreateConversationPermissionModeStateRef(runtime.listener, scope.agent_id, scope.conversation_id)
79304
+ permissionModeState
79241
79305
  });
79242
79306
  const autoDecisions = buildRecoveredAutoDecisions(autoAllowed, autoDenied);
79243
79307
  if (needsUserInput.length === 0) {
@@ -79252,6 +79316,7 @@ async function recoverApprovalStateForSync(runtime, scope) {
79252
79316
  const diffs = await computeDiffPreviews(approval.toolName, input, workingDirectory);
79253
79317
  approvalsByRequestId.set(requestId, {
79254
79318
  approval,
79319
+ approvalContext: approvalEntry.context,
79255
79320
  controlRequest: {
79256
79321
  type: "control_request",
79257
79322
  request_id: requestId,
@@ -79260,7 +79325,7 @@ async function recoverApprovalStateForSync(runtime, scope) {
79260
79325
  tool_name: approval.toolName,
79261
79326
  input,
79262
79327
  tool_call_id: approval.toolCallId,
79263
- permission_suggestions: [],
79328
+ ...buildApprovalSuggestionPayload(approvalEntry.context),
79264
79329
  blocked_path: null,
79265
79330
  ...diffs.length > 0 ? { diffs } : {}
79266
79331
  },
@@ -79290,6 +79355,40 @@ async function resolveRecoveredApprovalResponse(runtime, socket, response, proce
79290
79355
  }
79291
79356
  recovered.responsesByRequestId.set(requestId, response);
79292
79357
  recovered.pendingRequestIds.delete(requestId);
79358
+ const workingDirectory = getConversationWorkingDirectory(runtime.listener, recovered.agentId, recovered.conversationId);
79359
+ const respondedEntry = recovered.approvalsByRequestId.get(requestId);
79360
+ if (respondedEntry && "decision" in response && response.decision.behavior === "allow") {
79361
+ const savedSuggestions = await applySuggestedPermissionsForApproval({
79362
+ decision: response.decision,
79363
+ context: respondedEntry.approvalContext,
79364
+ workingDirectory
79365
+ });
79366
+ if (savedSuggestions && recovered.pendingRequestIds.size > 0) {
79367
+ const remainingRecoveredEntries = [...recovered.pendingRequestIds].map((id) => recovered.approvalsByRequestId.get(id)).filter((entry) => !!entry);
79368
+ const reclassified = await classifyApprovalsWithSuggestions(remainingRecoveredEntries.map((entry) => entry.approval), {
79369
+ alwaysRequiresUserInput: isInteractiveApprovalTool,
79370
+ requireArgsForAutoApprove: true,
79371
+ missingNameReason: "Tool call incomplete - missing name",
79372
+ workingDirectory,
79373
+ permissionModeState: getOrCreateConversationPermissionModeStateRef(runtime.listener, recovered.agentId, recovered.conversationId)
79374
+ });
79375
+ if (reclassified.autoAllowed.length > 0 || reclassified.autoDenied.length > 0) {
79376
+ recovered.autoDecisions = [
79377
+ ...recovered.autoDecisions ?? [],
79378
+ ...buildRecoveredAutoDecisions(reclassified.autoAllowed, reclassified.autoDenied)
79379
+ ];
79380
+ const reclassifiedToolCallIds = new Set([...reclassified.autoAllowed, ...reclassified.autoDenied].map((entry) => entry.approval.toolCallId));
79381
+ for (const pendingId of [...recovered.pendingRequestIds]) {
79382
+ const pendingEntry = recovered.approvalsByRequestId.get(pendingId);
79383
+ if (pendingEntry && reclassifiedToolCallIds.has(pendingEntry.approval.toolCallId)) {
79384
+ recovered.pendingRequestIds.delete(pendingId);
79385
+ recovered.approvalsByRequestId.delete(pendingId);
79386
+ recovered.responsesByRequestId.delete(pendingId);
79387
+ }
79388
+ }
79389
+ }
79390
+ }
79391
+ }
79293
79392
  if (recovered.pendingRequestIds.size > 0) {
79294
79393
  emitRuntimeStateUpdates(runtime, {
79295
79394
  agent_id: recovered.agentId,
@@ -79342,7 +79441,7 @@ async function resolveRecoveredApprovalResponse(runtime, socket, response, proce
79342
79441
  recovered.pendingRequestIds.clear();
79343
79442
  emitRuntimeStateUpdates(runtime, scope);
79344
79443
  runtime.isProcessing = true;
79345
- runtime.activeWorkingDirectory = getConversationWorkingDirectory(runtime.listener, recovered.agentId, recovered.conversationId);
79444
+ runtime.activeWorkingDirectory = workingDirectory;
79346
79445
  runtime.activeExecutingToolCallIds = [...approvedToolCallIds];
79347
79446
  setLoopStatus(runtime, "EXECUTING_CLIENT_SIDE_TOOL", scope);
79348
79447
  emitRuntimeStateUpdates(runtime, scope);
@@ -79367,7 +79466,7 @@ async function resolveRecoveredApprovalResponse(runtime, socket, response, proce
79367
79466
  const approvalResults = await executeApprovalBatch(decisions, undefined, {
79368
79467
  abortSignal: recoveryAbortController.signal,
79369
79468
  toolContextId: preparedToolContext.preparedToolContext.contextId,
79370
- workingDirectory: getConversationWorkingDirectory(runtime.listener, recovered.agentId, recovered.conversationId),
79469
+ workingDirectory,
79371
79470
  parentScope: recovered.agentId && recovered.conversationId ? {
79372
79471
  agentId: recovered.agentId,
79373
79472
  conversationId: recovered.conversationId
@@ -79434,9 +79533,9 @@ var init_recovery = __esm(async () => {
79434
79533
  init_approval_execution(),
79435
79534
  init_client2(),
79436
79535
  init_accumulator(),
79437
- init_approvalClassification(),
79438
79536
  init_stream(),
79439
79537
  init_toolset(),
79538
+ init_approval_suggestions(),
79440
79539
  init_interrupts(),
79441
79540
  init_protocol_outbound(),
79442
79541
  init_queue()
@@ -79526,12 +79625,13 @@ async function resolveStaleApprovals(runtime, socket, abortSignal, deps = {}) {
79526
79625
  throw new Error("Ambiguous pending approval batch mapping during recovery");
79527
79626
  }
79528
79627
  rememberPendingApprovalBatchIds(runtime, pendingApprovals, recoveryBatchId);
79529
- const { autoAllowed, autoDenied, needsUserInput } = await classifyApprovals(pendingApprovals, {
79628
+ const permissionModeState = getOrCreateConversationPermissionModeStateRef(runtime.listener, runtime.agentId, runtime.conversationId);
79629
+ const { autoAllowed, autoDenied, needsUserInput } = await classifyApprovalsWithSuggestions(pendingApprovals, {
79530
79630
  alwaysRequiresUserInput: isInteractiveApprovalTool,
79531
79631
  requireArgsForAutoApprove: true,
79532
79632
  missingNameReason: "Tool call incomplete - missing name",
79533
79633
  workingDirectory: recoveryWorkingDirectory,
79534
- permissionModeState: getOrCreateConversationPermissionModeStateRef(runtime.listener, runtime.agentId, runtime.conversationId)
79634
+ permissionModeState
79535
79635
  });
79536
79636
  const decisions = [
79537
79637
  ...autoAllowed.map((ac) => ({
@@ -79544,11 +79644,16 @@ async function resolveStaleApprovals(runtime, socket, abortSignal, deps = {}) {
79544
79644
  reason: ac.denyReason || ac.permission.reason || "Permission denied"
79545
79645
  }))
79546
79646
  ];
79547
- if (needsUserInput.length > 0) {
79647
+ let pendingNeedsUserInput = [...needsUserInput];
79648
+ if (pendingNeedsUserInput.length > 0) {
79548
79649
  runtime.lastStopReason = "requires_approval";
79549
79650
  setLoopStatus(runtime, "WAITING_ON_APPROVAL", scope);
79550
79651
  emitRuntimeStateUpdates(runtime, scope);
79551
- for (const ac of needsUserInput) {
79652
+ while (pendingNeedsUserInput.length > 0) {
79653
+ const ac = pendingNeedsUserInput.shift();
79654
+ if (!ac) {
79655
+ break;
79656
+ }
79552
79657
  if (abortSignal.aborted)
79553
79658
  throw new Error("Cancelled");
79554
79659
  const requestId = `perm-${ac.approval.toolCallId}`;
@@ -79561,7 +79666,7 @@ async function resolveStaleApprovals(runtime, socket, abortSignal, deps = {}) {
79561
79666
  tool_name: ac.approval.toolName,
79562
79667
  input: ac.parsedArgs,
79563
79668
  tool_call_id: ac.approval.toolCallId,
79564
- permission_suggestions: [],
79669
+ ...buildApprovalSuggestionPayload(ac.context),
79565
79670
  blocked_path: null,
79566
79671
  ...diffs.length > 0 ? { diffs } : {}
79567
79672
  },
@@ -79572,6 +79677,11 @@ async function resolveStaleApprovals(runtime, socket, abortSignal, deps = {}) {
79572
79677
  if ("decision" in responseBody) {
79573
79678
  const response = responseBody.decision;
79574
79679
  if (response.behavior === "allow") {
79680
+ const savedSuggestions = await applySuggestedPermissionsForApproval({
79681
+ decision: response,
79682
+ context: ac.context,
79683
+ workingDirectory: recoveryWorkingDirectory
79684
+ });
79575
79685
  decisions.push({
79576
79686
  type: "approve",
79577
79687
  approval: response.updated_input ? {
@@ -79580,6 +79690,24 @@ async function resolveStaleApprovals(runtime, socket, abortSignal, deps = {}) {
79580
79690
  } : ac.approval,
79581
79691
  reason: response.message
79582
79692
  });
79693
+ if (savedSuggestions && pendingNeedsUserInput.length > 0) {
79694
+ const reclassified = await classifyApprovalsWithSuggestions(pendingNeedsUserInput.map((entry) => entry.approval), {
79695
+ alwaysRequiresUserInput: isInteractiveApprovalTool,
79696
+ requireArgsForAutoApprove: true,
79697
+ missingNameReason: "Tool call incomplete - missing name",
79698
+ workingDirectory: recoveryWorkingDirectory,
79699
+ permissionModeState
79700
+ });
79701
+ decisions.push(...reclassified.autoAllowed.map((entry) => ({
79702
+ type: "approve",
79703
+ approval: entry.approval
79704
+ })), ...reclassified.autoDenied.map((entry) => ({
79705
+ type: "deny",
79706
+ approval: entry.approval,
79707
+ reason: entry.denyReason || entry.permission.reason || "Permission denied"
79708
+ })));
79709
+ pendingNeedsUserInput = [...reclassified.needsUserInput];
79710
+ }
79583
79711
  } else {
79584
79712
  decisions.push({
79585
79713
  type: "deny",
@@ -79929,9 +80057,9 @@ var init_send = __esm(async () => {
79929
80057
  init_approval_recovery(),
79930
80058
  init_client2(),
79931
80059
  init_message(),
79932
- init_approvalClassification(),
79933
80060
  init_toolset(),
79934
80061
  init_approval(),
80062
+ init_approval_suggestions(),
79935
80063
  init_interrupts(),
79936
80064
  init_protocol_outbound(),
79937
80065
  init_queue(),
@@ -79997,7 +80125,7 @@ async function handleApprovalStop(params) {
79997
80125
  }
79998
80126
  clearPendingApprovalBatchIds(runtime, approvals);
79999
80127
  rememberPendingApprovalBatchIds(runtime, approvals, dequeuedBatchId);
80000
- const { autoAllowed, autoDenied, needsUserInput } = await classifyApprovals(approvals, {
80128
+ const { autoAllowed, autoDenied, needsUserInput } = await classifyApprovalsWithSuggestions(approvals, {
80001
80129
  alwaysRequiresUserInput: isInteractiveApprovalTool,
80002
80130
  treatAskAsDeny: false,
80003
80131
  requireArgsForAutoApprove: true,
@@ -80005,7 +80133,8 @@ async function handleApprovalStop(params) {
80005
80133
  workingDirectory: turnWorkingDirectory,
80006
80134
  permissionModeState: turnPermissionModeState
80007
80135
  });
80008
- const lastNeedsUserInputToolCallIds = needsUserInput.map((ac) => ac.approval.toolCallId);
80136
+ let pendingNeedsUserInput = [...needsUserInput];
80137
+ let lastNeedsUserInputToolCallIds = pendingNeedsUserInput.map((ac) => ac.approval.toolCallId);
80009
80138
  let lastExecutionResults = null;
80010
80139
  let lastExecutingToolCallIds = [];
80011
80140
  const shouldInterrupt = () => abortController.signal.aborted || runtime.cancelRequested;
@@ -80044,7 +80173,7 @@ async function handleApprovalStop(params) {
80044
80173
  if (shouldInterrupt()) {
80045
80174
  return interruptTermination();
80046
80175
  }
80047
- if (needsUserInput.length > 0) {
80176
+ if (pendingNeedsUserInput.length > 0) {
80048
80177
  if (shouldInterrupt()) {
80049
80178
  return interruptTermination();
80050
80179
  }
@@ -80053,7 +80182,11 @@ async function handleApprovalStop(params) {
80053
80182
  agent_id: agentId,
80054
80183
  conversation_id: conversationId
80055
80184
  });
80056
- for (const ac of needsUserInput) {
80185
+ while (pendingNeedsUserInput.length > 0) {
80186
+ const ac = pendingNeedsUserInput.shift();
80187
+ if (!ac) {
80188
+ break;
80189
+ }
80057
80190
  if (shouldInterrupt()) {
80058
80191
  return interruptTermination();
80059
80192
  }
@@ -80070,7 +80203,7 @@ async function handleApprovalStop(params) {
80070
80203
  tool_name: ac.approval.toolName,
80071
80204
  input: ac.parsedArgs,
80072
80205
  tool_call_id: ac.approval.toolCallId,
80073
- permission_suggestions: [],
80206
+ ...buildApprovalSuggestionPayload(ac.context),
80074
80207
  blocked_path: null,
80075
80208
  ...diffs.length > 0 ? { diffs } : {}
80076
80209
  },
@@ -80092,6 +80225,11 @@ async function handleApprovalStop(params) {
80092
80225
  if ("decision" in responseBody) {
80093
80226
  const response = responseBody.decision;
80094
80227
  if (response.behavior === "allow") {
80228
+ const savedSuggestions = await applySuggestedPermissionsForApproval({
80229
+ decision: response,
80230
+ context: ac.context,
80231
+ workingDirectory: turnWorkingDirectory
80232
+ });
80095
80233
  const finalApproval = response.updated_input ? {
80096
80234
  ...ac.approval,
80097
80235
  toolArgs: JSON.stringify(response.updated_input)
@@ -80101,6 +80239,26 @@ async function handleApprovalStop(params) {
80101
80239
  approval: finalApproval,
80102
80240
  reason: response.message
80103
80241
  });
80242
+ if (savedSuggestions && pendingNeedsUserInput.length > 0) {
80243
+ const reclassified = await classifyApprovalsWithSuggestions(pendingNeedsUserInput.map((entry) => entry.approval), {
80244
+ alwaysRequiresUserInput: isInteractiveApprovalTool,
80245
+ treatAskAsDeny: false,
80246
+ requireArgsForAutoApprove: true,
80247
+ missingNameReason: "Tool call incomplete - missing name",
80248
+ workingDirectory: turnWorkingDirectory,
80249
+ permissionModeState: turnPermissionModeState
80250
+ });
80251
+ decisions.push(...reclassified.autoAllowed.map((entry) => ({
80252
+ type: "approve",
80253
+ approval: entry.approval
80254
+ })), ...reclassified.autoDenied.map((entry) => ({
80255
+ type: "deny",
80256
+ approval: entry.approval,
80257
+ reason: entry.denyReason || entry.permission.reason || "Permission denied"
80258
+ })));
80259
+ pendingNeedsUserInput = [...reclassified.needsUserInput];
80260
+ lastNeedsUserInputToolCallIds = pendingNeedsUserInput.map((entry) => entry.approval.toolCallId);
80261
+ }
80104
80262
  } else {
80105
80263
  decisions.push({
80106
80264
  type: "deny",
@@ -80248,8 +80406,8 @@ var init_turn_approval = __esm(async () => {
80248
80406
  init_skill_injection();
80249
80407
  await __promiseAll([
80250
80408
  init_approval_execution(),
80251
- init_approvalClassification(),
80252
80409
  init_approval(),
80410
+ init_approval_suggestions(),
80253
80411
  init_interrupts(),
80254
80412
  init_protocol_outbound(),
80255
80413
  init_queue(),
@@ -152041,4 +152199,4 @@ Error during initialization: ${message}`);
152041
152199
  }
152042
152200
  main();
152043
152201
 
152044
- //# debugId=55BEE0B212809B3B64756E2164756E21
152202
+ //# debugId=594F8E4675FBE99864756E2164756E21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letta-ai/letta-code",
3
- "version": "0.21.8",
3
+ "version": "0.21.9",
4
4
  "description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {