@meetopenbot/openbot 1.2.18 → 1.2.20
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/fast-forward.js +177 -0
- package/dist/runtime.js +157 -4
- package/dist/system-prompt.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { creditsProviderBaseUrl, resolveByokApiKey, resolveCreditsAuthConfig, shouldUseCreditsAuth, INTEGRATIONS_TOKEN_HEADER, } from '@meetopenbot/plugin-sdk';
|
|
2
|
+
import { isAutoModel } from './auto-model.js';
|
|
3
|
+
import { GENERAL_SPACE_ID } from './space-id.js';
|
|
4
|
+
import { isManuallyNamedThread } from './thread-naming.js';
|
|
5
|
+
/** Decisions model. Public beta supports only this id. */
|
|
6
|
+
export const DECISION_MODEL = 'gpt-6-luna';
|
|
7
|
+
/**
|
|
8
|
+
* Forward only when the predicate is confident the message is not OpenBot's
|
|
9
|
+
* to answer. The middle of the range falls through to the coordinator.
|
|
10
|
+
*/
|
|
11
|
+
export const SELF_ANSWER_FORWARD_MAX = 0.3;
|
|
12
|
+
const DECISION_TIMEOUT_MS = 2500;
|
|
13
|
+
const OPENAI_DECISIONS_URL = 'https://api.openai.com/v1/decisions';
|
|
14
|
+
const SELF_ANSWER_QUESTION = 'openbot_should_answer';
|
|
15
|
+
export const FAST_FORWARD_CLOSE_HINT = [
|
|
16
|
+
'## THIS TURN',
|
|
17
|
+
'You already asked the specialist. Do not call ask_agent again.',
|
|
18
|
+
'If they finished, close in one or two sentences and set ready_for_review.',
|
|
19
|
+
'If they are blocked on the human, set needs_input.',
|
|
20
|
+
'If they failed, say what blocked them.',
|
|
21
|
+
'Do not restate a successful reply.',
|
|
22
|
+
].join('\n');
|
|
23
|
+
function eventData(event) {
|
|
24
|
+
const data = event.data;
|
|
25
|
+
return data && typeof data === 'object' ? data : undefined;
|
|
26
|
+
}
|
|
27
|
+
function isUserInvoke(event) {
|
|
28
|
+
if (event.meta?.parentToolCallId)
|
|
29
|
+
return false;
|
|
30
|
+
if (event.type !== 'agent:invoke')
|
|
31
|
+
return false;
|
|
32
|
+
return eventData(event)?.role === 'user';
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Auto, dedicated Space, first user message, exactly one installed @mention,
|
|
36
|
+
* no attachments. Anything else stays on the coordinator.
|
|
37
|
+
*/
|
|
38
|
+
export function selectFastForwardCandidate(input) {
|
|
39
|
+
if (!isAutoModel(input.model))
|
|
40
|
+
return undefined;
|
|
41
|
+
if (!input.channelId || input.channelId === GENERAL_SPACE_ID)
|
|
42
|
+
return undefined;
|
|
43
|
+
const userMessages = input.events.filter(isUserInvoke);
|
|
44
|
+
if (userMessages.length !== 1)
|
|
45
|
+
return undefined;
|
|
46
|
+
const message = userMessages[0];
|
|
47
|
+
const data = eventData(message);
|
|
48
|
+
const attachments = data?.attachments;
|
|
49
|
+
if (Array.isArray(attachments) && attachments.length > 0)
|
|
50
|
+
return undefined;
|
|
51
|
+
const mentioned = Array.isArray(data?.mentionedAgentIds)
|
|
52
|
+
? data.mentionedAgentIds.filter((id) => typeof id === 'string' && id.length > 0)
|
|
53
|
+
: [];
|
|
54
|
+
if (mentioned.length !== 1)
|
|
55
|
+
return undefined;
|
|
56
|
+
const agentId = mentioned[0];
|
|
57
|
+
if (agentId === input.orchestratorAgentId)
|
|
58
|
+
return undefined;
|
|
59
|
+
const agent = input.installedAgents.find((item) => item.id === agentId);
|
|
60
|
+
if (!agent)
|
|
61
|
+
return undefined;
|
|
62
|
+
const prompt = typeof data?.content === 'string' ? data.content.trim() : '';
|
|
63
|
+
if (!prompt)
|
|
64
|
+
return undefined;
|
|
65
|
+
const alreadyAsked = input.events.some((event) => !event.meta?.parentToolCallId &&
|
|
66
|
+
(event.type === 'action:ask_agent' || event.type === 'action:delegate_task'));
|
|
67
|
+
if (alreadyAsked)
|
|
68
|
+
return undefined;
|
|
69
|
+
return { agentId, agentName: agent.name || agentId, prompt };
|
|
70
|
+
}
|
|
71
|
+
export function threadNeedsGeneratedTitle(state) {
|
|
72
|
+
const record = state && typeof state === 'object' ? state : {};
|
|
73
|
+
return record.isSmartNamed !== true && !isManuallyNamedThread(state);
|
|
74
|
+
}
|
|
75
|
+
export function sanitizeThreadTitle(raw) {
|
|
76
|
+
const line = raw.split('\n')[0]?.trim() ?? '';
|
|
77
|
+
const unquoted = line.replace(/^["'`]+|["'`]+$/g, '').trim();
|
|
78
|
+
const title = unquoted.replace(/\s+/g, ' ').slice(0, 80).trim();
|
|
79
|
+
return title || undefined;
|
|
80
|
+
}
|
|
81
|
+
/** Where to POST `/v1/decisions`. Credits when that mode is on, otherwise OpenAI BYOK. */
|
|
82
|
+
export function resolveDecisionEndpoint(options) {
|
|
83
|
+
if (shouldUseCreditsAuth({ authMode: options.authMode })) {
|
|
84
|
+
const config = resolveCreditsAuthConfig();
|
|
85
|
+
if (!config)
|
|
86
|
+
return undefined;
|
|
87
|
+
return {
|
|
88
|
+
url: `${creditsProviderBaseUrl(config, 'openai')}/decisions`,
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: `Bearer ${config.token}`,
|
|
91
|
+
[INTEGRATIONS_TOKEN_HEADER]: config.token,
|
|
92
|
+
'Content-Type': 'application/json',
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const apiKey = resolveByokApiKey('openai', [], options.env ?? process.env);
|
|
97
|
+
if (!apiKey)
|
|
98
|
+
return undefined;
|
|
99
|
+
return {
|
|
100
|
+
url: OPENAI_DECISIONS_URL,
|
|
101
|
+
headers: {
|
|
102
|
+
Authorization: `Bearer ${apiKey}`,
|
|
103
|
+
'Content-Type': 'application/json',
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export function interpretHandoffDecision(body) {
|
|
108
|
+
const answers = body && typeof body === 'object' && Array.isArray(body.answers)
|
|
109
|
+
? body.answers
|
|
110
|
+
: [];
|
|
111
|
+
const answer = answers.find((item) => item &&
|
|
112
|
+
typeof item === 'object' &&
|
|
113
|
+
item.name === SELF_ANSWER_QUESTION);
|
|
114
|
+
if (!answer)
|
|
115
|
+
return { forward: false, reason: 'no decision answer' };
|
|
116
|
+
if (answer.type === 'refusal')
|
|
117
|
+
return { forward: false, reason: 'decision refused' };
|
|
118
|
+
if (answer.type !== 'predicate' || typeof answer.probability !== 'number') {
|
|
119
|
+
return { forward: false, reason: 'decision was not a probability' };
|
|
120
|
+
}
|
|
121
|
+
if (!Number.isFinite(answer.probability)) {
|
|
122
|
+
return { forward: false, reason: 'decision was not a probability' };
|
|
123
|
+
}
|
|
124
|
+
if (answer.probability > SELF_ANSWER_FORWARD_MAX) {
|
|
125
|
+
return {
|
|
126
|
+
forward: false,
|
|
127
|
+
probability: answer.probability,
|
|
128
|
+
reason: `self-answer ${answer.probability}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return { forward: true, probability: answer.probability, reason: 'forward' };
|
|
132
|
+
}
|
|
133
|
+
function decisionInput(agentName, agentId, prompt) {
|
|
134
|
+
return `Specialist: ${agentName} (${agentId})\n\nMessage:\n${prompt}`;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* One predicate: should OpenBot answer this instead of the mentioned specialist?
|
|
138
|
+
* Failures and low confidence return `forward: false` so the normal turn runs.
|
|
139
|
+
*/
|
|
140
|
+
export async function classifySpecialistHandoff(args) {
|
|
141
|
+
if (args.signal?.aborted)
|
|
142
|
+
return { forward: false, reason: 'aborted' };
|
|
143
|
+
const endpoint = resolveDecisionEndpoint({ authMode: args.authMode, env: args.env });
|
|
144
|
+
if (!endpoint)
|
|
145
|
+
return { forward: false, reason: 'no decisions auth' };
|
|
146
|
+
const timeout = AbortSignal.timeout(args.timeoutMs ?? DECISION_TIMEOUT_MS);
|
|
147
|
+
const signal = args.signal ? AbortSignal.any([args.signal, timeout]) : timeout;
|
|
148
|
+
const fetchImpl = args.fetchImpl ?? fetch;
|
|
149
|
+
try {
|
|
150
|
+
const response = await fetchImpl(endpoint.url, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: endpoint.headers,
|
|
153
|
+
signal,
|
|
154
|
+
body: JSON.stringify({
|
|
155
|
+
model: DECISION_MODEL,
|
|
156
|
+
input: decisionInput(args.agentName, args.agentId, args.prompt),
|
|
157
|
+
questions: [
|
|
158
|
+
{
|
|
159
|
+
type: 'predicate',
|
|
160
|
+
name: SELF_ANSWER_QUESTION,
|
|
161
|
+
instructions: 'The human @mentioned one specialist. High probability if OpenBot should answer this itself: a question about the workspace, people, setup, status, or Spaces, or small talk that is not a task for that specialist. Low probability if the message is work, a change, or a question that specialist should handle.',
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
}),
|
|
165
|
+
});
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
return { forward: false, reason: `decision HTTP ${response.status}` };
|
|
168
|
+
}
|
|
169
|
+
return interpretHandoffDecision(await response.json());
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (args.signal?.aborted)
|
|
173
|
+
return { forward: false, reason: 'aborted' };
|
|
174
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
175
|
+
return { forward: false, reason: message || 'decision failed' };
|
|
176
|
+
}
|
|
177
|
+
}
|
package/dist/runtime.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { generateText, streamText, stepCountIs } from 'ai';
|
|
2
3
|
import { BYOK_ENV_KEYS, isAuthErrorMessage, isCreditsErrorMessage, fetchModelRegistry, getProviderModelOptions, listApiKeyProviders, PROVIDER_API_KEY_LINKS, } from '@meetopenbot/plugin-sdk';
|
|
3
4
|
import { eventsToModelMessages } from './history.js';
|
|
4
5
|
import { addAttachmentParts, chooseTurnCandidates, eventsHaveAttachments, modelSupportsImages, } from './images.js';
|
|
5
6
|
import { buildContext } from './context.js';
|
|
6
7
|
import { OPENBOT_SYSTEM_PROMPT, withGithubGitInstructions, withUserInstructions, } from './system-prompt.js';
|
|
7
|
-
import { AUTO_MODEL_ID, isAutoModel, isRetryableModelError, missingByokApiKeyMessage, } from './auto-model.js';
|
|
8
|
+
import { AUTO_MODEL_ID, isAutoModel, isRetryableModelError, missingByokApiKeyMessage, pickAutoModel, } from './auto-model.js';
|
|
9
|
+
import { classifySpecialistHandoff, FAST_FORWARD_CLOSE_HINT, sanitizeThreadTitle, selectFastForwardCandidate, threadNeedsGeneratedTitle, } from './fast-forward.js';
|
|
8
10
|
import { reasoningForModel, resolveModel } from './model.js';
|
|
9
11
|
import { errorFromStreamPart } from './stream-error.js';
|
|
10
12
|
import { createAgentOutputBuffer, isOutputBoundaryPart } from './output-buffer.js';
|
|
@@ -86,6 +88,139 @@ function toAiTools(tools, args, flushPendingOutput) {
|
|
|
86
88
|
}
|
|
87
89
|
return mapped;
|
|
88
90
|
}
|
|
91
|
+
async function executeCoordinatorTool(args, tools, name, input) {
|
|
92
|
+
const def = tools[name];
|
|
93
|
+
if (!def?.execute)
|
|
94
|
+
return;
|
|
95
|
+
const toolCallId = `ff_${randomUUID()}`;
|
|
96
|
+
const threadId = args.threadId;
|
|
97
|
+
const agentId = args.context.agentId;
|
|
98
|
+
await args.emit({
|
|
99
|
+
type: `action:${name}`,
|
|
100
|
+
data: input,
|
|
101
|
+
meta: { toolCallId, agentId, threadId },
|
|
102
|
+
});
|
|
103
|
+
try {
|
|
104
|
+
const result = await def.execute(input, {
|
|
105
|
+
agentId,
|
|
106
|
+
channelId: args.state.channelId,
|
|
107
|
+
threadId,
|
|
108
|
+
runId: args.state.runId,
|
|
109
|
+
toolCallId,
|
|
110
|
+
abortSignal: args.context.abortSignal,
|
|
111
|
+
storage: args.context.storage,
|
|
112
|
+
host: args.context.host,
|
|
113
|
+
publicBaseUrl: args.context.publicBaseUrl,
|
|
114
|
+
config: args.context.config,
|
|
115
|
+
state: args.state,
|
|
116
|
+
emit: args.emit,
|
|
117
|
+
});
|
|
118
|
+
const data = typeof result === 'string'
|
|
119
|
+
? { success: true, output: result }
|
|
120
|
+
: { success: result.success !== false, ...result };
|
|
121
|
+
await args.emit({
|
|
122
|
+
type: `action:${name}:result`,
|
|
123
|
+
data,
|
|
124
|
+
meta: { toolCallId, agentId, threadId },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
129
|
+
await args.emit({
|
|
130
|
+
type: `action:${name}:result`,
|
|
131
|
+
data: { success: false, error: message, output: message },
|
|
132
|
+
meta: { toolCallId, agentId, threadId },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async function suggestThreadTitle(args) {
|
|
137
|
+
const model = pickAutoModel({ authMode: args.authMode });
|
|
138
|
+
const result = await generateText({
|
|
139
|
+
model: resolveModel(model, { authMode: args.authMode }),
|
|
140
|
+
system: 'Name this job. Reply with a concise human title of at most 6 words. No quotes.',
|
|
141
|
+
prompt: args.prompt,
|
|
142
|
+
maxOutputTokens: 24,
|
|
143
|
+
abortSignal: args.signal,
|
|
144
|
+
reasoning: reasoningForModel(model),
|
|
145
|
+
});
|
|
146
|
+
return sanitizeThreadTitle(result.text);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* On Auto, a first message in a dedicated Space that @mentions one installed
|
|
150
|
+
* agent can skip the coordinator and ask that specialist immediately.
|
|
151
|
+
* Returns true when the specialist already ran and the caller should close.
|
|
152
|
+
*/
|
|
153
|
+
async function* attemptFastForward(args, options) {
|
|
154
|
+
if (args.context.agentId !== options.host.orchestratorAgentId)
|
|
155
|
+
return false;
|
|
156
|
+
if (!options.tools.ask_agent?.execute || options.abortSignal?.aborted)
|
|
157
|
+
return false;
|
|
158
|
+
let installed = [];
|
|
159
|
+
try {
|
|
160
|
+
const agents = await options.storage.getAgents?.();
|
|
161
|
+
if (Array.isArray(agents)) {
|
|
162
|
+
installed = agents.flatMap((agent) => {
|
|
163
|
+
if (!agent || typeof agent.id !== 'string' || !agent.id)
|
|
164
|
+
return [];
|
|
165
|
+
const name = typeof agent.name === 'string' && agent.name.trim() ? agent.name.trim() : agent.id;
|
|
166
|
+
return [{ id: agent.id, name }];
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
console.warn('[openbot] fast-forward could not list agents', error);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
const candidate = selectFastForwardCandidate({
|
|
175
|
+
model: options.model,
|
|
176
|
+
channelId: args.state.channelId,
|
|
177
|
+
orchestratorAgentId: options.host.orchestratorAgentId,
|
|
178
|
+
installedAgents: installed,
|
|
179
|
+
events: options.events,
|
|
180
|
+
});
|
|
181
|
+
if (!candidate)
|
|
182
|
+
return false;
|
|
183
|
+
const decision = await classifySpecialistHandoff({
|
|
184
|
+
prompt: candidate.prompt,
|
|
185
|
+
agentId: candidate.agentId,
|
|
186
|
+
agentName: candidate.agentName,
|
|
187
|
+
authMode: options.authMode,
|
|
188
|
+
env: args.context.env,
|
|
189
|
+
signal: options.abortSignal,
|
|
190
|
+
});
|
|
191
|
+
if (!decision.forward) {
|
|
192
|
+
console.info(`[openbot] fast-forward skipped: ${decision.reason}`);
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
console.info(`[openbot] fast-forward ${candidate.agentId} (self-answer ${decision.probability ?? 'n/a'})`);
|
|
196
|
+
yield {
|
|
197
|
+
type: 'agent:output',
|
|
198
|
+
data: { content: `Asking ${candidate.agentName}.` },
|
|
199
|
+
meta: { agentId: args.context.agentId, threadId: args.threadId },
|
|
200
|
+
};
|
|
201
|
+
await executeCoordinatorTool(args, options.tools, 'set_thread_status', { status: 'working' });
|
|
202
|
+
const titleTask = (async () => {
|
|
203
|
+
const threadState = args.state.threadDetails?.state;
|
|
204
|
+
if (!threadNeedsGeneratedTitle(threadState))
|
|
205
|
+
return;
|
|
206
|
+
const title = await suggestThreadTitle({
|
|
207
|
+
prompt: candidate.prompt,
|
|
208
|
+
authMode: options.authMode,
|
|
209
|
+
signal: options.abortSignal,
|
|
210
|
+
});
|
|
211
|
+
if (!title || options.abortSignal?.aborted)
|
|
212
|
+
return;
|
|
213
|
+
await executeCoordinatorTool(args, options.tools, 'set_thread_title', { title });
|
|
214
|
+
})().catch((error) => {
|
|
215
|
+
console.warn('[openbot] fast-forward title failed', error);
|
|
216
|
+
});
|
|
217
|
+
await executeCoordinatorTool(args, options.tools, 'ask_agent', {
|
|
218
|
+
agentId: candidate.agentId,
|
|
219
|
+
prompt: candidate.prompt,
|
|
220
|
+
});
|
|
221
|
+
await titleTask;
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
89
224
|
export async function* runOpenbotTurn(args, options) {
|
|
90
225
|
const { model: modelString = AUTO_MODEL_ID, authMode = 'byok', instructions, storage, toolDefinitions = {}, abortSignal, host, } = options;
|
|
91
226
|
let configuredModelString = modelString;
|
|
@@ -95,8 +230,8 @@ export async function* runOpenbotTurn(args, options) {
|
|
|
95
230
|
const state = args.state;
|
|
96
231
|
state.model = configuredModelString;
|
|
97
232
|
const threadId = args.threadId || state.threadId;
|
|
98
|
-
|
|
99
|
-
|
|
233
|
+
let systemPrompt = await buildSystemPrompt(state, storage, instructions, host);
|
|
234
|
+
let events = await storage.getEvents({
|
|
100
235
|
channelId: state.channelId ?? '',
|
|
101
236
|
threadId,
|
|
102
237
|
});
|
|
@@ -136,6 +271,24 @@ export async function* runOpenbotTurn(args, options) {
|
|
|
136
271
|
const missingKey = missingByokApiKeyMessage(candidates[0], { authMode });
|
|
137
272
|
if (missingKey)
|
|
138
273
|
throw new Error(missingKey);
|
|
274
|
+
const forwarded = yield* attemptFastForward(args, {
|
|
275
|
+
model: configuredModelString,
|
|
276
|
+
authMode,
|
|
277
|
+
storage,
|
|
278
|
+
tools: toolDefinitions,
|
|
279
|
+
events: events,
|
|
280
|
+
abortSignal,
|
|
281
|
+
host,
|
|
282
|
+
});
|
|
283
|
+
if (abortSignal?.aborted)
|
|
284
|
+
return;
|
|
285
|
+
if (forwarded) {
|
|
286
|
+
events = await storage.getEvents({
|
|
287
|
+
channelId: state.channelId ?? '',
|
|
288
|
+
threadId,
|
|
289
|
+
});
|
|
290
|
+
systemPrompt = `${await buildSystemPrompt(state, storage, instructions, host)}\n\n${FAST_FORWARD_CLOSE_HINT}`;
|
|
291
|
+
}
|
|
139
292
|
let lastError;
|
|
140
293
|
let started = false;
|
|
141
294
|
for (let i = 0; i < candidates.length; i++) {
|
package/dist/system-prompt.js
CHANGED
|
@@ -15,7 +15,7 @@ export const OPENBOT_SYSTEM_PROMPT = [
|
|
|
15
15
|
"# THE AGENT LOOP",
|
|
16
16
|
"You operate in an iterative loop:",
|
|
17
17
|
"1. **Analyze Events**: Understand the human, mentioned people, the current Space, and `## SPACES`.",
|
|
18
|
-
"2. **Right Space**: Use `## SPACES` to see where work belongs. If the human's intent clearly fits a different existing Space than the one you are in — especially project work posted in `#general` —
|
|
18
|
+
"2. **Right Space**: Use `## SPACES` to see where work belongs. If the human's intent clearly fits a different existing Space than the one you are in — especially project work posted in `#general` — explain in text which Space it belongs in and why, then wait for the human to continue there. Do not move or copy their message. In `#general`, handle questions, listings, setup, and routing only; do not run project execution there. If they are already in the right Space, continue with `ask_agent`. If no Space fits, propose a concrete name and why, then wait for the human to agree before `create_channel` — unless they already named a new Space or explicitly asked you to create it. Never invent placeholder ids (`noop`, `temp`, `misc`, `untitled`).",
|
|
19
19
|
"3. **Ask people**: Use `ask_agent` to send a message to a specialist. Write it as a request to a colleague, not as a tool invocation.",
|
|
20
20
|
"4. **Wait**: Let the asked agent run in its own harness. Read their reply, then continue or ask someone else.",
|
|
21
21
|
"5. **Close**: The human already saw the specialist speak in this thread. Do not restate, re-bullet, or paraphrase their findings. Name who you asked, then add only what is new — a decision, a conflict, a next step, or a caveat. If nothing is new, close in one or two sentences.",
|
|
@@ -42,7 +42,7 @@ export const OPENBOT_SYSTEM_PROMPT = [
|
|
|
42
42
|
"- The current status is injected each turn as `## THREAD STATUS`.",
|
|
43
43
|
"",
|
|
44
44
|
"# OPERATIONAL GUIDELINES",
|
|
45
|
-
'- **Spaces**: Work lives in Spaces (product name for channels). `#general` is dispatch — setup, questions, listings, routing, and "what should we do next." Project, coding, design, and ops work belongs in a dedicated Space. Prefer existing Spaces. When the human is in the wrong Space,
|
|
45
|
+
'- **Spaces**: Work lives in Spaces (product name for channels). `#general` is dispatch — setup, questions, listings, routing, and "what should we do next." Project, coding, design, and ops work belongs in a dedicated Space. Prefer existing Spaces. When the human is in the wrong existing Space, explain the appropriate Space in text and wait for them to continue there. Never auto-move threads or messages. Create a new Space only when the human asked for one, or they agreed after you suggested it.',
|
|
46
46
|
"- **Asking**: You ask specialists with `ask_agent`. If `## MENTIONED AGENTS` is present, you MUST ask those people. You may also ask others if needed. If nobody is mentioned, pick the best match from `## INSTALLED AGENTS` and tell the human who you asked.",
|
|
47
47
|
"- **Models**: When you `ask_agent`, pick `model` from that agent's list in `## INSTALLED AGENTS` using the descriptions — cheaper/faster for simple work, stronger for complex coding or reasoning. Omit `model` to use their default. Do not reuse another agent's ids.",
|
|
48
48
|
"- **Closer**: After `ask_agent`, you are the closer, not a second author of their work. Don't repeat. Add or close. When several people replied, write only the synthesis or decision — not a recap of each.",
|