openbot 0.3.5 → 0.4.0
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/README.md +15 -16
- package/dist/app/agent-ids.js +4 -0
- package/dist/app/cli.js +1 -1
- package/dist/app/config.js +0 -19
- package/dist/app/server.js +8 -14
- package/dist/assets/icon.svg +9 -3
- package/dist/bus/services.js +78 -132
- package/dist/harness/agent-invoke-run.js +44 -0
- package/dist/harness/agent-turn.js +99 -0
- package/dist/harness/channel-participants.js +40 -0
- package/dist/harness/constants.js +2 -0
- package/dist/harness/context-meter.js +97 -0
- package/dist/harness/context.js +98 -45
- package/dist/harness/dispatch.js +144 -0
- package/dist/harness/dispatcher.js +45 -156
- package/dist/harness/history.js +177 -0
- package/dist/harness/index.js +91 -0
- package/dist/harness/orchestration.js +88 -0
- package/dist/harness/participants.js +22 -0
- package/dist/harness/run-harness.js +154 -0
- package/dist/harness/run.js +98 -0
- package/dist/harness/runtime-factory.js +0 -34
- package/dist/harness/runtime.js +57 -0
- package/dist/harness/todo-dispatch.js +51 -0
- package/dist/harness/todos.js +5 -0
- package/dist/harness/turn.js +79 -0
- package/dist/plugins/approval/index.js +105 -149
- package/dist/plugins/delegation/index.js +119 -32
- package/dist/plugins/memory/index.js +103 -14
- package/dist/plugins/memory/service.js +152 -0
- package/dist/plugins/openbot/context.js +80 -0
- package/dist/plugins/openbot/history.js +98 -0
- package/dist/plugins/openbot/index.js +31 -0
- package/dist/plugins/openbot/runtime.js +317 -0
- package/dist/plugins/openbot/system-prompt.js +5 -0
- package/dist/plugins/plugin-manager/index.js +105 -0
- package/dist/plugins/storage/index.js +573 -0
- package/dist/plugins/storage/service.js +1159 -0
- package/dist/plugins/storage-tools/index.js +2 -2
- package/dist/plugins/thread-namer/index.js +72 -0
- package/dist/plugins/thread-naming/generate-title.js +44 -0
- package/dist/plugins/thread-naming/index.js +103 -0
- package/dist/plugins/threads/index.js +114 -0
- package/dist/plugins/todo/index.js +24 -25
- package/dist/plugins/ui/index.js +2 -32
- package/dist/registry/plugins.js +3 -9
- package/dist/services/plugins/domain.js +1 -0
- package/dist/services/plugins/plugin-cache.js +9 -0
- package/dist/services/plugins/registry.js +110 -0
- package/dist/services/plugins/service.js +177 -0
- package/dist/services/plugins/types.js +1 -0
- package/dist/services/process.js +29 -0
- package/dist/services/storage.js +41 -15
- package/dist/services/thread-naming.js +81 -0
- package/docs/agents.md +16 -10
- package/docs/architecture.md +2 -2
- package/docs/plugins.md +6 -15
- package/docs/templates/AGENT.example.md +7 -13
- package/package.json +1 -2
- package/src/app/agent-ids.ts +5 -0
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +1 -31
- package/src/app/server.ts +8 -16
- package/src/app/types.ts +70 -190
- package/src/assets/icon.svg +9 -3
- package/src/harness/index.ts +145 -0
- package/src/plugins/approval/index.ts +91 -189
- package/src/plugins/delegation/index.ts +136 -39
- package/src/plugins/memory/index.ts +112 -15
- package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
- package/src/plugins/openbot/context.ts +91 -0
- package/src/plugins/openbot/history.ts +107 -0
- package/src/plugins/openbot/index.ts +37 -0
- package/src/plugins/openbot/runtime.ts +384 -0
- package/src/plugins/openbot/system-prompt.ts +7 -0
- package/src/plugins/plugin-manager/index.ts +122 -0
- package/src/plugins/shell/index.ts +1 -1
- package/src/plugins/storage/index.ts +633 -0
- package/src/{services/storage.ts → plugins/storage/service.ts} +257 -72
- package/src/{bus/types.ts → services/plugins/domain.ts} +20 -7
- package/src/services/plugins/plugin-cache.ts +13 -0
- package/src/{registry/plugins.ts → services/plugins/registry.ts} +25 -27
- package/src/services/{plugins.ts → plugins/service.ts} +96 -2
- package/src/{bus/plugin.ts → services/plugins/types.ts} +3 -3
- package/src/bus/services.ts +0 -908
- package/src/harness/context.ts +0 -356
- package/src/harness/dispatcher.ts +0 -379
- package/src/harness/mcp.ts +0 -78
- package/src/harness/runtime-factory.ts +0 -129
- package/src/harness/todo-advance.ts +0 -128
- package/src/plugins/ai-sdk/index.ts +0 -41
- package/src/plugins/ai-sdk/runtime.ts +0 -468
- package/src/plugins/ai-sdk/system-prompt.ts +0 -18
- package/src/plugins/mcp/index.ts +0 -128
- package/src/plugins/storage-tools/index.ts +0 -90
- package/src/plugins/todo/index.ts +0 -64
- package/src/plugins/ui/index.ts +0 -227
- /package/src/{harness → services}/process.ts +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel `participants` (from `state.json`) scope which agents may collaborate
|
|
3
|
+
* in that channel. Used for system-prompt hints and dispatch guards.
|
|
4
|
+
*/
|
|
5
|
+
/** Multi-participant channel: user messages always route to the orchestrator. */
|
|
6
|
+
export function isMultiAgentChannel(participants) {
|
|
7
|
+
return participants.length > 1;
|
|
8
|
+
}
|
|
9
|
+
/** Solo DM: exactly one participant and it is the acting agent (no peer bots). */
|
|
10
|
+
export function isDmSoloChannel(participants, actingAgentId) {
|
|
11
|
+
return participants.length === 1 && participants[0] === actingAgentId;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Resolve which agent handles an incoming user message.
|
|
15
|
+
* Multi-participant channels always route to the orchestrator (hub-and-spoke).
|
|
16
|
+
*/
|
|
17
|
+
export function resolveMessageTargetAgent(participants, orchestratorAgentId, requestedAgentId) {
|
|
18
|
+
if (isMultiAgentChannel(participants)) {
|
|
19
|
+
return orchestratorAgentId;
|
|
20
|
+
}
|
|
21
|
+
if (participants.length === 1) {
|
|
22
|
+
return requestedAgentId || participants[0];
|
|
23
|
+
}
|
|
24
|
+
return requestedAgentId || orchestratorAgentId;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* When `participants` is non-empty, todo dispatch targets must appear
|
|
28
|
+
* in that list. Solo DM forbids targeting any agent other than yourself (for
|
|
29
|
+
* chained steps); there are no peer bots.
|
|
30
|
+
*/
|
|
31
|
+
export function isParticipantDispatchAllowed(participants, actingAgentId, targetAgentId) {
|
|
32
|
+
if (participants.length === 0)
|
|
33
|
+
return true;
|
|
34
|
+
if (!participants.includes(targetAgentId))
|
|
35
|
+
return false;
|
|
36
|
+
if (isDmSoloChannel(participants, actingAgentId) && targetAgentId !== actingAgentId) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { OPENBOT_SYSTEM_PROMPT } from '../plugins/openbot/system-prompt.js';
|
|
2
|
+
import { ORCHESTRATOR_AGENT_ID, estimateTokens, } from './context.js';
|
|
3
|
+
import { reconstructHistory } from './history.js';
|
|
4
|
+
/** Reserved headroom for model output when computing fill percentage. */
|
|
5
|
+
export const CONTEXT_METER_OUTPUT_RESERVE = 4096;
|
|
6
|
+
const DEFAULT_CONTEXT_LIMIT = 128000;
|
|
7
|
+
const MODEL_CONTEXT_LIMITS = {
|
|
8
|
+
'gpt-4o': 128000,
|
|
9
|
+
'gpt-4o-mini': 128000,
|
|
10
|
+
'gpt-4-turbo': 128000,
|
|
11
|
+
'gpt-4': 128000,
|
|
12
|
+
'gpt-3.5-turbo': 16385,
|
|
13
|
+
'claude-3-5-sonnet-20240620': 200000,
|
|
14
|
+
'claude-3-5-sonnet-20241022': 200000,
|
|
15
|
+
'claude-3-opus-20240229': 200000,
|
|
16
|
+
'claude-3-haiku-20240307': 200000,
|
|
17
|
+
};
|
|
18
|
+
export function getModelContextLimit(modelString) {
|
|
19
|
+
const modelId = modelString.split('/').slice(1).join('/');
|
|
20
|
+
if (MODEL_CONTEXT_LIMITS[modelId])
|
|
21
|
+
return MODEL_CONTEXT_LIMITS[modelId];
|
|
22
|
+
if (modelId.includes('claude'))
|
|
23
|
+
return 200000;
|
|
24
|
+
return DEFAULT_CONTEXT_LIMIT;
|
|
25
|
+
}
|
|
26
|
+
function buildInstructions(state) {
|
|
27
|
+
if (state.agentId === ORCHESTRATOR_AGENT_ID) {
|
|
28
|
+
return state.agentDetails?.instructions?.trim() || OPENBOT_SYSTEM_PROMPT;
|
|
29
|
+
}
|
|
30
|
+
return OPENBOT_SYSTEM_PROMPT;
|
|
31
|
+
}
|
|
32
|
+
function estimateMessagesTokens(messages) {
|
|
33
|
+
if (messages.length === 0)
|
|
34
|
+
return 0;
|
|
35
|
+
return estimateTokens(JSON.stringify(messages));
|
|
36
|
+
}
|
|
37
|
+
function estimateToolsTokens(tools) {
|
|
38
|
+
const names = Object.keys(tools);
|
|
39
|
+
if (names.length === 0)
|
|
40
|
+
return 0;
|
|
41
|
+
return estimateTokens(JSON.stringify(tools));
|
|
42
|
+
}
|
|
43
|
+
function applyTrigger(messages, trigger) {
|
|
44
|
+
const triggerContent = trigger?.data?.content?.trim();
|
|
45
|
+
if (!triggerContent || !trigger)
|
|
46
|
+
return messages;
|
|
47
|
+
const role = (trigger.data?.role || 'user');
|
|
48
|
+
const last = messages[messages.length - 1];
|
|
49
|
+
const alreadyLast = last &&
|
|
50
|
+
last.role === role &&
|
|
51
|
+
typeof last.content === 'string' &&
|
|
52
|
+
last.content.trim() === triggerContent;
|
|
53
|
+
if (alreadyLast)
|
|
54
|
+
return messages;
|
|
55
|
+
return [...messages, { role, content: triggerContent }];
|
|
56
|
+
}
|
|
57
|
+
function computePercent(used, limit) {
|
|
58
|
+
const budget = Math.max(limit - CONTEXT_METER_OUTPUT_RESERVE, 1);
|
|
59
|
+
return Math.min(100, Math.round((used / budget) * 100));
|
|
60
|
+
}
|
|
61
|
+
export async function computeContextMeter(options) {
|
|
62
|
+
const { state, storage, modelString, contextEngine, toolDefinitions = {}, trigger, lastUsage, } = options;
|
|
63
|
+
const limit = getModelContextLimit(modelString);
|
|
64
|
+
const instructions = buildInstructions(state);
|
|
65
|
+
const contextBlock = await contextEngine.buildContext(state, storage);
|
|
66
|
+
const systemTokens = estimateTokens([instructions, contextBlock].filter(Boolean).join('\n\n'));
|
|
67
|
+
const events = await storage.getEvents({
|
|
68
|
+
channelId: state.channelId,
|
|
69
|
+
threadId: state.threadId,
|
|
70
|
+
});
|
|
71
|
+
const messages = applyTrigger(reconstructHistory(events), trigger);
|
|
72
|
+
const historyTokens = estimateMessagesTokens(messages);
|
|
73
|
+
const toolsTokens = estimateToolsTokens(toolDefinitions);
|
|
74
|
+
let used = systemTokens + historyTokens + toolsTokens;
|
|
75
|
+
let estimated = true;
|
|
76
|
+
if (lastUsage?.input && lastUsage.input > 0) {
|
|
77
|
+
used = lastUsage.input;
|
|
78
|
+
estimated = false;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
model: modelString,
|
|
82
|
+
limit,
|
|
83
|
+
used,
|
|
84
|
+
percent: computePercent(used, limit),
|
|
85
|
+
estimated,
|
|
86
|
+
breakdown: {
|
|
87
|
+
system: systemTokens,
|
|
88
|
+
history: historyTokens,
|
|
89
|
+
tools: toolsTokens,
|
|
90
|
+
},
|
|
91
|
+
messageCount: messages.length,
|
|
92
|
+
...(lastUsage ? { lastUsage } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export function contextMeterEvent(snapshot, meta) {
|
|
96
|
+
return { type: 'client:ui:context-meter', data: snapshot, meta };
|
|
97
|
+
}
|
package/dist/harness/context.js
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
import { isDmSoloChannel } from './channel-participants.js';
|
|
2
|
+
export const DEFAULT_CONTEXT_BUDGET = 8000;
|
|
3
|
+
/**
|
|
4
|
+
* Returns the known context window budget (in tokens) for a given model string.
|
|
5
|
+
* This is used to drive the context usage ring in the UI and to configure
|
|
6
|
+
* the prompt pruning budget.
|
|
7
|
+
*/
|
|
8
|
+
export const getContextBudgetForModel = (modelString) => {
|
|
9
|
+
const budgets = {
|
|
10
|
+
'openai/gpt-4o': 128000,
|
|
11
|
+
'openai/gpt-4o-mini': 128000,
|
|
12
|
+
'openai/o1-preview': 128000,
|
|
13
|
+
'openai/o1-mini': 128000,
|
|
14
|
+
'anthropic/claude-3-5-sonnet-20240620': 200000,
|
|
15
|
+
'anthropic/claude-3-5-sonnet-latest': 200000,
|
|
16
|
+
'anthropic/claude-3-opus-20240229': 200000,
|
|
17
|
+
'anthropic/claude-3-sonnet-20240229': 200000,
|
|
18
|
+
'anthropic/claude-3-haiku-20240307': 200000,
|
|
19
|
+
};
|
|
20
|
+
return budgets[modelString] || DEFAULT_CONTEXT_BUDGET;
|
|
21
|
+
};
|
|
22
|
+
/** Built-in orchestrator agent id (`~/.openbot/agents/system/AGENT.md` overrides instructions). */
|
|
23
|
+
export const ORCHESTRATOR_AGENT_ID = 'system';
|
|
1
24
|
/**
|
|
2
25
|
* Cheap, dependency-free token estimator. Roughly char/4 — fine for budget
|
|
3
26
|
* enforcement; can be swapped for a tokenizer-backed implementation later
|
|
@@ -55,66 +78,95 @@ export class ContextEngine {
|
|
|
55
78
|
*/
|
|
56
79
|
export function createDefaultContextEngine() {
|
|
57
80
|
const engine = new ContextEngine();
|
|
81
|
+
engine.registerProvider(new EnvironmentProvider());
|
|
82
|
+
engine.registerProvider(new ChannelSpecProvider());
|
|
58
83
|
engine.registerProvider(new AgentDetailsProvider());
|
|
59
|
-
engine.registerProvider(new ChannelDetailsProvider());
|
|
60
|
-
engine.registerProvider(new ThreadDetailsProvider());
|
|
61
84
|
engine.registerProvider(new TodoProvider());
|
|
62
85
|
engine.registerProvider(new MemoryProvider());
|
|
63
|
-
engine.registerProvider(new RecentEventsProvider());
|
|
86
|
+
// engine.registerProvider(new RecentEventsProvider());
|
|
64
87
|
engine.registerProcessor(new TokenBudgetProcessor());
|
|
65
88
|
return engine;
|
|
66
89
|
}
|
|
67
|
-
class
|
|
90
|
+
class EnvironmentProvider {
|
|
68
91
|
constructor() {
|
|
69
|
-
this.name = '
|
|
92
|
+
this.name = 'environment';
|
|
70
93
|
}
|
|
71
94
|
async provide(state) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
95
|
+
const { channelId, threadId, channelDetails, agentId, threadDetails } = state;
|
|
96
|
+
const participants = channelDetails?.participants || [];
|
|
97
|
+
const isDm = isDmSoloChannel(participants, agentId);
|
|
98
|
+
let content = '## ENVIRONMENT\n';
|
|
99
|
+
if (isDm) {
|
|
100
|
+
content += '- Mode: Direct Message (Solo)\n';
|
|
101
|
+
content += '- Context: You are in a private conversation. No other agents are present.\n';
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const channelName = channelDetails?.name || channelId;
|
|
105
|
+
content += `- Mode: Channel (#${channelName})\n`;
|
|
106
|
+
if (threadId) {
|
|
107
|
+
content += `- Thread: ${threadDetails?.name || threadId}\n`;
|
|
108
|
+
}
|
|
109
|
+
const peerIds = participants.filter((id) => id !== agentId);
|
|
110
|
+
if (peerIds.length > 0) {
|
|
111
|
+
content += `- Participants: ${peerIds.join(', ')}\n`;
|
|
112
|
+
content += ` (Use these plain ids for todo assignees and delegate_to_agent — no @ prefix.)\n`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
117
|
+
id: 'environment',
|
|
118
|
+
type: 'environment',
|
|
119
|
+
priority: 110,
|
|
120
|
+
content,
|
|
121
|
+
},
|
|
122
|
+
];
|
|
83
123
|
}
|
|
84
124
|
}
|
|
85
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Injects SPEC.md (`channelDetails.spec`). Kept distinct from EnvironmentProvider
|
|
127
|
+
* so each block gets its own truncate budget and channel rules survive long
|
|
128
|
+
* participant lists under {@link ITEM_HARD_CHAR_CAP}.
|
|
129
|
+
*/
|
|
130
|
+
class ChannelSpecProvider {
|
|
86
131
|
constructor() {
|
|
87
|
-
this.name = 'channel-
|
|
132
|
+
this.name = 'channel-spec';
|
|
88
133
|
}
|
|
89
134
|
async provide(state) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const spec = state.channelDetails.spec?.trim();
|
|
135
|
+
const raw = state.channelDetails?.spec;
|
|
136
|
+
const spec = typeof raw === 'string' ? raw.trim() : '';
|
|
93
137
|
if (!spec)
|
|
94
138
|
return [];
|
|
95
|
-
return [
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
139
|
+
return [
|
|
140
|
+
{
|
|
141
|
+
id: 'channel-spec',
|
|
142
|
+
type: 'channel-spec',
|
|
143
|
+
/** Below environment (110), above agent / {@link TokenBudgetProcessor.KEEP_FLOOR}. */
|
|
144
|
+
priority: 108,
|
|
145
|
+
content: `## CHANNEL SPECIFICATION (SPEC.md)\n` +
|
|
146
|
+
`Channel-level goals and constraints. Prefer these unless the user contradicts them.\n\n` +
|
|
147
|
+
`${spec}`,
|
|
148
|
+
},
|
|
149
|
+
];
|
|
101
150
|
}
|
|
102
151
|
}
|
|
103
|
-
class
|
|
152
|
+
class AgentDetailsProvider {
|
|
104
153
|
constructor() {
|
|
105
|
-
this.name = '
|
|
154
|
+
this.name = 'agent-details';
|
|
106
155
|
}
|
|
107
156
|
async provide(state) {
|
|
108
|
-
if (!state.
|
|
157
|
+
if (!state.agentDetails)
|
|
158
|
+
return [];
|
|
159
|
+
if (state.agentId === ORCHESTRATOR_AGENT_ID)
|
|
160
|
+
return [];
|
|
161
|
+
const instructions = state.agentDetails.instructions?.trim();
|
|
162
|
+
if (!instructions)
|
|
109
163
|
return [];
|
|
110
|
-
// For now, this provider is a placeholder for future state-based assembly.
|
|
111
|
-
// It currently only surfaces the thread name to provide basic context.
|
|
112
164
|
return [
|
|
113
165
|
{
|
|
114
|
-
id: '
|
|
115
|
-
type: '
|
|
116
|
-
priority:
|
|
117
|
-
content:
|
|
166
|
+
id: 'agent-details',
|
|
167
|
+
type: 'agent',
|
|
168
|
+
priority: 100,
|
|
169
|
+
content: `## AGENT: ${state.agentDetails.name}\n\n${instructions}`,
|
|
118
170
|
},
|
|
119
171
|
];
|
|
120
172
|
}
|
|
@@ -130,6 +182,8 @@ class TodoProvider {
|
|
|
130
182
|
this.name = 'todos';
|
|
131
183
|
}
|
|
132
184
|
async provide(state) {
|
|
185
|
+
if (state.agentId !== ORCHESTRATOR_AGENT_ID)
|
|
186
|
+
return [];
|
|
133
187
|
const raw = state.threadDetails?.state?.todos;
|
|
134
188
|
const todos = Array.isArray(raw) ? raw : [];
|
|
135
189
|
if (todos.length === 0)
|
|
@@ -160,9 +214,7 @@ class TodoProvider {
|
|
|
160
214
|
id: 'todos',
|
|
161
215
|
type: 'todos',
|
|
162
216
|
priority: 92,
|
|
163
|
-
content: `##
|
|
164
|
-
`Orchestrator authors with \`todo_write\`; assignees run one step at a time. ` +
|
|
165
|
-
`When an item is \`done\`, its captured output appears below so every agent can see prior steps without relying on merged chat history.\n\n` +
|
|
217
|
+
content: `## SHARED TODO PLAN (thread state)\n` +
|
|
166
218
|
`${formatted}`,
|
|
167
219
|
},
|
|
168
220
|
];
|
|
@@ -199,7 +251,7 @@ class MemoryProvider {
|
|
|
199
251
|
id: 'memory',
|
|
200
252
|
type: 'memory',
|
|
201
253
|
priority: 95,
|
|
202
|
-
content: `## Remembered facts\nTrust these unless the user contradicts them. Use \`forget\` to remove stale ones.\n\n${formatted}`,
|
|
254
|
+
content: `## Remembered global facts\nTrust these unless the user contradicts them. Use \`forget\` to remove stale ones.\n\n${formatted}`,
|
|
203
255
|
},
|
|
204
256
|
];
|
|
205
257
|
}
|
|
@@ -275,20 +327,21 @@ class RecentEventsProvider {
|
|
|
275
327
|
}
|
|
276
328
|
/**
|
|
277
329
|
* Drops the lowest-priority items until the assembled prompt fits within the
|
|
278
|
-
* token budget. The first item with priority >=
|
|
330
|
+
* token budget. The first item with priority >= \`keepFloor\` is always kept,
|
|
279
331
|
* so the agent's own instructions can never be evicted. Stable on ties:
|
|
280
332
|
* later-emitted items go first.
|
|
281
333
|
*/
|
|
282
334
|
export class TokenBudgetProcessor {
|
|
283
|
-
constructor(budget =
|
|
335
|
+
constructor(budget = undefined, keepFloor = TokenBudgetProcessor.KEEP_FLOOR) {
|
|
284
336
|
this.budget = budget;
|
|
285
337
|
this.keepFloor = keepFloor;
|
|
286
338
|
this.name = 'token-budget';
|
|
287
339
|
}
|
|
288
|
-
async process(items) {
|
|
340
|
+
async process(items, state) {
|
|
289
341
|
const sorted = [...items].sort((a, b) => b.priority - a.priority);
|
|
290
342
|
const out = [];
|
|
291
343
|
let used = 0;
|
|
344
|
+
const activeBudget = this.budget ?? (state.model ? getContextBudgetForModel(state.model) : TokenBudgetProcessor.DEFAULT_BUDGET);
|
|
292
345
|
for (const item of sorted) {
|
|
293
346
|
const cost = estimateTokens(item.content);
|
|
294
347
|
if (item.priority >= this.keepFloor) {
|
|
@@ -296,7 +349,7 @@ export class TokenBudgetProcessor {
|
|
|
296
349
|
used += cost;
|
|
297
350
|
continue;
|
|
298
351
|
}
|
|
299
|
-
if (used + cost <=
|
|
352
|
+
if (used + cost <= activeBudget) {
|
|
300
353
|
out.push(item);
|
|
301
354
|
used += cost;
|
|
302
355
|
}
|
|
@@ -305,6 +358,6 @@ export class TokenBudgetProcessor {
|
|
|
305
358
|
}
|
|
306
359
|
}
|
|
307
360
|
/** Soft prompt budget in tokens (matches gpt-4o-mini's reasonable system slice). */
|
|
308
|
-
TokenBudgetProcessor.DEFAULT_BUDGET =
|
|
361
|
+
TokenBudgetProcessor.DEFAULT_BUDGET = DEFAULT_CONTEXT_BUDGET;
|
|
309
362
|
/** Items at or above this priority are never dropped. */
|
|
310
363
|
TokenBudgetProcessor.KEEP_FLOOR = 100;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { ensureEventId } from '../app/utils.js';
|
|
2
|
+
import { storageService } from '../services/storage.js';
|
|
3
|
+
import { ORCHESTRATOR_AGENT_ID } from './constants.js';
|
|
4
|
+
import { runTurn } from './run.js';
|
|
5
|
+
const stopRequests = [];
|
|
6
|
+
const STOP_REQUEST_TTL_MS = 30 * 60 * 1000;
|
|
7
|
+
const pruneStopRequests = () => {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
for (let i = stopRequests.length - 1; i >= 0; i -= 1) {
|
|
10
|
+
if (now - stopRequests[i].requestedAt > STOP_REQUEST_TTL_MS) {
|
|
11
|
+
stopRequests.splice(i, 1);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const findStopRequest = (target) => {
|
|
16
|
+
pruneStopRequests();
|
|
17
|
+
return stopRequests.find((r) => {
|
|
18
|
+
if (r.runId !== target.runId)
|
|
19
|
+
return false;
|
|
20
|
+
if (r.agentId && r.agentId !== target.agentId)
|
|
21
|
+
return false;
|
|
22
|
+
if (r.channelId && r.channelId !== target.channelId)
|
|
23
|
+
return false;
|
|
24
|
+
if (r.threadId && r.threadId !== target.threadId)
|
|
25
|
+
return false;
|
|
26
|
+
return true;
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
export async function dispatch(options) {
|
|
30
|
+
const { runId, channelId, onEvent } = options;
|
|
31
|
+
let { threadId } = options;
|
|
32
|
+
const { event } = options;
|
|
33
|
+
ensureEventId(event);
|
|
34
|
+
if (event.type === 'action:agent_run_stop') {
|
|
35
|
+
await handleStop(event, options);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const agentId = options.agentId || ORCHESTRATOR_AGENT_ID;
|
|
39
|
+
let turnEvent = event;
|
|
40
|
+
if (event.type === 'user:input' || event.type === 'agent:invoke') {
|
|
41
|
+
turnEvent = await normalizeUserInput(event, { runId, channelId, threadId, onEvent });
|
|
42
|
+
if (event.type === 'user:input') {
|
|
43
|
+
threadId = turnEvent.meta?.threadId || threadId || event.id;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const target = { runId, agentId, channelId, threadId };
|
|
47
|
+
const preStop = findStopRequest(target);
|
|
48
|
+
if (preStop) {
|
|
49
|
+
const state = await storageService.getOpenBotState({ ...target, event: turnEvent });
|
|
50
|
+
await onEvent({ type: 'agent:run:stopped', data: { ...target, reason: preStop.reason } }, state);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let state;
|
|
54
|
+
try {
|
|
55
|
+
state = await storageService.getOpenBotState({ ...target, event: turnEvent });
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error.code === 'AGENT_NOT_FOUND') {
|
|
59
|
+
const fallback = await storageService.getOpenBotState({
|
|
60
|
+
...target,
|
|
61
|
+
agentId: ORCHESTRATOR_AGENT_ID,
|
|
62
|
+
event: turnEvent,
|
|
63
|
+
});
|
|
64
|
+
await onEvent({
|
|
65
|
+
type: 'agent:output',
|
|
66
|
+
data: {
|
|
67
|
+
content: `⚠️ Agent **${agentId}** does not exist. Use participant ids without an @ prefix.`,
|
|
68
|
+
},
|
|
69
|
+
meta: { agentId: ORCHESTRATOR_AGENT_ID, threadId },
|
|
70
|
+
}, fallback);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
const turn = runTurn({ runId, channelId, threadId, agentId, event: turnEvent });
|
|
76
|
+
let next = await turn.next();
|
|
77
|
+
while (!next.done) {
|
|
78
|
+
const chunk = next.value;
|
|
79
|
+
const stop = findStopRequest(target);
|
|
80
|
+
if (stop) {
|
|
81
|
+
await onEvent({ type: 'agent:run:stopped', data: { ...target, reason: stop.reason } }, state);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
if (chunk.type === 'action:create_thread:result' && chunk.data.success) {
|
|
85
|
+
threadId = chunk.data.threadId || threadId;
|
|
86
|
+
}
|
|
87
|
+
await onEvent(chunk, state);
|
|
88
|
+
next = await turn.next();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function normalizeUserInput(event, ctx) {
|
|
92
|
+
const rawContent = event.data?.content || '';
|
|
93
|
+
const userFacing = {
|
|
94
|
+
type: 'agent:invoke',
|
|
95
|
+
id: event.id,
|
|
96
|
+
data: { content: rawContent, role: 'user' },
|
|
97
|
+
meta: {
|
|
98
|
+
agentId: ORCHESTRATOR_AGENT_ID,
|
|
99
|
+
userId: event.meta?.userId,
|
|
100
|
+
userName: event.meta?.userName,
|
|
101
|
+
userAvatarUrl: event.meta?.userAvatarUrl,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
const initialState = await storageService.getOpenBotState({
|
|
105
|
+
runId: ctx.runId,
|
|
106
|
+
agentId: ORCHESTRATOR_AGENT_ID,
|
|
107
|
+
channelId: ctx.channelId,
|
|
108
|
+
threadId: ctx.threadId,
|
|
109
|
+
event: userFacing,
|
|
110
|
+
});
|
|
111
|
+
await ctx.onEvent(userFacing, initialState);
|
|
112
|
+
return {
|
|
113
|
+
...event,
|
|
114
|
+
type: 'agent:invoke',
|
|
115
|
+
data: { ...(event.data || {}), content: rawContent, role: 'user' },
|
|
116
|
+
meta: {
|
|
117
|
+
...(event.meta || {}),
|
|
118
|
+
threadId: ctx.threadId || event.id,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async function handleStop(stopEvent, options) {
|
|
123
|
+
const { runId, channelId, threadId, onEvent } = options;
|
|
124
|
+
stopRequests.push({
|
|
125
|
+
runId: stopEvent.data.runId,
|
|
126
|
+
agentId: stopEvent.data.agentId,
|
|
127
|
+
channelId: stopEvent.data.channelId || channelId,
|
|
128
|
+
threadId: stopEvent.data.threadId || threadId,
|
|
129
|
+
reason: stopEvent.data.reason,
|
|
130
|
+
requestedAt: Date.now(),
|
|
131
|
+
});
|
|
132
|
+
const state = await storageService.getOpenBotState({
|
|
133
|
+
runId,
|
|
134
|
+
agentId: options.agentId || ORCHESTRATOR_AGENT_ID,
|
|
135
|
+
channelId,
|
|
136
|
+
threadId,
|
|
137
|
+
event: stopEvent,
|
|
138
|
+
});
|
|
139
|
+
await onEvent({
|
|
140
|
+
type: 'action:agent_run_stop:result',
|
|
141
|
+
data: { success: true, message: `Stop requested for run ${stopEvent.data.runId}.` },
|
|
142
|
+
meta: stopEvent.meta,
|
|
143
|
+
}, state);
|
|
144
|
+
}
|