@meetopenbot/openbot 0.1.1
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/dist/cloud-mode.d.ts +5 -0
- package/dist/cloud-mode.js +4 -0
- package/dist/context.d.ts +11 -0
- package/dist/context.js +120 -0
- package/dist/history.d.ts +9 -0
- package/dist/history.js +145 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +77 -0
- package/dist/model.d.ts +2 -0
- package/dist/model.js +20 -0
- package/dist/runtime.d.ts +25 -0
- package/dist/runtime.js +401 -0
- package/dist/system-prompt.d.ts +1 -0
- package/dist/system-prompt.js +57 -0
- package/dist/tools/approval.d.ts +3 -0
- package/dist/tools/approval.js +130 -0
- package/dist/tools/bash.d.ts +3 -0
- package/dist/tools/bash.js +425 -0
- package/dist/tools/delegation.d.ts +3 -0
- package/dist/tools/delegation.js +130 -0
- package/dist/tools/memory.d.ts +3 -0
- package/dist/tools/memory.js +163 -0
- package/dist/tools/preview.d.ts +4 -0
- package/dist/tools/preview.js +269 -0
- package/dist/tools/storage.d.ts +57 -0
- package/dist/tools/storage.js +335 -0
- package/dist/tools/todo-service.d.ts +28 -0
- package/dist/tools/todo-service.js +93 -0
- package/dist/tools/todo.d.ts +3 -0
- package/dist/tools/todo.js +146 -0
- package/dist/tools/ui.d.ts +9 -0
- package/dist/tools/ui.js +120 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.js +12 -0
- package/dist/utils/paths.d.ts +3 -0
- package/dist/utils/paths.js +12 -0
- package/dist/utils/workspace-url.d.ts +5 -0
- package/dist/utils/workspace-url.js +6 -0
- package/package.json +32 -0
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { generateText } from 'ai';
|
|
2
|
+
import { eventsToModelMessages } from './history.js';
|
|
3
|
+
import { buildContext } from './context.js';
|
|
4
|
+
import { OPENBOT_SYSTEM_PROMPT } from './system-prompt.js';
|
|
5
|
+
import { resolveModel } from './model.js';
|
|
6
|
+
async function buildSystemPrompt(state, storage) {
|
|
7
|
+
const context = await buildContext(state, storage);
|
|
8
|
+
const sections = [OPENBOT_SYSTEM_PROMPT, '', context];
|
|
9
|
+
// Hardcoded naming hint logic
|
|
10
|
+
const threadState = state.threadDetails?.state;
|
|
11
|
+
if (!threadState?.isSmartNamed) {
|
|
12
|
+
sections.push('', '## SYSTEM HINT', 'This thread is unnamed. Please use the `patch_thread_details` tool to set a concise, descriptive, and regular `name` (e.g., "Project Brainstorming" instead of "project-brainstorm") in the thread state and set `isSmartNamed: true` in the same patch. Only do this once.');
|
|
13
|
+
}
|
|
14
|
+
return sections.join('\n');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Tracks tool-call IDs from one LLM turn until matching `:result` events arrive.
|
|
18
|
+
*
|
|
19
|
+
* Melony runs yielded `action:*` events depth-first, so parallel tool calls from
|
|
20
|
+
* a single `generateText` response execute one-by-one. We must wait for every ID
|
|
21
|
+
* in the batch before calling the LLM again — not after the first result.
|
|
22
|
+
*/
|
|
23
|
+
function createToolBatchTracker(state, storage, channelId, threadId) {
|
|
24
|
+
const save = async (ids) => {
|
|
25
|
+
if (!storage || !channelId || !threadId)
|
|
26
|
+
return;
|
|
27
|
+
try {
|
|
28
|
+
await storage.patchThreadState({
|
|
29
|
+
channelId,
|
|
30
|
+
threadId,
|
|
31
|
+
state: { pendingToolCallIds: ids },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.error('[openbot] Failed to persist pendingToolCallIds:', error);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
async startBatch(toolCallIds) {
|
|
40
|
+
state.pendingToolCallIds = [...toolCallIds];
|
|
41
|
+
await save(state.pendingToolCallIds);
|
|
42
|
+
},
|
|
43
|
+
async clear() {
|
|
44
|
+
state.pendingToolCallIds = undefined;
|
|
45
|
+
await save(undefined);
|
|
46
|
+
},
|
|
47
|
+
/** Returns true when this result completes the batch (time to call the LLM again). */
|
|
48
|
+
async recordResult(toolCallId) {
|
|
49
|
+
if (!state.pendingToolCallIds?.includes(toolCallId))
|
|
50
|
+
return false;
|
|
51
|
+
state.pendingToolCallIds = state.pendingToolCallIds.filter((id) => id !== toolCallId);
|
|
52
|
+
const done = state.pendingToolCallIds.length === 0;
|
|
53
|
+
if (done) {
|
|
54
|
+
state.pendingToolCallIds = undefined;
|
|
55
|
+
}
|
|
56
|
+
await save(state.pendingToolCallIds);
|
|
57
|
+
return done;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* OpenBot agent runtime.
|
|
63
|
+
*
|
|
64
|
+
* - One `generateText` call per `runLLM` (tools have no `execute`; SDK stops at 1 step).
|
|
65
|
+
* - Tool calls become `action:*` events; plugins emit `:result` when done.
|
|
66
|
+
* - When a full batch of results is in, `runLLM` runs again with updated history.
|
|
67
|
+
*/
|
|
68
|
+
export const openbotRuntime = (options) => (builder) => {
|
|
69
|
+
const { model: modelString = 'openai/gpt-4o-mini', authMode = 'byok', agentId, storage, toolDefinitions = {}, abortSignal, host, } = options;
|
|
70
|
+
let currentModelString = modelString;
|
|
71
|
+
let model = resolveModel(currentModelString, agentId);
|
|
72
|
+
const isCreditsCloudAgent = (id) => host.isCloudSystemAgent(id ?? '') && authMode === 'credits';
|
|
73
|
+
const runLLM = async function* (context, threadId, trigger) {
|
|
74
|
+
if (!storage)
|
|
75
|
+
return;
|
|
76
|
+
if (abortSignal?.aborted)
|
|
77
|
+
return;
|
|
78
|
+
const toolBatch = createToolBatchTracker(context.state, storage, context.state.channelId, threadId || context.state.threadId);
|
|
79
|
+
// Capture parent metadata for event enrichment
|
|
80
|
+
const triggerEvent = trigger || context.state.triggerEvent;
|
|
81
|
+
const parentAgentId = triggerEvent?.meta?.parentAgentId;
|
|
82
|
+
const parentToolCallId = triggerEvent?.meta?.parentToolCallId;
|
|
83
|
+
const state = context.state;
|
|
84
|
+
state.model = currentModelString;
|
|
85
|
+
const systemPrompt = await buildSystemPrompt(context.state, storage);
|
|
86
|
+
const events = await storage.getEvents({
|
|
87
|
+
channelId: context.state.channelId,
|
|
88
|
+
threadId: context.state.threadId,
|
|
89
|
+
});
|
|
90
|
+
const messages = eventsToModelMessages(events);
|
|
91
|
+
// console.log('systemPrompt:::::::\n', systemPrompt);
|
|
92
|
+
// console.log('messages:::::::\n', JSON.stringify(messages));
|
|
93
|
+
// console.log('toolDefinitions:::::::\n', JSON.stringify(toolDefinitions));
|
|
94
|
+
try {
|
|
95
|
+
// Single LLM request — tool execution happens externally via action:* handlers.
|
|
96
|
+
const result = await generateText({
|
|
97
|
+
model,
|
|
98
|
+
system: systemPrompt,
|
|
99
|
+
messages,
|
|
100
|
+
tools: toolDefinitions,
|
|
101
|
+
stopWhen: ({ steps }) => steps.length === 1,
|
|
102
|
+
allowSystemInMessages: true,
|
|
103
|
+
abortSignal,
|
|
104
|
+
});
|
|
105
|
+
const toolCalls = result.toolCalls ?? [];
|
|
106
|
+
// if (result.usage) {
|
|
107
|
+
// const usage = result.usage;
|
|
108
|
+
// yield {
|
|
109
|
+
// type: 'agent:usage',
|
|
110
|
+
// data: {
|
|
111
|
+
// usage: {
|
|
112
|
+
// promptTokens: usage.inputTokens,
|
|
113
|
+
// completionTokens: usage.outputTokens,
|
|
114
|
+
// totalTokens: usage.totalTokens,
|
|
115
|
+
// currentContextTokens: usage.inputTokens,
|
|
116
|
+
// contextBudget: getContextBudgetForModel(currentModelString),
|
|
117
|
+
// },
|
|
118
|
+
// model: currentModelString,
|
|
119
|
+
// },
|
|
120
|
+
// meta: {
|
|
121
|
+
// agentId: context.state.agentId,
|
|
122
|
+
// threadId,
|
|
123
|
+
// runId: context.state.runId,
|
|
124
|
+
// },
|
|
125
|
+
// } as OpenBotEvent;
|
|
126
|
+
// }
|
|
127
|
+
const outputMeta = {
|
|
128
|
+
agentId: context.state.agentId,
|
|
129
|
+
threadId,
|
|
130
|
+
parentAgentId,
|
|
131
|
+
parentToolCallId,
|
|
132
|
+
};
|
|
133
|
+
// Text before actions so history/UI show the model's intent first.
|
|
134
|
+
if (result.text) {
|
|
135
|
+
yield {
|
|
136
|
+
type: 'agent:output',
|
|
137
|
+
data: { content: result.text },
|
|
138
|
+
meta: outputMeta,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (toolCalls.length > 0) {
|
|
142
|
+
// when multiple tool calls are made, Melony runtime handles them one by one, thats why we need to start a new batch
|
|
143
|
+
await toolBatch.startBatch(toolCalls.map((tc) => tc.toolCallId));
|
|
144
|
+
for (const toolCall of toolCalls) {
|
|
145
|
+
yield {
|
|
146
|
+
type: `action:${toolCall.toolName}`,
|
|
147
|
+
data: toolCall.input,
|
|
148
|
+
meta: {
|
|
149
|
+
toolCallId: toolCall.toolCallId,
|
|
150
|
+
...outputMeta,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
// clear the tool batch if there are no tool calls
|
|
157
|
+
await toolBatch.clear();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
// Run was stopped — unwind quietly without surfacing an error.
|
|
162
|
+
if (abortSignal?.aborted)
|
|
163
|
+
return;
|
|
164
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
165
|
+
const isApiKeyError = errorMessage.includes('API key') ||
|
|
166
|
+
errorMessage.includes('401') ||
|
|
167
|
+
errorMessage.includes('Unauthorized') ||
|
|
168
|
+
errorMessage.includes('authentication');
|
|
169
|
+
if (isApiKeyError && !isCreditsCloudAgent(context.state.agentId)) {
|
|
170
|
+
const registry = await host.resolveModelRegistry();
|
|
171
|
+
const providerActions = host.listApiKeyProvidersFromRegistry(registry).map((provider) => ({
|
|
172
|
+
id: provider.id,
|
|
173
|
+
label: provider.label,
|
|
174
|
+
variant: 'primary',
|
|
175
|
+
}));
|
|
176
|
+
yield {
|
|
177
|
+
type: 'client:ui:widget',
|
|
178
|
+
data: {
|
|
179
|
+
kind: 'choice',
|
|
180
|
+
widgetId: `api_provider_selection_${Date.now()}`,
|
|
181
|
+
title: `Setup AI Provider`,
|
|
182
|
+
description: `Select a provider to continue.`,
|
|
183
|
+
actions: providerActions.length > 0
|
|
184
|
+
? providerActions
|
|
185
|
+
: [
|
|
186
|
+
{ id: 'openai', label: 'OpenAI', variant: 'primary' },
|
|
187
|
+
{ id: 'anthropic', label: 'Anthropic', variant: 'primary' },
|
|
188
|
+
{ id: 'google', label: 'Google', variant: 'primary' },
|
|
189
|
+
],
|
|
190
|
+
metadata: {
|
|
191
|
+
type: 'api_provider_selection',
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
meta: { agentId: context.state.agentId, threadId },
|
|
195
|
+
};
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
builder.on('agent:invoke', async function* (event, context) {
|
|
202
|
+
const routedTo = event.data?.agentId;
|
|
203
|
+
if (typeof routedTo === 'string' && routedTo && routedTo !== context.state.agentId) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// Capture user info from meta if available
|
|
207
|
+
if (event.meta?.userName) {
|
|
208
|
+
context.state.currentUser = {
|
|
209
|
+
userName: event.meta.userName,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const threadId = event.meta?.threadId || context.state.threadId;
|
|
213
|
+
// clear the tool batch if the agent is invoked
|
|
214
|
+
// this is to prevent the tool batch from being used for a new agent invocation
|
|
215
|
+
await createToolBatchTracker(context.state, storage, context.state.channelId, threadId).clear();
|
|
216
|
+
yield* runLLM(context, threadId, event);
|
|
217
|
+
});
|
|
218
|
+
// this is to handle the tool results from the tool calls
|
|
219
|
+
// because Melony runtime handles them one by one, thats why we need to record the result
|
|
220
|
+
builder.on('*', async function* (event, context) {
|
|
221
|
+
if (!event.type.endsWith(':result'))
|
|
222
|
+
return;
|
|
223
|
+
if (event.meta?.agentId !== context.state.agentId)
|
|
224
|
+
return;
|
|
225
|
+
const toolCallId = event.meta?.toolCallId;
|
|
226
|
+
// record the result of the tool call
|
|
227
|
+
if (!toolCallId ||
|
|
228
|
+
!(await createToolBatchTracker(context.state, storage, context.state.channelId, event.meta?.threadId || context.state.threadId).recordResult(toolCallId)))
|
|
229
|
+
return;
|
|
230
|
+
const threadId = event.meta?.threadId || context.state.threadId;
|
|
231
|
+
yield* runLLM(context, threadId);
|
|
232
|
+
});
|
|
233
|
+
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
234
|
+
const { metadata, values, actionId } = event.data;
|
|
235
|
+
const threadId = event.meta?.threadId || context.state.threadId;
|
|
236
|
+
if (isCreditsCloudAgent(context.state.agentId))
|
|
237
|
+
return;
|
|
238
|
+
if (metadata?.type === 'api_provider_selection') {
|
|
239
|
+
const provider = actionId;
|
|
240
|
+
const [_, ...rest] = currentModelString.split('/');
|
|
241
|
+
const currentModelId = rest.join('/');
|
|
242
|
+
const registry = await host.resolveModelRegistry();
|
|
243
|
+
const providerData = registry.providers?.[provider];
|
|
244
|
+
const providerLinks = {
|
|
245
|
+
openai: 'https://platform.openai.com/api-keys',
|
|
246
|
+
anthropic: 'https://console.anthropic.com/settings/keys',
|
|
247
|
+
google: 'https://aistudio.google.com/app/apikey',
|
|
248
|
+
};
|
|
249
|
+
const label = providerData?.label || provider;
|
|
250
|
+
const link = providerLinks[provider] || '';
|
|
251
|
+
const modelOptions = providerData?.models.map((m) => ({
|
|
252
|
+
label: m.label,
|
|
253
|
+
value: m.id,
|
|
254
|
+
}));
|
|
255
|
+
if (!modelOptions || modelOptions.length === 0) {
|
|
256
|
+
yield {
|
|
257
|
+
type: 'agent:output',
|
|
258
|
+
data: {
|
|
259
|
+
content: `No models are listed for **${label}** in the marketplace registry.`,
|
|
260
|
+
},
|
|
261
|
+
meta: { agentId: context.state.agentId },
|
|
262
|
+
};
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const defaultModel = modelOptions[0].value;
|
|
266
|
+
const defaultValue = modelOptions.find((m) => m.value === currentModelId)?.value || defaultModel;
|
|
267
|
+
yield {
|
|
268
|
+
type: 'client:ui:widget',
|
|
269
|
+
data: {
|
|
270
|
+
widgetId: event.data.widgetId,
|
|
271
|
+
kind: 'message',
|
|
272
|
+
title: 'Provider Selected',
|
|
273
|
+
body: `${label} provider was selected.`,
|
|
274
|
+
state: 'submitted',
|
|
275
|
+
display: 'collapsed',
|
|
276
|
+
disabled: true,
|
|
277
|
+
actions: [],
|
|
278
|
+
},
|
|
279
|
+
meta: { agentId: context.state.agentId, threadId },
|
|
280
|
+
};
|
|
281
|
+
yield {
|
|
282
|
+
type: 'client:ui:widget',
|
|
283
|
+
data: {
|
|
284
|
+
kind: 'form',
|
|
285
|
+
widgetId: `api_key_request_${Date.now()}`,
|
|
286
|
+
title: `${label} Setup`,
|
|
287
|
+
description: `Enter your API key and select a model.`,
|
|
288
|
+
fields: [
|
|
289
|
+
{
|
|
290
|
+
id: 'model',
|
|
291
|
+
label: 'Model',
|
|
292
|
+
type: 'select',
|
|
293
|
+
options: modelOptions,
|
|
294
|
+
required: true,
|
|
295
|
+
defaultValue,
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
id: 'apiKey',
|
|
299
|
+
label: 'API Key',
|
|
300
|
+
type: 'password',
|
|
301
|
+
description: `Get your key here: [${link}](${link})`,
|
|
302
|
+
placeholder: `sk-...`,
|
|
303
|
+
required: true,
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
submitLabel: 'Save & Continue',
|
|
307
|
+
metadata: {
|
|
308
|
+
type: 'api_key_request',
|
|
309
|
+
provider,
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
meta: { agentId: context.state.agentId, threadId },
|
|
313
|
+
};
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (metadata?.type !== 'api_key_request')
|
|
317
|
+
return;
|
|
318
|
+
if (!values?.apiKey || !values?.model)
|
|
319
|
+
return;
|
|
320
|
+
const provider = String(values.provider || metadata.provider);
|
|
321
|
+
const modelId = String(values.model).trim();
|
|
322
|
+
const apiKey = String(values.apiKey);
|
|
323
|
+
if (provider !== 'openai' && provider !== 'anthropic' && provider !== 'google') {
|
|
324
|
+
yield {
|
|
325
|
+
type: 'agent:output',
|
|
326
|
+
data: { content: `Unsupported provider: ${provider}` },
|
|
327
|
+
meta: { agentId: context.state.agentId },
|
|
328
|
+
};
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const envVar = provider === 'openai'
|
|
332
|
+
? 'OPENAI_API_KEY'
|
|
333
|
+
: provider === 'anthropic'
|
|
334
|
+
? 'ANTHROPIC_API_KEY'
|
|
335
|
+
: 'GOOGLE_GENERATIVE_AI_API_KEY';
|
|
336
|
+
const newModelString = `${provider}/${modelId}`;
|
|
337
|
+
if (!storage)
|
|
338
|
+
return;
|
|
339
|
+
try {
|
|
340
|
+
await storage.createVariable({ key: envVar, value: apiKey, secret: true });
|
|
341
|
+
process.env[envVar] = apiKey;
|
|
342
|
+
currentModelString = newModelString;
|
|
343
|
+
model = resolveModel(currentModelString, agentId);
|
|
344
|
+
try {
|
|
345
|
+
host.saveConfig({ model: currentModelString });
|
|
346
|
+
const details = await storage.getAgentDetails({ agentId: context.state.agentId });
|
|
347
|
+
const updatedPlugins = details.pluginRefs.map((ref) => {
|
|
348
|
+
if (ref.id === host.openbotPluginId) {
|
|
349
|
+
return {
|
|
350
|
+
...ref,
|
|
351
|
+
config: {
|
|
352
|
+
...ref.config,
|
|
353
|
+
model: currentModelString,
|
|
354
|
+
...(host.isCloudSystemAgent(context.state.agentId) ? { authMode: 'byok' } : {}),
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
return ref;
|
|
359
|
+
});
|
|
360
|
+
await storage.updateAgent({
|
|
361
|
+
agentId: context.state.agentId,
|
|
362
|
+
plugins: updatedPlugins,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
// best-effort: config persistence failure shouldn't block the conversation
|
|
367
|
+
}
|
|
368
|
+
yield {
|
|
369
|
+
type: 'agent:output',
|
|
370
|
+
data: {
|
|
371
|
+
content: `Saved ${provider} API key and set model to \`${newModelString}\`.`,
|
|
372
|
+
},
|
|
373
|
+
meta: { agentId: context.state.agentId },
|
|
374
|
+
};
|
|
375
|
+
yield {
|
|
376
|
+
type: 'client:ui:widget',
|
|
377
|
+
data: {
|
|
378
|
+
widgetId: event.data.widgetId,
|
|
379
|
+
kind: 'message',
|
|
380
|
+
title: 'API Key Saved',
|
|
381
|
+
body: `Successfully saved ${provider} API key and selected model \`${newModelString}\`. You can now continue your conversation.`,
|
|
382
|
+
state: 'submitted',
|
|
383
|
+
display: 'collapsed',
|
|
384
|
+
disabled: true,
|
|
385
|
+
actions: [],
|
|
386
|
+
},
|
|
387
|
+
meta: { agentId: context.state.agentId },
|
|
388
|
+
};
|
|
389
|
+
yield* runLLM(context, threadId);
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
yield {
|
|
393
|
+
type: 'agent:output',
|
|
394
|
+
data: {
|
|
395
|
+
content: `Failed to save API key: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
396
|
+
},
|
|
397
|
+
meta: { agentId: context.state.agentId },
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const OPENBOT_SYSTEM_PROMPT: string;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export const OPENBOT_SYSTEM_PROMPT = [
|
|
2
|
+
'# ROLE',
|
|
3
|
+
'You are an OpenBot, the main coordinator and router agent. Your primary role is to orchestrate specialized agents and manage tasks methodically to help the human achieve their goals.',
|
|
4
|
+
'',
|
|
5
|
+
'# SECURITY POLICY',
|
|
6
|
+
'- **CRITICAL**: Never request API keys, passwords, or sensitive credentials via text or UI widgets; these are managed deterministically via secure forms and must never enter your context.',
|
|
7
|
+
'- **Credential Guidance**: If an agent or tool requires credentials, inform the user they can be managed under "Settings > Variables".',
|
|
8
|
+
'',
|
|
9
|
+
'# CORE MISSION',
|
|
10
|
+
'You act as a high-level manager, ensuring the right specialized agent is working on the right task. However, when orchestrating or when specialized agents are not available, you are capable of executing complex steps yourself using a highly structured, stateful, and disciplined approach.',
|
|
11
|
+
'',
|
|
12
|
+
'# THE AGENT LOOP',
|
|
13
|
+
'You operate in an iterative agent loop to complete user-assigned tasks step-by-step:',
|
|
14
|
+
'1. **Analyze Events**: Understand user needs and the current state through the chronological event stream (focusing on latest user messages and execution/observation results).',
|
|
15
|
+
'2. **Select Tools**: Choose the next tool call based on current state, todos, and environment constraints. To maximize precision, choose only one tool call per iteration for complex tasks, or parallel calls if they are independent.',
|
|
16
|
+
'3. **Wait for Execution**: Let the system execute the tool action and add observations/results back into the event stream.',
|
|
17
|
+
'4. **Iterate**: Patiently repeat the above steps, analyzing results at each turn, until all steps of the task are completed.',
|
|
18
|
+
'5. **Submit Results**: Message the human with clear, polished, and detailed outcomes, attaching any relevant files or deliverables.',
|
|
19
|
+
'6. **Enter Standby**: Enter an idle state and wait for new tasks.',
|
|
20
|
+
'',
|
|
21
|
+
'# TASK TRACKING (TODOS)',
|
|
22
|
+
'- For any complex multi-step task (3+ steps), you **MUST** maintain a todo list with `todo_write`.',
|
|
23
|
+
'- Write the full intended list at task start. Each call replaces the previous list (not a partial patch).',
|
|
24
|
+
'- Keep exactly one item `in_progress` while working. Mark items `completed` immediately after finishing them.',
|
|
25
|
+
'- Use `cancelled` for steps that are no longer needed. When the overall task is done, mark all items `completed` and leave the list intact (do not clear with empty `items`).',
|
|
26
|
+
'- The current list is injected into context each turn as `## TODOS`; call `todo_read` only if you need an explicit refresh.',
|
|
27
|
+
'- Do not stop with open `pending` or `in_progress` items unless you are blocked and have told the user why.',
|
|
28
|
+
'',
|
|
29
|
+
'# OPERATIONAL GUIDELINES',
|
|
30
|
+
'- **Channel and Threads**: The main way to communicate and act is through channels and threads. There might be a channel called "uncategorized" for general purpose communication.',
|
|
31
|
+
'- **Delegation**: You can delegate tasks to any specialized agent in the `INSTALLED AGENTS` list using the `delegate` tool.',
|
|
32
|
+
'- **Durable Memory**: Use the `remember` tool to store important facts, preferences, or project details that should persist across sessions.',
|
|
33
|
+
'- **Hub-and-Spoke**: Specialized agents cannot communicate directly; as coordinator, you must pass relevant data from one agent to another.',
|
|
34
|
+
'',
|
|
35
|
+
'# SHELL & FILE EXECUTION RULES',
|
|
36
|
+
'- **Stateful Sessions**: Use `shell_exec` with a session `id` (e.g. `default`, `server`). Reuse the same id to keep shell state (cwd, env) across commands.',
|
|
37
|
+
'- **Working Directory**: Always pass an absolute `exec_dir` (use the channel workspace path from ENVIRONMENT).',
|
|
38
|
+
'- **Command Discipline**: Avoid interactive prompts when possible; use `-y`, `-f`, or non-interactive flags. For prompts, use `shell_write_to_process`.',
|
|
39
|
+
'- **Long-Running Processes**: Start dev servers with `shell_exec` using `&` at the end (e.g. `pnpm dev &`). If you forget `&`, `shell_exec` returns after ~15s with partial logs — the server may still be running.',
|
|
40
|
+
'- **Polling dev servers**: After starting (or after a timeout), poll with `shell_wait` (2–5s) then `shell_view` until logs show ready (URL/port). Do not start a duplicate server; reuse the same session id.',
|
|
41
|
+
'- **Stop dev servers**: Use `shell_kill_process` before restarting or when finished.',
|
|
42
|
+
'- **Preview URLs**: After a dev server is ready, use `expose_port` with its port to generate a public preview URL (stored on channel details as `previewUrl`). Expose tunnels are temporary; close with `unexpose_port`.',
|
|
43
|
+
'- **Calculations**: Use `bc` or Python for math. Do not calculate complex math mentally.',
|
|
44
|
+
'- **Chaining**: Chain related commands with `&&` in a single `shell_exec` when they must run sequentially.',
|
|
45
|
+
'',
|
|
46
|
+
'# COMMUNICATION & WRITING STYLE',
|
|
47
|
+
'- Be concise, professional, proactive, and polite.',
|
|
48
|
+
'- Confirm receipt of user messages quickly and outline your high-level strategy brief before executing a long series of steps.',
|
|
49
|
+
'- Inform the user via messages when you change methods or find strategic issues.',
|
|
50
|
+
'- Write in clean, continuous prose. Avoid excessive bullet points or lists in final responses unless explicitly requested.',
|
|
51
|
+
'- All major deliverables and documents must be highly detailed and complete; do not summarize or truncate final outputs unless specified.',
|
|
52
|
+
'',
|
|
53
|
+
'# ERROR HANDLING',
|
|
54
|
+
'- Tool failures are provided as events. When an error occurs, do not panic or immediately ask the user for help.',
|
|
55
|
+
'- Carefully analyze the stderr or failure logs, verify tool/argument names, and attempt alternative approaches or parameter fixes.',
|
|
56
|
+
'- Only report failure reasons and ask the human for help after multiple alternative approaches have failed.',
|
|
57
|
+
].join('\n');
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* `approval` — gates protected tool calls behind a UI confirmation widget.
|
|
4
|
+
*
|
|
5
|
+
* This is a simplified version that intercepts specified actions (default: bash)
|
|
6
|
+
* and requires user approval before they are allowed to proceed.
|
|
7
|
+
*/
|
|
8
|
+
// In-memory tracking for pending approval IDs with TTL (shared across plugin instances)
|
|
9
|
+
const pendingApprovals = new Map();
|
|
10
|
+
const TTL_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
11
|
+
export const approvalPlugin = {
|
|
12
|
+
id: 'approval',
|
|
13
|
+
name: 'Approval',
|
|
14
|
+
description: 'Gate protected tool calls behind a UI confirmation widget.',
|
|
15
|
+
factory: ({ config, storage }) => (builder) => {
|
|
16
|
+
// Actions that require approval. Defaults to bash.
|
|
17
|
+
const actionsToApprove = config.actions || ['action:shell_exec'];
|
|
18
|
+
for (const action of actionsToApprove) {
|
|
19
|
+
builder.intercept(action, (event, context) => {
|
|
20
|
+
// If already approved in this flow, let it pass to the actual handler
|
|
21
|
+
if (event.meta?.approvalStatus === 'approved')
|
|
22
|
+
return event;
|
|
23
|
+
// Otherwise, intercept and ask for approval via a UI widget
|
|
24
|
+
const displayData = JSON.stringify(event?.data) || '';
|
|
25
|
+
const widgetId = randomUUID();
|
|
26
|
+
pendingApprovals.set(widgetId, Date.now());
|
|
27
|
+
return {
|
|
28
|
+
type: 'client:ui:widget',
|
|
29
|
+
data: {
|
|
30
|
+
widgetId,
|
|
31
|
+
kind: 'message',
|
|
32
|
+
title: `The agent wants to perform \`${action}\``,
|
|
33
|
+
body: displayData,
|
|
34
|
+
metadata: {
|
|
35
|
+
type: 'approval:request',
|
|
36
|
+
originalEvent: event,
|
|
37
|
+
},
|
|
38
|
+
actions: [
|
|
39
|
+
{ id: 'approve', label: 'Approve', variant: 'primary' },
|
|
40
|
+
{ id: 'deny', label: 'Deny', variant: 'danger' },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
meta: { agentId: context.state.agentId, threadId: context.state.threadId },
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// Handle the user's response from the UI widget
|
|
48
|
+
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
49
|
+
const { widgetId, actionId } = event.data;
|
|
50
|
+
const metadata = event.data?.metadata;
|
|
51
|
+
if (metadata?.type !== 'approval:request')
|
|
52
|
+
return;
|
|
53
|
+
// Verify the widget is still pending and hasn't expired
|
|
54
|
+
if (!widgetId || !pendingApprovals.has(widgetId)) {
|
|
55
|
+
console.warn(`[approval] Received response for unknown or already handled widget: ${widgetId}`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const timestamp = pendingApprovals.get(widgetId);
|
|
59
|
+
if (Date.now() - timestamp > TTL_MS) {
|
|
60
|
+
pendingApprovals.delete(widgetId);
|
|
61
|
+
console.warn(`[approval] Received response for expired widget: ${widgetId}`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Mark as handled
|
|
65
|
+
pendingApprovals.delete(widgetId);
|
|
66
|
+
const originalEvent = metadata.originalEvent;
|
|
67
|
+
const approved = actionId === 'approve';
|
|
68
|
+
const displayData = JSON.stringify(event?.data) || '';
|
|
69
|
+
// Yield a "responded" widget update to the UI
|
|
70
|
+
yield {
|
|
71
|
+
type: 'client:ui:widget',
|
|
72
|
+
data: {
|
|
73
|
+
widgetId,
|
|
74
|
+
kind: 'message',
|
|
75
|
+
title: `Action ${approved ? 'Approved' : 'Denied'}`,
|
|
76
|
+
body: displayData,
|
|
77
|
+
state: approved ? 'submitted' : 'cancelled',
|
|
78
|
+
display: 'collapsed',
|
|
79
|
+
disabled: true,
|
|
80
|
+
actions: [], // Clear actions to disable buttons in UI
|
|
81
|
+
},
|
|
82
|
+
meta: { agentId: context.state.agentId, threadId: context.state.threadId },
|
|
83
|
+
};
|
|
84
|
+
if (approved) {
|
|
85
|
+
// Re-emit the original event with approved status so the actual handler can run
|
|
86
|
+
yield {
|
|
87
|
+
...originalEvent,
|
|
88
|
+
meta: {
|
|
89
|
+
...(originalEvent.meta || {}),
|
|
90
|
+
approvalStatus: 'approved',
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// Manually store the original event with denied status so it's recorded in history
|
|
96
|
+
// but NOT re-emitted to the pipeline (to avoid actual execution).
|
|
97
|
+
if (storage) {
|
|
98
|
+
await storage.storeEvent({
|
|
99
|
+
channelId: context.state.channelId,
|
|
100
|
+
threadId: context.state.threadId,
|
|
101
|
+
event: {
|
|
102
|
+
...originalEvent,
|
|
103
|
+
meta: {
|
|
104
|
+
...(originalEvent.meta || {}),
|
|
105
|
+
approvalStatus: 'denied',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// Emit a failure result event for the denied action to clear the pending tool batch
|
|
111
|
+
yield {
|
|
112
|
+
type: `${originalEvent.type}:result`,
|
|
113
|
+
data: {
|
|
114
|
+
success: false,
|
|
115
|
+
error: 'Action denied by user.',
|
|
116
|
+
stderr: 'Action denied by user.',
|
|
117
|
+
output: 'Action denied by user.',
|
|
118
|
+
},
|
|
119
|
+
meta: originalEvent.meta,
|
|
120
|
+
};
|
|
121
|
+
yield {
|
|
122
|
+
type: 'agent:output',
|
|
123
|
+
data: { content: `Action \`${originalEvent.type}\` was denied.` },
|
|
124
|
+
meta: { agentId: context.state.agentId },
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
export default approvalPlugin;
|