mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-18 05:18:31 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fd9fa0252d | |||
| 5461c6260a | |||
| 12979ae0cf | |||
| a51f2332c5 | |||
| 9b5fd01190 | |||
| 32aa89214c | |||
| 006f0cd277 | |||
| dad876882e |
@@ -391,9 +391,13 @@
|
||||
"tokenTag.overload": "Exceeded Limit",
|
||||
"tokenTag.remained": "Remaining",
|
||||
"tokenTag.used": "Used",
|
||||
"tool.intervention.allowSession": "Allow for This Session",
|
||||
"tool.intervention.approve": "Approve",
|
||||
"tool.intervention.approveAll": "Approve All",
|
||||
"tool.intervention.approveAndRemember": "Approve and Remember",
|
||||
"tool.intervention.approveOnce": "Approve This Time Only",
|
||||
"tool.intervention.batchTitle_one": "{{count}} operation requires approval",
|
||||
"tool.intervention.batchTitle_other": "{{count}} operations require approval",
|
||||
"tool.intervention.mode.allowList": "Allow List",
|
||||
"tool.intervention.mode.allowListDesc": "Only automatically execute approved tools",
|
||||
"tool.intervention.mode.autoRun": "Auto Approve",
|
||||
@@ -401,11 +405,13 @@
|
||||
"tool.intervention.mode.manual": "Manual",
|
||||
"tool.intervention.mode.manualDesc": "Manual approval required for each invocation",
|
||||
"tool.intervention.reject": "Reject",
|
||||
"tool.intervention.rejectAll": "Reject All",
|
||||
"tool.intervention.rejectAndContinue": "Reject and Retry",
|
||||
"tool.intervention.rejectOnly": "Reject",
|
||||
"tool.intervention.rejectReasonPlaceholder": "A reason helps the Agent understand your boundaries and improve future actions",
|
||||
"tool.intervention.rejectTitle": "Reject this Skill call",
|
||||
"tool.intervention.rejectedWithReason": "This Skill call was rejected: {{reason}}",
|
||||
"tool.intervention.sessionBypassed": "Auto-approved (session authorized)",
|
||||
"tool.intervention.toolAbort": "You canceled this Skill call",
|
||||
"tool.intervention.toolRejected": "This Skill call was rejected",
|
||||
"toolAuth.authorize": "Authorize",
|
||||
|
||||
@@ -391,9 +391,13 @@
|
||||
"tokenTag.overload": "超出限制",
|
||||
"tokenTag.remained": "剩余",
|
||||
"tokenTag.used": "已使用",
|
||||
"tool.intervention.allowSession": "允许当前会话",
|
||||
"tool.intervention.approve": "批准",
|
||||
"tool.intervention.approveAll": "全部批准",
|
||||
"tool.intervention.approveAndRemember": "批准并记住",
|
||||
"tool.intervention.approveOnce": "仅本次批准",
|
||||
"tool.intervention.batchTitle_one": "{{count}} 个操作需要确认",
|
||||
"tool.intervention.batchTitle_other": "{{count}} 个操作需要确认",
|
||||
"tool.intervention.mode.allowList": "白名单",
|
||||
"tool.intervention.mode.allowListDesc": "仅自动执行已批准的技能",
|
||||
"tool.intervention.mode.autoRun": "自动批准",
|
||||
@@ -401,11 +405,13 @@
|
||||
"tool.intervention.mode.manual": "手动批准",
|
||||
"tool.intervention.mode.manualDesc": "每次调用都需要你确认",
|
||||
"tool.intervention.reject": "拒绝",
|
||||
"tool.intervention.rejectAll": "全部拒绝",
|
||||
"tool.intervention.rejectAndContinue": "拒绝后继续",
|
||||
"tool.intervention.rejectOnly": "仅拒绝",
|
||||
"tool.intervention.rejectReasonPlaceholder": "填写原因可帮助助理理解你的边界,并优化后续行动",
|
||||
"tool.intervention.rejectTitle": "拒绝本次技能调用",
|
||||
"tool.intervention.rejectedWithReason": "本次技能调用已被拒绝:{{reason}}",
|
||||
"tool.intervention.sessionBypassed": "已自动批准(会话授权)",
|
||||
"tool.intervention.toolAbort": "你已取消本次技能调用",
|
||||
"tool.intervention.toolRejected": "本次技能调用已被拒绝",
|
||||
"toolAuth.authorize": "授权",
|
||||
|
||||
@@ -128,18 +128,12 @@ export class GeneralChatAgent implements Agent {
|
||||
const toolsNeedingIntervention: ChatToolPayload[] = [];
|
||||
const toolsToExecute: ChatToolPayload[] = [];
|
||||
|
||||
// Get security blacklist for resolver metadata
|
||||
const securityBlacklist = state.securityBlacklist ?? DEFAULT_SECURITY_BLACKLIST;
|
||||
|
||||
// Build resolver metadata: merge state.metadata with security blacklist
|
||||
const resolverMetadata = { ...state.metadata, securityBlacklist };
|
||||
|
||||
// Get user config (default to 'manual' mode)
|
||||
const userConfig = state.userInterventionConfig || { approvalMode: 'manual' };
|
||||
const { approvalMode, allowList = [] } = userConfig;
|
||||
|
||||
// Global audits: default to security blacklist audit if not provided
|
||||
const globalResolvers = this.config.globalInterventionAudits ?? createDefaultGlobalAudits();
|
||||
const sessionBypassed = state.sessionBypassedAudits ?? [];
|
||||
|
||||
for (const toolCalling of toolsCalling) {
|
||||
const { identifier, apiName } = toolCalling;
|
||||
@@ -161,6 +155,10 @@ export class GeneralChatAgent implements Agent {
|
||||
if (globalResolver.resolver(toolArgs, resolverMetadata)) {
|
||||
globalBlocked = true;
|
||||
globalPolicy = globalResolver.policy ?? 'always';
|
||||
toolCalling.intervention = {
|
||||
...toolCalling.intervention,
|
||||
auditType: globalResolver.type,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -194,14 +192,29 @@ export class GeneralChatAgent implements Agent {
|
||||
if (dynamicPolicy === 'never') {
|
||||
toolsToExecute.push(toolCalling);
|
||||
} else {
|
||||
toolsNeedingIntervention.push(toolCalling);
|
||||
const dynamicConfig = config as { dynamic: { type: string } };
|
||||
const auditType = dynamicConfig.dynamic.type;
|
||||
toolCalling.intervention = { ...toolCalling.intervention, auditType };
|
||||
|
||||
if (sessionBypassed.includes(auditType)) {
|
||||
toolCalling.intervention = { ...toolCalling.intervention, status: 'session_bypassed' };
|
||||
toolsToExecute.push(toolCalling);
|
||||
} else {
|
||||
toolsNeedingIntervention.push(toolCalling);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Phase 3.5: Handle overridable global block (policy !== 'always')
|
||||
if (globalBlocked && globalPolicy !== 'always') {
|
||||
toolsNeedingIntervention.push(toolCalling);
|
||||
const auditType = toolCalling.intervention?.auditType;
|
||||
if (auditType && sessionBypassed.includes(auditType)) {
|
||||
toolCalling.intervention = { ...toolCalling.intervention, status: 'session_bypassed' };
|
||||
toolsToExecute.push(toolCalling);
|
||||
} else {
|
||||
toolsNeedingIntervention.push(toolCalling);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -2507,4 +2507,346 @@ describe('GeneralChatAgent', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('audit type annotation and session bypass', () => {
|
||||
it('should annotate auditType from global resolver on tools needing intervention', async () => {
|
||||
const customResolver: GlobalInterventionAuditConfig = {
|
||||
type: 'customBlocker',
|
||||
policy: 'always',
|
||||
resolver: (toolArgs) => toolArgs.dangerous === true,
|
||||
};
|
||||
|
||||
const agent = new GeneralChatAgent({
|
||||
agentConfig: { maxSteps: 100 },
|
||||
globalInterventionAudits: [customResolver],
|
||||
operationId: 'test-session',
|
||||
modelRuntimeConfig: mockModelRuntimeConfig,
|
||||
});
|
||||
|
||||
const toolCall: ChatToolPayload = {
|
||||
id: 'call-1',
|
||||
identifier: 'my-tool',
|
||||
apiName: 'doSomething',
|
||||
arguments: '{"dangerous":true}',
|
||||
type: 'default',
|
||||
};
|
||||
|
||||
const state = createMockState({
|
||||
toolManifestMap: {
|
||||
'my-tool': { identifier: 'my-tool', humanIntervention: 'never' },
|
||||
},
|
||||
});
|
||||
|
||||
const context = createMockContext('llm_result', {
|
||||
hasToolsCalling: true,
|
||||
toolsCalling: [toolCall],
|
||||
parentMessageId: 'msg-1',
|
||||
});
|
||||
|
||||
const result = await agent.runner(context, state);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'request_human_approve',
|
||||
pendingToolsCalling: [
|
||||
expect.objectContaining({
|
||||
id: 'call-1',
|
||||
intervention: { auditType: 'customBlocker' },
|
||||
}),
|
||||
],
|
||||
reason: 'human_intervention_required',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should annotate auditType from dynamic resolver on tools needing intervention', async () => {
|
||||
const agent = new GeneralChatAgent({
|
||||
agentConfig: { maxSteps: 100 },
|
||||
dynamicInterventionAudits: {
|
||||
pathScopeAudit: (toolArgs, metadata) => {
|
||||
const workingDirectory = metadata?.workingDirectory as string | undefined;
|
||||
if (!workingDirectory) return false;
|
||||
const path = toolArgs.path as string;
|
||||
return !path.startsWith(workingDirectory);
|
||||
},
|
||||
},
|
||||
operationId: 'test-session',
|
||||
modelRuntimeConfig: mockModelRuntimeConfig,
|
||||
});
|
||||
|
||||
const toolCall: ChatToolPayload = {
|
||||
id: 'call-1',
|
||||
identifier: 'local-system',
|
||||
apiName: 'readLocalFile',
|
||||
arguments: '{"path":"/etc/passwd"}',
|
||||
type: 'builtin',
|
||||
};
|
||||
|
||||
const state = createMockState({
|
||||
metadata: { workingDirectory: '/workspace' },
|
||||
toolManifestMap: {
|
||||
'local-system': {
|
||||
identifier: 'local-system',
|
||||
api: [
|
||||
{
|
||||
name: 'readLocalFile',
|
||||
humanIntervention: {
|
||||
dynamic: {
|
||||
default: 'never',
|
||||
policy: 'required',
|
||||
type: 'pathScopeAudit',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const context = createMockContext('llm_result', {
|
||||
hasToolsCalling: true,
|
||||
toolsCalling: [toolCall],
|
||||
parentMessageId: 'msg-1',
|
||||
});
|
||||
|
||||
const result = await agent.runner(context, state);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'request_human_approve',
|
||||
pendingToolsCalling: [
|
||||
expect.objectContaining({
|
||||
id: 'call-1',
|
||||
intervention: { auditType: 'pathScopeAudit' },
|
||||
}),
|
||||
],
|
||||
reason: 'human_intervention_required',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should session-bypass dynamic audit type when in sessionBypassedAudits', async () => {
|
||||
const agent = new GeneralChatAgent({
|
||||
agentConfig: { maxSteps: 100 },
|
||||
dynamicInterventionAudits: {
|
||||
pathScopeAudit: () => true,
|
||||
},
|
||||
operationId: 'test-session',
|
||||
modelRuntimeConfig: mockModelRuntimeConfig,
|
||||
});
|
||||
|
||||
const toolCall: ChatToolPayload = {
|
||||
id: 'call-1',
|
||||
identifier: 'local-system',
|
||||
apiName: 'readLocalFile',
|
||||
arguments: '{"path":"/etc/passwd"}',
|
||||
type: 'builtin',
|
||||
};
|
||||
|
||||
const state = createMockState({
|
||||
sessionBypassedAudits: ['pathScopeAudit'],
|
||||
toolManifestMap: {
|
||||
'local-system': {
|
||||
identifier: 'local-system',
|
||||
api: [
|
||||
{
|
||||
name: 'readLocalFile',
|
||||
humanIntervention: {
|
||||
dynamic: {
|
||||
default: 'never',
|
||||
policy: 'required',
|
||||
type: 'pathScopeAudit',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const context = createMockContext('llm_result', {
|
||||
hasToolsCalling: true,
|
||||
toolsCalling: [toolCall],
|
||||
parentMessageId: 'msg-1',
|
||||
});
|
||||
|
||||
const result = await agent.runner(context, state);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'call_tool',
|
||||
payload: {
|
||||
parentMessageId: 'msg-1',
|
||||
toolCalling: expect.objectContaining({
|
||||
id: 'call-1',
|
||||
intervention: { auditType: 'pathScopeAudit', status: 'session_bypassed' },
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should NOT session-bypass dynamic audit type when NOT in sessionBypassedAudits', async () => {
|
||||
const agent = new GeneralChatAgent({
|
||||
agentConfig: { maxSteps: 100 },
|
||||
dynamicInterventionAudits: {
|
||||
pathScopeAudit: () => true,
|
||||
},
|
||||
operationId: 'test-session',
|
||||
modelRuntimeConfig: mockModelRuntimeConfig,
|
||||
});
|
||||
|
||||
const toolCall: ChatToolPayload = {
|
||||
id: 'call-1',
|
||||
identifier: 'local-system',
|
||||
apiName: 'readLocalFile',
|
||||
arguments: '{"path":"/etc/passwd"}',
|
||||
type: 'builtin',
|
||||
};
|
||||
|
||||
const state = createMockState({
|
||||
sessionBypassedAudits: ['someOtherAudit'],
|
||||
toolManifestMap: {
|
||||
'local-system': {
|
||||
identifier: 'local-system',
|
||||
api: [
|
||||
{
|
||||
name: 'readLocalFile',
|
||||
humanIntervention: {
|
||||
dynamic: {
|
||||
default: 'never',
|
||||
policy: 'required',
|
||||
type: 'pathScopeAudit',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const context = createMockContext('llm_result', {
|
||||
hasToolsCalling: true,
|
||||
toolsCalling: [toolCall],
|
||||
parentMessageId: 'msg-1',
|
||||
});
|
||||
|
||||
const result = await agent.runner(context, state);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'request_human_approve',
|
||||
pendingToolsCalling: [
|
||||
expect.objectContaining({
|
||||
id: 'call-1',
|
||||
intervention: { auditType: 'pathScopeAudit' },
|
||||
}),
|
||||
],
|
||||
reason: 'human_intervention_required',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should session-bypass overridable global block when auditType is in sessionBypassedAudits', async () => {
|
||||
const softBlocker: GlobalInterventionAuditConfig = {
|
||||
type: 'softBlocker',
|
||||
policy: 'required',
|
||||
resolver: () => true,
|
||||
};
|
||||
|
||||
const agent = new GeneralChatAgent({
|
||||
agentConfig: { maxSteps: 100 },
|
||||
globalInterventionAudits: [softBlocker],
|
||||
operationId: 'test-session',
|
||||
modelRuntimeConfig: mockModelRuntimeConfig,
|
||||
});
|
||||
|
||||
const toolCall: ChatToolPayload = {
|
||||
id: 'call-1',
|
||||
identifier: 'my-tool',
|
||||
apiName: 'doSomething',
|
||||
arguments: '{}',
|
||||
type: 'default',
|
||||
};
|
||||
|
||||
const state = createMockState({
|
||||
sessionBypassedAudits: ['softBlocker'],
|
||||
toolManifestMap: {
|
||||
'my-tool': { identifier: 'my-tool', humanIntervention: 'never' },
|
||||
},
|
||||
});
|
||||
|
||||
const context = createMockContext('llm_result', {
|
||||
hasToolsCalling: true,
|
||||
toolsCalling: [toolCall],
|
||||
parentMessageId: 'msg-1',
|
||||
});
|
||||
|
||||
const result = await agent.runner(context, state);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'call_tool',
|
||||
payload: {
|
||||
parentMessageId: 'msg-1',
|
||||
toolCalling: expect.objectContaining({
|
||||
id: 'call-1',
|
||||
intervention: { auditType: 'softBlocker', status: 'session_bypassed' },
|
||||
}),
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should NOT session-bypass global audit with policy always (e.g., securityBlacklist)', async () => {
|
||||
const alwaysBlocker: GlobalInterventionAuditConfig = {
|
||||
type: 'securityBlacklist',
|
||||
policy: 'always',
|
||||
resolver: (toolArgs) => toolArgs.dangerous === true,
|
||||
};
|
||||
|
||||
const agent = new GeneralChatAgent({
|
||||
agentConfig: { maxSteps: 100 },
|
||||
globalInterventionAudits: [alwaysBlocker],
|
||||
operationId: 'test-session',
|
||||
modelRuntimeConfig: mockModelRuntimeConfig,
|
||||
});
|
||||
|
||||
const toolCall: ChatToolPayload = {
|
||||
id: 'call-1',
|
||||
identifier: 'my-tool',
|
||||
apiName: 'doSomething',
|
||||
arguments: '{"dangerous":true}',
|
||||
type: 'default',
|
||||
};
|
||||
|
||||
const state = createMockState({
|
||||
sessionBypassedAudits: ['securityBlacklist'],
|
||||
toolManifestMap: {
|
||||
'my-tool': { identifier: 'my-tool', humanIntervention: 'never' },
|
||||
},
|
||||
});
|
||||
|
||||
const context = createMockContext('llm_result', {
|
||||
hasToolsCalling: true,
|
||||
toolsCalling: [toolCall],
|
||||
parentMessageId: 'msg-1',
|
||||
});
|
||||
|
||||
const result = await agent.runner(context, state);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'request_human_approve',
|
||||
pendingToolsCalling: [
|
||||
expect.objectContaining({
|
||||
id: 'call-1',
|
||||
intervention: { auditType: 'securityBlacklist' },
|
||||
}),
|
||||
],
|
||||
reason: 'human_intervention_required',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,6 +121,32 @@ export class AgentRuntime {
|
||||
},
|
||||
type: 'call_tool',
|
||||
};
|
||||
} else if (runtimeContext.phase === 'human_approved_tools') {
|
||||
// Batch approve: execute all approved tools
|
||||
const approvedPayload = runtimeContext.payload as {
|
||||
approvedToolCalls: ChatToolPayload[];
|
||||
parentMessageId: string;
|
||||
};
|
||||
|
||||
if (approvedPayload.approvedToolCalls.length === 1) {
|
||||
rawInstructions = {
|
||||
payload: {
|
||||
parentMessageId: approvedPayload.parentMessageId,
|
||||
skipCreateToolMessage: true,
|
||||
toolCalling: approvedPayload.approvedToolCalls[0],
|
||||
},
|
||||
type: 'call_tool',
|
||||
};
|
||||
} else {
|
||||
rawInstructions = {
|
||||
payload: {
|
||||
parentMessageId: approvedPayload.parentMessageId,
|
||||
skipCreateToolMessage: true,
|
||||
toolsCalling: approvedPayload.approvedToolCalls,
|
||||
},
|
||||
type: 'call_tools_batch',
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Standard flow: Plan -> Execute
|
||||
rawInstructions = await this.agent.runner(runtimeContext, newState);
|
||||
@@ -685,7 +711,11 @@ export class AgentRuntime {
|
||||
const results = await pMap(instruction.payload.toolsCalling, (toolCalling: ChatToolPayload) =>
|
||||
this.executors.call_tool(
|
||||
{
|
||||
payload: { parentMessageId: payload.parentMessageId, toolCalling },
|
||||
payload: {
|
||||
parentMessageId: payload.parentMessageId,
|
||||
skipCreateToolMessage: payload.skipCreateToolMessage,
|
||||
toolCalling,
|
||||
},
|
||||
type: 'call_tool',
|
||||
} as AgentInstructionCallTool,
|
||||
structuredClone(baseState), // Each tool starts from the same base state
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface AgentRuntimeContext {
|
||||
| 'tasks_batch_result'
|
||||
| 'human_response'
|
||||
| 'human_approved_tool'
|
||||
| 'human_approved_tools'
|
||||
| 'human_abort'
|
||||
| 'compression_result'
|
||||
| 'error';
|
||||
|
||||
@@ -104,6 +104,7 @@ export interface AgentState {
|
||||
* If not provided, DEFAULT_SECURITY_BLACKLIST will be used.
|
||||
*/
|
||||
securityBlacklist?: SecurityBlacklistConfig;
|
||||
sessionBypassedAudits?: string[];
|
||||
// --- State Machine ---
|
||||
status: 'idle' | 'running' | 'waiting_for_human' | 'done' | 'error' | 'interrupted';
|
||||
|
||||
|
||||
@@ -6,13 +6,17 @@ import type { LobeToolRenderType } from '../../tool';
|
||||
|
||||
// ToolIntervention must be defined first to avoid circular dependency
|
||||
export interface ToolIntervention {
|
||||
auditType?: string;
|
||||
rejectedReason?: string;
|
||||
status?: 'pending' | 'approved' | 'rejected' | 'aborted' | 'none';
|
||||
status?: 'pending' | 'approved' | 'rejected' | 'aborted' | 'none' | 'session_bypassed';
|
||||
}
|
||||
|
||||
export const ToolInterventionSchema = z.object({
|
||||
auditType: z.string().optional(),
|
||||
rejectedReason: z.string().optional(),
|
||||
status: z.enum(['pending', 'approved', 'rejected', 'aborted', 'none']).optional(),
|
||||
status: z
|
||||
.enum(['pending', 'approved', 'rejected', 'aborted', 'none', 'session_bypassed'])
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export interface ChatPluginPayload {
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { Button, Flexbox } from '@lobehub/ui';
|
||||
import { memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useChatStore } from '@/store/chat';
|
||||
|
||||
import { useConversationStore } from '../../../store';
|
||||
import { useMessageAggregationContext } from '../../Contexts/MessageAggregationContext';
|
||||
|
||||
interface BatchApprovalActionsProps {
|
||||
auditType: string;
|
||||
toolMessageIds: string[];
|
||||
}
|
||||
|
||||
const BatchApprovalActions = memo<BatchApprovalActionsProps>(({ auditType, toolMessageIds }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const { assistantGroupId } = useMessageAggregationContext();
|
||||
const [loading, setLoading] = useState<string | null>(null);
|
||||
|
||||
const approveAllToolCallings = useChatStore((s) => s.approveAllToolCallings);
|
||||
const bypassAuditForSession = useChatStore((s) => s.bypassAuditForSession);
|
||||
const rejectToolCall = useConversationStore((s) => s.rejectToolCall);
|
||||
const context = useConversationStore((s) => s.context);
|
||||
|
||||
const handleApproveAll = async () => {
|
||||
setLoading('approveAll');
|
||||
try {
|
||||
await approveAllToolCallings(toolMessageIds, assistantGroupId, context);
|
||||
} finally {
|
||||
setLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRejectAll = async () => {
|
||||
setLoading('rejectAll');
|
||||
try {
|
||||
for (const id of toolMessageIds) {
|
||||
await rejectToolCall(id);
|
||||
}
|
||||
} finally {
|
||||
setLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAllowSession = async () => {
|
||||
setLoading('allowSession');
|
||||
try {
|
||||
bypassAuditForSession(auditType);
|
||||
await approveAllToolCallings(toolMessageIds, assistantGroupId, context);
|
||||
} finally {
|
||||
setLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const isAnyLoading = loading !== null;
|
||||
|
||||
return (
|
||||
<Flexbox horizontal gap={8} justify="flex-end">
|
||||
<Button
|
||||
color="default"
|
||||
disabled={isAnyLoading}
|
||||
loading={loading === 'rejectAll'}
|
||||
size="small"
|
||||
variant="filled"
|
||||
onClick={handleRejectAll}
|
||||
>
|
||||
{t('tool.intervention.rejectAll')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isAnyLoading}
|
||||
loading={loading === 'approveAll'}
|
||||
size="small"
|
||||
type="primary"
|
||||
variant="outlined"
|
||||
onClick={handleApproveAll}
|
||||
>
|
||||
{t('tool.intervention.approveAll')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isAnyLoading}
|
||||
loading={loading === 'allowSession'}
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={handleAllowSession}
|
||||
>
|
||||
{t('tool.intervention.allowSession')}
|
||||
</Button>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default BatchApprovalActions;
|
||||
@@ -0,0 +1,97 @@
|
||||
import { getBuiltinIntervention } from '@lobechat/builtin-tools/interventions';
|
||||
import { type ChatToolPayloadWithResult } from '@lobechat/types';
|
||||
import { safeParseJSON } from '@lobechat/utils';
|
||||
import { AccordionItem, Button, Flexbox } from '@lobehub/ui';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useConversationStore } from '../../../store';
|
||||
import { useMessageAggregationContext } from '../../Contexts/MessageAggregationContext';
|
||||
|
||||
interface BatchToolItemProps {
|
||||
tool: ChatToolPayloadWithResult;
|
||||
}
|
||||
|
||||
const BatchToolItem = memo<BatchToolItemProps>(({ tool }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const { assistantGroupId } = useMessageAggregationContext();
|
||||
const [approveToolCall, rejectToolCall] = useConversationStore((s) => [
|
||||
s.approveToolCall,
|
||||
s.rejectToolCall,
|
||||
]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const toolName = `${tool.identifier}/${tool.apiName}`;
|
||||
const parsedArgs = useMemo(() => safeParseJSON(tool.arguments || '') ?? {}, [tool.arguments]);
|
||||
|
||||
const BuiltinIntervention = getBuiltinIntervention(tool.identifier, tool.apiName);
|
||||
|
||||
const isMessageCreating = !tool.result_msg_id || tool.result_msg_id.startsWith('tmp_');
|
||||
|
||||
const handleApprove = async () => {
|
||||
if (isMessageCreating) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await approveToolCall(tool.result_msg_id!, assistantGroupId);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReject = async () => {
|
||||
if (isMessageCreating) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await rejectToolCall(tool.result_msg_id!);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
expand
|
||||
itemKey={tool.id}
|
||||
paddingBlock={4}
|
||||
paddingInline={8}
|
||||
title={<span style={{ fontSize: 13 }}>{toolName}</span>}
|
||||
action={
|
||||
<Flexbox horizontal gap={4}>
|
||||
<Button
|
||||
color="default"
|
||||
disabled={loading || isMessageCreating}
|
||||
size="small"
|
||||
variant="text"
|
||||
onClick={handleReject}
|
||||
>
|
||||
{t('tool.intervention.reject')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isMessageCreating}
|
||||
loading={loading}
|
||||
size="small"
|
||||
type="primary"
|
||||
variant="text"
|
||||
onClick={handleApprove}
|
||||
>
|
||||
{t('tool.intervention.approve')}
|
||||
</Button>
|
||||
</Flexbox>
|
||||
}
|
||||
>
|
||||
{BuiltinIntervention ? (
|
||||
<BuiltinIntervention
|
||||
apiName={tool.apiName}
|
||||
args={parsedArgs}
|
||||
identifier={tool.identifier}
|
||||
messageId={tool.result_msg_id || ''}
|
||||
/>
|
||||
) : (
|
||||
<pre style={{ fontSize: 12, margin: 0, whiteSpace: 'pre-wrap' }}>
|
||||
{JSON.stringify(parsedArgs, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</AccordionItem>
|
||||
);
|
||||
});
|
||||
|
||||
export default BatchToolItem;
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { Icon, Tag } from '@lobehub/ui';
|
||||
import { ShieldCheck } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const SessionBypassBadge = memo(() => {
|
||||
const { t } = useTranslation('chat');
|
||||
return (
|
||||
<Tag icon={<Icon icon={ShieldCheck} />} size="small">
|
||||
{t('tool.intervention.sessionBypassed')}
|
||||
</Tag>
|
||||
);
|
||||
});
|
||||
|
||||
export default SessionBypassBadge;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { type ChatToolPayloadWithResult } from '@lobechat/types';
|
||||
import { Flexbox } from '@lobehub/ui';
|
||||
import { Divider } from 'antd';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import BatchApprovalActions from './BatchApprovalActions';
|
||||
import BatchToolItem from './BatchToolItem';
|
||||
|
||||
interface BatchInterventionProps {
|
||||
assistantMessageId: string;
|
||||
auditType: string;
|
||||
tools: ChatToolPayloadWithResult[];
|
||||
}
|
||||
|
||||
const BatchIntervention = memo<BatchInterventionProps>(({ auditType, tools }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
|
||||
const toolMessageIds = tools
|
||||
.map((tool) => tool.result_msg_id)
|
||||
.filter((id): id is string => !!id && !id.startsWith('tmp_'));
|
||||
|
||||
return (
|
||||
<Flexbox
|
||||
gap={8}
|
||||
style={{
|
||||
border: '1px solid var(--lobe-color-border)',
|
||||
borderRadius: 'var(--lobe-border-radius)',
|
||||
padding: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 13, fontWeight: 500 }}>
|
||||
{t('tool.intervention.batchTitle', { count: tools.length })}
|
||||
</div>
|
||||
<Flexbox gap={4}>
|
||||
{tools.map((tool) => (
|
||||
<BatchToolItem key={tool.id} tool={tool} />
|
||||
))}
|
||||
</Flexbox>
|
||||
<Divider dashed style={{ margin: '4px 0' }} />
|
||||
<BatchApprovalActions auditType={auditType} toolMessageIds={toolMessageIds} />
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default BatchIntervention;
|
||||
+8
-1
@@ -1,7 +1,7 @@
|
||||
import { type ToolIntervention } from '@lobechat/types';
|
||||
import { Block, Icon, Tooltip } from '@lobehub/ui';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { Ban, Check, HandIcon, PauseIcon, X } from 'lucide-react';
|
||||
import { Ban, Check, HandIcon, PauseIcon, ShieldCheck, X } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -22,6 +22,7 @@ const StatusIndicator = memo<StatusIndicatorProps>(({ intervention, result }) =>
|
||||
const isPending = intervention?.status === 'pending';
|
||||
const isReject = intervention?.status === 'rejected';
|
||||
const isAbort = intervention?.status === 'aborted';
|
||||
const isSessionBypassed = intervention?.status === 'session_bypassed';
|
||||
|
||||
let icon;
|
||||
|
||||
@@ -39,6 +40,12 @@ const StatusIndicator = memo<StatusIndicatorProps>(({ intervention, result }) =>
|
||||
);
|
||||
} else if (hasError) {
|
||||
icon = <Icon color={cssVar.colorError} icon={X} />;
|
||||
} else if (isSessionBypassed) {
|
||||
icon = (
|
||||
<Tooltip title={t('tool.intervention.sessionBypassed')}>
|
||||
<Icon color={cssVar.colorSuccess} icon={ShieldCheck} />
|
||||
</Tooltip>
|
||||
);
|
||||
} else if (isPending) {
|
||||
icon = <Icon color={cssVar.colorInfo} icon={HandIcon} />;
|
||||
} else if (hasResult) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { type ChatToolPayloadWithResult } from '@lobechat/types';
|
||||
import { Flexbox } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
|
||||
import BatchIntervention from './BatchIntervention';
|
||||
import Tool from './Tool';
|
||||
|
||||
interface ToolsRendererProps {
|
||||
@@ -11,11 +12,51 @@ interface ToolsRendererProps {
|
||||
}
|
||||
|
||||
export const Tools = memo<ToolsRendererProps>(({ disableEditing, messageId, tools }) => {
|
||||
const { batchGroups, nonBatchTools } = useMemo(() => {
|
||||
if (!tools || tools.length === 0) return { batchGroups: [], nonBatchTools: [] };
|
||||
|
||||
const pendingByAuditType = new Map<string, ChatToolPayloadWithResult[]>();
|
||||
const others: ChatToolPayloadWithResult[] = [];
|
||||
|
||||
for (const tool of tools) {
|
||||
const isPending = tool.intervention?.status === 'pending';
|
||||
const auditType = tool.intervention?.auditType;
|
||||
|
||||
if (isPending && auditType) {
|
||||
const group = pendingByAuditType.get(auditType) ?? [];
|
||||
group.push(tool);
|
||||
pendingByAuditType.set(auditType, group);
|
||||
} else {
|
||||
others.push(tool);
|
||||
}
|
||||
}
|
||||
|
||||
// Only batch groups with 2+ tools; singles go to nonBatchTools
|
||||
const batches: Array<{ auditType: string; tools: ChatToolPayloadWithResult[] }> = [];
|
||||
for (const [auditType, groupedTools] of pendingByAuditType) {
|
||||
if (groupedTools.length > 1) {
|
||||
batches.push({ auditType, tools: groupedTools });
|
||||
} else {
|
||||
others.push(groupedTools[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return { batchGroups: batches, nonBatchTools: others };
|
||||
}, [tools]);
|
||||
|
||||
if (!tools || tools.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Flexbox gap={8}>
|
||||
{tools.map((tool) => (
|
||||
{batchGroups.map(({ auditType, tools: groupedTools }) => (
|
||||
<BatchIntervention
|
||||
assistantMessageId={messageId}
|
||||
auditType={auditType}
|
||||
key={`batch-${auditType}`}
|
||||
tools={groupedTools}
|
||||
/>
|
||||
))}
|
||||
{nonBatchTools.map((tool) => (
|
||||
<Tool
|
||||
apiName={tool.apiName}
|
||||
arguments={tool.arguments}
|
||||
|
||||
@@ -11,19 +11,11 @@ import { dataSelectors } from '../data/selectors';
|
||||
* Handles tool call approval and rejection
|
||||
*/
|
||||
export interface ToolAction {
|
||||
/**
|
||||
* Approve a tool call
|
||||
*/
|
||||
approveAllByAuditType: (auditType: string, assistantGroupId: string) => Promise<void>;
|
||||
approveToolCall: (toolMessageId: string, assistantGroupId: string) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Reject a tool call and continue the conversation
|
||||
*/
|
||||
bypassAuditForSession: (auditType: string, assistantGroupId: string) => Promise<void>;
|
||||
rejectAllByAuditType: (auditType: string, reason?: string) => Promise<void>;
|
||||
rejectAndContinueToolCall: (toolMessageId: string, reason?: string) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Reject a tool call
|
||||
*/
|
||||
rejectToolCall: (toolMessageId: string, reason?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -33,6 +25,44 @@ export const toolSlice: StateCreator<
|
||||
[],
|
||||
ToolAction
|
||||
> = (set, get) => ({
|
||||
approveAllByAuditType: async (auditType: string, assistantGroupId: string) => {
|
||||
const state = get();
|
||||
const { context, hooks, waitForPendingArgsUpdate } = state;
|
||||
const allMessages = dataSelectors.dbMessages(state);
|
||||
const pendingIds = allMessages
|
||||
.filter(
|
||||
(m) =>
|
||||
m.role === 'tool' &&
|
||||
!m.id.startsWith('tmp_') &&
|
||||
m.pluginIntervention?.status === 'pending' &&
|
||||
m.pluginIntervention?.auditType === auditType,
|
||||
)
|
||||
.map((m) => m.id);
|
||||
|
||||
if (pendingIds.length === 0) return;
|
||||
|
||||
// Wait for all pending args updates before batch approval
|
||||
await Promise.all(pendingIds.map((id) => waitForPendingArgsUpdate(id)));
|
||||
|
||||
// Fire onToolApproved hook for each tool
|
||||
if (hooks.onToolApproved) {
|
||||
for (const id of pendingIds) {
|
||||
const shouldProceed = await hooks.onToolApproved(id);
|
||||
if (shouldProceed === false) return;
|
||||
}
|
||||
}
|
||||
|
||||
const chatStore = useChatStore.getState();
|
||||
await chatStore.approveAllToolCallings(pendingIds, assistantGroupId, context);
|
||||
|
||||
// Fire onToolCallComplete hook for each tool
|
||||
if (hooks.onToolCallComplete) {
|
||||
for (const id of pendingIds) {
|
||||
hooks.onToolCallComplete(id, undefined);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
approveToolCall: async (toolMessageId: string, assistantGroupId: string) => {
|
||||
const state = get();
|
||||
const { hooks, context, waitForPendingArgsUpdate } = state;
|
||||
@@ -56,6 +86,30 @@ export const toolSlice: StateCreator<
|
||||
}
|
||||
},
|
||||
|
||||
bypassAuditForSession: async (auditType: string, assistantGroupId: string) => {
|
||||
const chatStore = useChatStore.getState();
|
||||
chatStore.bypassAuditForSession(auditType);
|
||||
await get().approveAllByAuditType(auditType, assistantGroupId);
|
||||
},
|
||||
|
||||
rejectAllByAuditType: async (auditType: string, reason?: string) => {
|
||||
const state = get();
|
||||
const allMessages = dataSelectors.dbMessages(state);
|
||||
const pendingIds = allMessages
|
||||
.filter(
|
||||
(m) =>
|
||||
m.role === 'tool' &&
|
||||
!m.id.startsWith('tmp_') &&
|
||||
m.pluginIntervention?.status === 'pending' &&
|
||||
m.pluginIntervention?.auditType === auditType,
|
||||
)
|
||||
.map((m) => m.id);
|
||||
|
||||
for (const id of pendingIds) {
|
||||
await get().rejectToolCall(id, reason);
|
||||
}
|
||||
},
|
||||
|
||||
rejectAndContinueToolCall: async (toolMessageId: string, reason?: string) => {
|
||||
const { context, waitForPendingArgsUpdate } = get();
|
||||
|
||||
|
||||
@@ -443,6 +443,12 @@ export default {
|
||||
'tool.intervention.rejectedWithReason': 'This Skill call was rejected: {{reason}}',
|
||||
'tool.intervention.toolAbort': 'You canceled this Skill call',
|
||||
'tool.intervention.toolRejected': 'This Skill call was rejected',
|
||||
'tool.intervention.approveAll': 'Approve All',
|
||||
'tool.intervention.rejectAll': 'Reject All',
|
||||
'tool.intervention.allowSession': 'Allow for This Session',
|
||||
'tool.intervention.sessionBypassed': 'Auto-approved (session authorized)',
|
||||
'tool.intervention.batchTitle_one': '{{count}} operation requires approval',
|
||||
'tool.intervention.batchTitle_other': '{{count}} operations require approval',
|
||||
'toolAuth.authorize': 'Authorize',
|
||||
'toolAuth.authorizing': 'Authorizing...',
|
||||
'toolAuth.hint':
|
||||
|
||||
@@ -694,6 +694,10 @@ export const createAgentExecutors = (context: {
|
||||
groupId: assistantMessage?.groupId,
|
||||
parentId: payload.parentMessageId,
|
||||
plugin: chatToolPayload,
|
||||
pluginIntervention:
|
||||
chatToolPayload.intervention?.status === 'session_bypassed'
|
||||
? { status: 'session_bypassed', auditType: chatToolPayload.intervention.auditType }
|
||||
: undefined,
|
||||
role: 'tool',
|
||||
agentId: effectiveAgentId!,
|
||||
threadId: opContext.threadId,
|
||||
@@ -1025,7 +1029,10 @@ export const createAgentExecutors = (context: {
|
||||
plugin: {
|
||||
...toolPayload,
|
||||
},
|
||||
pluginIntervention: { status: 'pending' },
|
||||
pluginIntervention: {
|
||||
auditType: toolPayload.intervention?.auditType,
|
||||
status: 'pending',
|
||||
},
|
||||
role: 'tool',
|
||||
agentId: effectiveAgentId!,
|
||||
threadId: opContext.threadId,
|
||||
|
||||
@@ -190,6 +190,103 @@ export class ConversationControlActionImpl {
|
||||
}
|
||||
};
|
||||
|
||||
approveAllToolCallings = async (
|
||||
toolMessageIds: string[],
|
||||
_assistantGroupId: string,
|
||||
context?: ConversationContext,
|
||||
): Promise<void> => {
|
||||
const { internal_execAgentRuntime, startOperation, completeOperation } = this.#get();
|
||||
|
||||
const effectiveContext: ConversationContext = context ?? {
|
||||
agentId: this.#get().activeAgentId,
|
||||
topicId: this.#get().activeTopicId,
|
||||
threadId: this.#get().activeThreadId,
|
||||
};
|
||||
|
||||
const { agentId, topicId, threadId, scope } = effectiveContext;
|
||||
|
||||
// Use the last tool message as parent for the resumed execution
|
||||
const parentMessageId = toolMessageIds.at(-1);
|
||||
if (!parentMessageId) return;
|
||||
|
||||
const { operationId } = startOperation({
|
||||
type: 'approveToolCalling',
|
||||
context: {
|
||||
agentId,
|
||||
topicId: topicId ?? undefined,
|
||||
threadId: threadId ?? undefined,
|
||||
scope,
|
||||
messageId: parentMessageId,
|
||||
},
|
||||
});
|
||||
|
||||
const optimisticContext = { operationId };
|
||||
|
||||
// Update all intervention statuses to approved
|
||||
const approvedToolCalls: unknown[] = [];
|
||||
for (const toolMsgId of toolMessageIds) {
|
||||
const toolMessage = dbMessageSelectors.getDbMessageById(toolMsgId)(this.#get());
|
||||
if (!toolMessage) continue;
|
||||
|
||||
await this.#get().optimisticUpdatePlugin(
|
||||
toolMsgId,
|
||||
{ intervention: { status: 'approved' } },
|
||||
optimisticContext,
|
||||
);
|
||||
|
||||
approvedToolCalls.push(toolMessage.plugin);
|
||||
}
|
||||
|
||||
// Bail out if no valid tool messages were found
|
||||
if (approvedToolCalls.length === 0) {
|
||||
completeOperation(operationId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current messages for state construction
|
||||
const chatKey = messageMapKey({ agentId, topicId, threadId, scope });
|
||||
const currentMessages = displayMessageSelectors.getDisplayMessagesByKey(chatKey)(this.#get());
|
||||
|
||||
const { state, context: initialContext } = this.#get().internal_createAgentState({
|
||||
messages: currentMessages,
|
||||
parentMessageId,
|
||||
agentId,
|
||||
topicId,
|
||||
threadId: threadId ?? undefined,
|
||||
operationId,
|
||||
});
|
||||
|
||||
const agentRuntimeContext: AgentRuntimeContext = {
|
||||
...initialContext,
|
||||
phase: 'human_approved_tools',
|
||||
payload: {
|
||||
approvedToolCalls,
|
||||
parentMessageId,
|
||||
skipCreateToolMessage: true,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await internal_execAgentRuntime({
|
||||
context: effectiveContext,
|
||||
messages: currentMessages,
|
||||
parentMessageId,
|
||||
parentMessageType: 'tool',
|
||||
initialState: state,
|
||||
initialContext: agentRuntimeContext,
|
||||
parentOperationId: operationId,
|
||||
});
|
||||
completeOperation(operationId);
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('[approveAllToolCallings] Error executing agent runtime:', err);
|
||||
this.#get().failOperation(operationId, {
|
||||
type: 'approveToolCalling',
|
||||
message: err.message || 'Unknown error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
rejectToolCalling = async (
|
||||
messageId: string,
|
||||
reason?: string,
|
||||
|
||||
@@ -9,6 +9,8 @@ import { type ConversationLifecycleAction } from './conversationLifecycle';
|
||||
import { ConversationLifecycleActionImpl } from './conversationLifecycle';
|
||||
import { type ChatMemoryAction } from './memory';
|
||||
import { ChatMemoryActionImpl } from './memory';
|
||||
import { type SessionBypassAction } from './sessionBypass';
|
||||
import { SessionBypassActionImpl } from './sessionBypass';
|
||||
import { type StreamingExecutorAction } from './streamingExecutor';
|
||||
import { StreamingExecutorActionImpl } from './streamingExecutor';
|
||||
import { type StreamingStatesAction } from './streamingStates';
|
||||
@@ -17,6 +19,7 @@ import { StreamingStatesActionImpl } from './streamingStates';
|
||||
export type ChatAIChatAction = ChatMemoryAction &
|
||||
ConversationLifecycleAction &
|
||||
ConversationControlAction &
|
||||
SessionBypassAction &
|
||||
StreamingExecutorAction &
|
||||
StreamingStatesAction;
|
||||
|
||||
@@ -34,6 +37,7 @@ export const chatAiChat: StateCreator<
|
||||
new ChatMemoryActionImpl(...params),
|
||||
new ConversationLifecycleActionImpl(...params),
|
||||
new ConversationControlActionImpl(...params),
|
||||
new SessionBypassActionImpl(...params),
|
||||
new StreamingExecutorActionImpl(...params),
|
||||
new StreamingStatesActionImpl(...params),
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { type ChatStore } from '@/store/chat/store';
|
||||
import { type StoreSetter } from '@/store/types';
|
||||
|
||||
type Setter = StoreSetter<ChatStore>;
|
||||
|
||||
export class SessionBypassActionImpl {
|
||||
readonly #get: () => ChatStore;
|
||||
readonly #set: Setter;
|
||||
|
||||
constructor(set: Setter, get: () => ChatStore, _api?: unknown) {
|
||||
void _api;
|
||||
this.#set = set;
|
||||
this.#get = get;
|
||||
}
|
||||
|
||||
bypassAuditForSession = (auditType: string): void => {
|
||||
const key = this.#getCurrentKey();
|
||||
const current = this.#get().sessionBypassedAudits[key] ?? [];
|
||||
if (current.includes(auditType)) return;
|
||||
|
||||
this.#set(
|
||||
{
|
||||
sessionBypassedAudits: {
|
||||
...this.#get().sessionBypassedAudits,
|
||||
[key]: [...current, auditType],
|
||||
},
|
||||
},
|
||||
false,
|
||||
'bypassAuditForSession',
|
||||
);
|
||||
};
|
||||
|
||||
getSessionBypassedAudits = (): string[] => {
|
||||
const key = this.#getCurrentKey();
|
||||
return this.#get().sessionBypassedAudits[key] ?? [];
|
||||
};
|
||||
|
||||
#getCurrentKey(): string {
|
||||
const { activeAgentId, activeTopicId } = this.#get();
|
||||
return `${activeAgentId}-${activeTopicId ?? 'default'}`;
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionBypassAction = Pick<SessionBypassActionImpl, keyof SessionBypassActionImpl>;
|
||||
@@ -194,6 +194,10 @@ export class StreamingExecutorActionImpl {
|
||||
const agentWorkingDirectory = agentSelectors.currentAgentWorkingDirectory(getAgentStoreState());
|
||||
const workingDirectory = topicWorkingDirectory ?? agentWorkingDirectory;
|
||||
|
||||
// Read session-bypassed audits for this conversation
|
||||
const conversationKey = `${agentId}-${topicId ?? 'default'}`;
|
||||
const sessionBypassedAudits = this.#get().sessionBypassedAudits[conversationKey] ?? [];
|
||||
|
||||
// Create initial state or use provided state
|
||||
const state =
|
||||
initialState ||
|
||||
@@ -214,6 +218,7 @@ export class StreamingExecutorActionImpl {
|
||||
sourceMap: {},
|
||||
tools: toolsDetailed.tools ?? [],
|
||||
},
|
||||
sessionBypassedAudits: sessionBypassedAudits.length > 0 ? sessionBypassedAudits : undefined,
|
||||
toolManifestMap,
|
||||
userInterventionConfig,
|
||||
});
|
||||
|
||||
@@ -5,6 +5,11 @@ export interface ChatAIChatState {
|
||||
inputMessage: string;
|
||||
mainInputEditor: ChatInputEditor | null;
|
||||
searchWorkflowLoadingIds: string[];
|
||||
/**
|
||||
* Session-bypassed audit types per conversation key.
|
||||
* Key format: `${agentId}-${topicId ?? 'default'}`
|
||||
*/
|
||||
sessionBypassedAudits: Record<string, string[]>;
|
||||
threadInputEditor: ChatInputEditor | null;
|
||||
/**
|
||||
* the tool calling stream ids
|
||||
@@ -17,6 +22,7 @@ export const initialAiChatState: ChatAIChatState = {
|
||||
inputMessage: '',
|
||||
mainInputEditor: null,
|
||||
searchWorkflowLoadingIds: [],
|
||||
sessionBypassedAudits: {},
|
||||
threadInputEditor: null,
|
||||
toolCallingStreamIds: {},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user