-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathintentHandlers.ts
More file actions
203 lines (188 loc) · 9.06 KB
/
Copy pathintentHandlers.ts
File metadata and controls
203 lines (188 loc) · 9.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
import { logger } from './logger.js';
import { BridgeMessage } from './types.js';
import { HandlerContext } from './handlerContext.js';
import { discoveredAgents, agentStatuses } from './agentDiscovery.js';
import { resolveTargetAgentId } from './agentRouting.js';
import { getIntentProviderMethod } from './intentDispatchPolicy.js';
import { setAgentRunning } from './websocket.js';
import { handleInvokeTool } from './openclawInvoker.js';
import { prepareFollowupFiles } from './providers/providerFileUtils.js';
/**
* Main entry point for action-based intents from SaaS.
* For dispatch_task, delegates to ctx.provider.dispatchTask() so that different
* providers (OpenClaw, ClaudeCode) can handle execution differently.
*/
export async function handleIntentAction(msg: BridgeMessage, ctx: HandlerContext, intentType: string): Promise<void> {
const contextTaskId = msg.contextTaskId ?? msg.taskId;
const { requestId, args, content, executionId } = msg;
const providerMethod = getIntentProviderMethod(intentType);
const targetId = resolveTargetAgentId(msg.agentId);
let agentInfo = discoveredAgents[targetId!];
if (!agentInfo) {
ctx.sendToSaas({ action: 'intent_result', requestId, agentId: targetId, intentType, providerMethod, executionId, contextTaskId, error: 'AGENT_NOT_FOUND' });
return;
}
let parsedArgs: any;
try {
parsedArgs = typeof args === 'string' && args.trim() ? JSON.parse(args) : (args ?? (content ? { message: content } : undefined));
} catch {
parsedArgs = args ?? (content ? { message: content } : undefined);
}
logger.debug('intent.received', { agentId: targetId, intentType, providerMethod, executionId, contextTaskId, rawArgs: args });
// ── dispatch_task: delegate to provider ──────────────────────────────────────
if (intentType === 'dispatch_task') {
const prompt = parsedArgs?.task || parsedArgs?.message || content || '';
if (!prompt) {
ctx.sendToSaas({ action: 'intent_result', requestId, agentId: targetId, intentType, executionId, contextTaskId, error: 'MISSING_INTENT_PAYLOAD' });
return;
}
const taskFolderName = parsedArgs?.taskFolderName ?? msg.taskFolderName;
const taskMode = parsedArgs?.taskMode as string | undefined;
const repoPath = parsedArgs?.repoPath as string | undefined;
const taskLogRelativePath = parsedArgs?.taskLogRelativePath as string | undefined;
// In repo mode, use the repository root as the working directory instead of the agent workspace.
const workingDir = (taskMode === 'repo' && repoPath) ? repoPath : agentInfo.workspace;
// Mark the agent as running immediately so the UI reflects activity from the start.
setAgentRunning(targetId!);
try {
await ctx.provider.dispatchTask(
{
agentId: targetId!,
taskId: contextTaskId || '',
prompt,
workingDir,
tools: parsedArgs?.tools,
taskFolderName,
skipSessionWipe: parsedArgs?.skipSessionWipe,
executionId,
taskMode,
repoPath,
taskLogRelativePath,
},
{
onStream: (event) => {
if (event?.type === 'assistant' || event?.type === 'tool_use' || event?.type === 'tool_result'
|| event?.kind === 'text_chunk' || event?.kind === 'tool_call' || event?.kind === 'tool_result') {
setAgentRunning(targetId!);
ctx.sendToSaas({ action: 'agent_stream', agentId: targetId, taskId: contextTaskId, event });
}
},
onMessage: (text) => {
setAgentRunning(targetId!);
ctx.sendToSaas({ action: 'agent_activity', agentId: targetId, taskId: contextTaskId, delta: text });
},
onModelDiscovered: (model) => {
ctx.sendToSaas({ action: 'task_model_update', taskId: contextTaskId, model });
},
onWaitingForInput: (prompt) => {
ctx.sendToSaas({ action: 'task_waiting_for_input', agentId: targetId, taskId: contextTaskId, prompt });
},
onComplete: (status, reason) => {
ctx.sendToSaas({ action: 'task_complete', agentId: targetId, taskId: contextTaskId, status, reason, source: 'provider' });
ctx.sendToSaas({ action: 'intent_result', requestId, agentId: targetId, intentType, providerMethod, executionId, contextTaskId, result: status });
},
}
);
} catch (err: any) {
logger.error('intent.dispatch_task.critical_error', { taskId: contextTaskId, error: err.message, stack: err.stack });
ctx.sendToSaas({
action: 'task_complete',
agentId: targetId,
taskId: contextTaskId,
status: 'failed',
reason: `Bridge provider exception: ${err.message}`,
source: 'bridge'
});
ctx.sendToSaas({
action: 'intent_result',
requestId,
agentId: targetId,
intentType,
providerMethod,
executionId,
contextTaskId,
error: 'BRIDGE_INTERNAL_ERROR'
});
}
return;
}
// ── followup / agent_command / init_ping: delegate to provider.sendToSession ─
if (intentType === 'followup' || intentType === 'agent_command') {
const message = parsedArgs?.message || content || '';
const taskFolderName: string | undefined = parsedArgs?.taskFolderName;
setAgentRunning(targetId!);
// Write followup input file and build output-file instruction for all providers.
// Providers with native session resume get only the output-file instruction.
// Stateless providers (codex, hermes-acp, gemini-acp, copilot-acp) also receive
// the prior agent_log.md as context so the agent knows what was done before.
const providerName = ctx.provider.providerName;
const hasNativeSession = providerName === 'claude-agent-sdk' || providerName === 'claude-code'
|| providerName === 'openclaw' || providerName === 'cursor';
let augmentedMessage = message;
if (intentType === 'followup' && (contextTaskId || taskFolderName)) {
try {
const { followupLogBlock, followupLogBlockWithHistory } = prepareFollowupFiles(contextTaskId || '', message, taskFolderName);
const block = hasNativeSession ? followupLogBlock : followupLogBlockWithHistory;
augmentedMessage = `<system>\n${block}\n</system>\n\n${message}`;
} catch (e: any) {
logger.warn('intent.followup_file_prep_failed', { taskId: contextTaskId, error: e?.message });
}
}
try {
await ctx.provider.sendToSession(
{
agentId: targetId!,
taskId: contextTaskId || '',
sessionId: parsedArgs?.sessionId || parsedArgs?.session_id,
sessionKey: parsedArgs?.sessionKey,
message: augmentedMessage,
intentType,
executionId,
taskFolderName,
},
{
onStream: (event) => {
if (event?.type === 'assistant' || event?.type === 'tool_use' || event?.type === 'tool_result'
|| event?.kind === 'text_chunk' || event?.kind === 'tool_call' || event?.kind === 'tool_result') {
setAgentRunning(targetId!);
ctx.sendToSaas({ action: 'agent_stream', agentId: targetId, taskId: contextTaskId, event });
}
},
onMessage: (text) => {
setAgentRunning(targetId!);
ctx.sendToSaas({ action: 'agent_activity', agentId: targetId, taskId: contextTaskId, delta: text });
},
onComplete: (status, reason) => {
// followup runs in the same session as the original task — emit task_complete so
// the backend transitions the task back to done/failed after the followup finishes.
ctx.sendToSaas({ action: 'task_complete', agentId: targetId, taskId: contextTaskId, status, reason, source: 'provider' });
ctx.sendToSaas({ action: 'intent_result', requestId, agentId: targetId, intentType, providerMethod, executionId, contextTaskId, result: status, ...(reason ? { error: reason } : {}) });
},
}
);
} catch (err: any) {
logger.error('intent.session_action.critical_error', { taskId: contextTaskId, error: err.message });
ctx.sendToSaas({
action: 'intent_result',
requestId,
agentId: targetId,
intentType,
providerMethod,
executionId,
contextTaskId,
error: `BRIDGE_INTERNAL_ERROR: ${err.message}`
});
}
return;
}
// ── Other intents (init_ping, etc.): fall through to OpenClaw HTTP invoker ───
if (!providerMethod) {
ctx.sendToSaas({ action: 'intent_result', requestId, agentId: targetId, intentType, executionId, contextTaskId, error: 'UNSUPPORTED_INTENT' });
return;
}
logger.debug('intent.request', { agentId: targetId, intentType, providerMethod, args: parsedArgs, executionId, contextTaskId });
await handleInvokeTool(msg, ctx, { intentType, providerMethod });
}
// Re-export so existing callers (messageHandlers.ts, OpenClawProvider) keep working
// without updating their import paths.
export { handleInvokeTool } from './openclawInvoker.js';