openbot 0.2.14 → 0.3.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/agents/openbot/index.js +76 -0
- package/dist/agents/openbot/middleware/approval.js +132 -0
- package/dist/agents/openbot/runtime.js +289 -0
- package/dist/agents/openbot/system-prompt.js +32 -0
- package/dist/agents/openbot/tools/delegation.js +78 -0
- package/dist/agents/openbot/tools/mcp.js +99 -0
- package/dist/agents/openbot/tools/shell.js +91 -0
- package/dist/agents/openbot/tools/storage.js +75 -0
- package/dist/agents/openbot/tools/ui.js +176 -0
- package/dist/agents/system.js +20 -93
- package/dist/app/cli.js +1 -1
- package/dist/app/config.js +4 -1
- package/dist/app/server.js +15 -8
- package/dist/bus/agent-package.js +1 -0
- package/dist/bus/plugin.js +1 -0
- package/dist/bus/services.js +711 -0
- package/dist/bus/types.js +1 -0
- package/dist/harness/context.js +250 -0
- package/dist/harness/event-normalizer.js +59 -0
- package/dist/harness/orchestrator.js +27 -227
- package/dist/harness/process.js +25 -3
- package/dist/harness/queue-processor.js +227 -0
- package/dist/harness/runtime-factory.js +103 -0
- package/dist/plugins/ai-sdk/index.js +37 -0
- package/dist/plugins/ai-sdk/runtime.js +402 -0
- package/dist/plugins/ai-sdk/system-prompt.js +3 -0
- package/dist/plugins/ai-sdk.js +277 -87
- package/dist/plugins/approval/index.js +159 -0
- package/dist/plugins/approval.js +163 -0
- package/dist/plugins/delegation/index.js +79 -0
- package/dist/plugins/delegation.js +67 -11
- package/dist/plugins/mcp/index.js +108 -0
- package/dist/plugins/memory/index.js +71 -0
- package/dist/plugins/shell/index.js +99 -0
- package/dist/plugins/shell.js +123 -0
- package/dist/plugins/storage-tools/index.js +85 -0
- package/dist/plugins/storage.js +240 -5
- package/dist/plugins/ui/index.js +184 -0
- package/dist/plugins/ui.js +185 -21
- package/dist/registry/agents.js +138 -0
- package/dist/registry/plugins.js +93 -50
- package/dist/services/agent-packages.js +103 -0
- package/dist/services/memory.js +152 -0
- package/dist/services/plugins.js +98 -0
- package/dist/services/storage.js +366 -94
- package/docs/agents.md +52 -65
- package/docs/architecture.md +1 -1
- package/docs/plugins.md +70 -58
- package/docs/templates/AGENT.example.md +57 -0
- package/package.json +8 -7
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +14 -4
- package/src/app/server.ts +23 -10
- package/src/app/types.ts +445 -16
- package/src/assets/icon.svg +4 -1
- package/src/bus/plugin.ts +67 -0
- package/src/bus/services.ts +786 -0
- package/src/bus/types.ts +160 -0
- package/src/harness/context.ts +293 -0
- package/src/harness/event-normalizer.ts +82 -0
- package/src/harness/orchestrator.ts +35 -273
- package/src/harness/process.ts +28 -4
- package/src/harness/queue-processor.ts +309 -0
- package/src/harness/runtime-factory.ts +125 -0
- package/src/plugins/ai-sdk/index.ts +44 -0
- package/src/plugins/ai-sdk/runtime.ts +484 -0
- package/src/plugins/ai-sdk/system-prompt.ts +4 -0
- package/src/plugins/approval/index.ts +228 -0
- package/src/plugins/delegation/index.ts +94 -0
- package/src/plugins/mcp/index.ts +128 -0
- package/src/plugins/memory/index.ts +85 -0
- package/src/plugins/shell/index.ts +123 -0
- package/src/plugins/storage-tools/index.ts +101 -0
- package/src/plugins/ui/index.ts +227 -0
- package/src/registry/plugins.ts +108 -55
- package/src/services/memory.ts +213 -0
- package/src/services/plugins.ts +133 -0
- package/src/services/storage.ts +472 -137
- package/src/agents/system.ts +0 -112
- package/src/plugins/ai-sdk.ts +0 -197
- package/src/plugins/delegation.ts +0 -60
- package/src/plugins/mcp.ts +0 -154
- package/src/plugins/storage.ts +0 -725
- package/src/plugins/ui.ts +0 -57
package/src/bus/types.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { OpenBotEvent } from '../app/types.js';
|
|
2
|
+
import type { PluginRef } from './plugin.js';
|
|
3
|
+
import type { MemoryRecord, ListMemoriesArgs } from '../services/memory.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Public data types exposed by the OpenBot bus.
|
|
7
|
+
*
|
|
8
|
+
* The bus is the platform layer that owns channels, threads, the agent registry,
|
|
9
|
+
* and the event stream. Agents are composed entirely of Plugins (see
|
|
10
|
+
* `bus/plugin.ts`); their internal implementation is opaque to the bus.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type Agent = {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
image?: string;
|
|
18
|
+
/** Plugin ids that compose this agent (mirrors AGENT.md `plugins[].id`). */
|
|
19
|
+
plugins: string[];
|
|
20
|
+
createdAt: Date;
|
|
21
|
+
updatedAt: Date;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type AgentDetails = Agent & {
|
|
25
|
+
instructions: string;
|
|
26
|
+
/** Full plugin refs from AGENT.md (with per-plugin config). */
|
|
27
|
+
pluginRefs: PluginRef[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type PluginDescriptor = {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
description: string;
|
|
34
|
+
/** True when bundled with the core server (`src/registry/plugins`); false for ~/.openbot/plugins installs. */
|
|
35
|
+
builtIn: boolean;
|
|
36
|
+
image?: string;
|
|
37
|
+
defaultInstructions?: string;
|
|
38
|
+
configSchema?: ConfigSchema;
|
|
39
|
+
createdAt: Date;
|
|
40
|
+
updatedAt: Date;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type ConfigSchema = {
|
|
44
|
+
type: 'object';
|
|
45
|
+
properties: {
|
|
46
|
+
[key: string]: {
|
|
47
|
+
type: 'string' | 'number' | 'boolean' | 'integer';
|
|
48
|
+
description?: string;
|
|
49
|
+
default?: unknown;
|
|
50
|
+
enum?: unknown[];
|
|
51
|
+
minimum?: number;
|
|
52
|
+
maximum?: number;
|
|
53
|
+
format?: 'password' | 'url' | 'email';
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
required?: string[];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type Channel = {
|
|
60
|
+
id: string;
|
|
61
|
+
name: string;
|
|
62
|
+
description: string;
|
|
63
|
+
cwd?: string;
|
|
64
|
+
createdAt: Date;
|
|
65
|
+
updatedAt: Date;
|
|
66
|
+
hasUnseenMessages?: boolean;
|
|
67
|
+
recentThreads?: Thread[];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type Thread = {
|
|
71
|
+
id: string;
|
|
72
|
+
name: string;
|
|
73
|
+
channelId: string;
|
|
74
|
+
createdAt: Date;
|
|
75
|
+
updatedAt: Date;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type ThreadDetails = {
|
|
79
|
+
id: string;
|
|
80
|
+
name: string;
|
|
81
|
+
channelId: string;
|
|
82
|
+
spec: string;
|
|
83
|
+
state: unknown;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type ChannelDetails = {
|
|
87
|
+
id: string;
|
|
88
|
+
name: string;
|
|
89
|
+
spec: string;
|
|
90
|
+
state: unknown;
|
|
91
|
+
cwd?: string;
|
|
92
|
+
threads?: Thread[];
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export interface Storage {
|
|
96
|
+
getChannels: () => Promise<Channel[]>;
|
|
97
|
+
createChannel: (args: {
|
|
98
|
+
channelId: string;
|
|
99
|
+
spec?: string;
|
|
100
|
+
initialState?: Record<string, unknown>;
|
|
101
|
+
cwd?: string;
|
|
102
|
+
}) => Promise<void>;
|
|
103
|
+
createThread: (args: {
|
|
104
|
+
channelId: string;
|
|
105
|
+
threadId: string;
|
|
106
|
+
threadTitle?: string;
|
|
107
|
+
spec?: string;
|
|
108
|
+
initialState?: Record<string, unknown>;
|
|
109
|
+
}) => Promise<void>;
|
|
110
|
+
getThreads: (args: { channelId: string }) => Promise<Thread[]>;
|
|
111
|
+
getThreadDetails: (args: { channelId: string; threadId: string }) => Promise<ThreadDetails>;
|
|
112
|
+
getAgents: () => Promise<Agent[]>;
|
|
113
|
+
getPlugins: () => Promise<PluginDescriptor[]>;
|
|
114
|
+
getAgentDetails: (args: { agentId: string }) => Promise<AgentDetails>;
|
|
115
|
+
createAgent: (args: {
|
|
116
|
+
agentId: string;
|
|
117
|
+
name: string;
|
|
118
|
+
description?: string;
|
|
119
|
+
instructions: string;
|
|
120
|
+
plugins: PluginRef[];
|
|
121
|
+
}) => Promise<void>;
|
|
122
|
+
updateAgent: (args: {
|
|
123
|
+
agentId: string;
|
|
124
|
+
name?: string;
|
|
125
|
+
description?: string;
|
|
126
|
+
instructions?: string;
|
|
127
|
+
plugins?: PluginRef[];
|
|
128
|
+
}) => Promise<void>;
|
|
129
|
+
deleteAgent: (args: { agentId: string }) => Promise<void>;
|
|
130
|
+
getEvents: (args: { channelId: string; threadId?: string }) => Promise<OpenBotEvent[]>;
|
|
131
|
+
getChannelDetails: (args: { channelId: string }) => Promise<ChannelDetails>;
|
|
132
|
+
patchChannelState: (args: { channelId: string; state: unknown }) => Promise<void>;
|
|
133
|
+
patchThreadState: (args: {
|
|
134
|
+
channelId: string;
|
|
135
|
+
threadId: string;
|
|
136
|
+
state: unknown;
|
|
137
|
+
}) => Promise<void>;
|
|
138
|
+
patchChannelSpec: (args: { channelId: string; spec: string }) => Promise<void>;
|
|
139
|
+
patchThreadSpec: (args: { channelId: string; threadId: string; spec: string }) => Promise<void>;
|
|
140
|
+
getVariables: () => Promise<Record<string, string | { value: string; secret: boolean }>>;
|
|
141
|
+
createVariable: (args: { key: string; value: string; secret?: boolean }) => Promise<void>;
|
|
142
|
+
deleteVariable: (args: { key: string }) => Promise<void>;
|
|
143
|
+
listFiles: (args: {
|
|
144
|
+
channelId: string;
|
|
145
|
+
path?: string;
|
|
146
|
+
}) => Promise<Array<{ name: string; isDirectory: boolean }>>;
|
|
147
|
+
readFile: (args: { channelId: string; path: string }) => Promise<string>;
|
|
148
|
+
/** Persist a memory record into the global memory log. */
|
|
149
|
+
appendMemory: (args: {
|
|
150
|
+
scope: string;
|
|
151
|
+
content: string;
|
|
152
|
+
tags?: string[];
|
|
153
|
+
}) => Promise<MemoryRecord>;
|
|
154
|
+
/** Read memories matching the given filter. */
|
|
155
|
+
listMemories: (args?: ListMemoriesArgs) => Promise<MemoryRecord[]>;
|
|
156
|
+
/** Soft-delete a memory by id. Returns true if a record was deleted. */
|
|
157
|
+
deleteMemory: (args: { id: string }) => Promise<boolean>;
|
|
158
|
+
/** Update a memory's content/tags by id. Returns true if a record was updated. */
|
|
159
|
+
updateMemory: (args: { id: string; content?: string; tags?: string[] }) => Promise<boolean>;
|
|
160
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { OpenBotEvent, OpenBotState } from '../app/types.js';
|
|
2
|
+
import { Storage } from '../bus/types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Represents a piece of context that can be used in a prompt.
|
|
6
|
+
*
|
|
7
|
+
* Items flow through the engine in two phases:
|
|
8
|
+
* 1. Each registered `ContextProvider` emits zero or more items.
|
|
9
|
+
* 2. Each registered `ContextProcessor` may transform / drop / re-rank
|
|
10
|
+
* items (e.g. token-budget enforcement).
|
|
11
|
+
*
|
|
12
|
+
* Higher `priority` items appear first in the assembled prompt and are the
|
|
13
|
+
* last to be dropped under budget pressure.
|
|
14
|
+
*/
|
|
15
|
+
export interface ContextItem {
|
|
16
|
+
id: string;
|
|
17
|
+
type: string;
|
|
18
|
+
priority: number;
|
|
19
|
+
content: string;
|
|
20
|
+
metadata?: Record<string, any>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ContextProvider {
|
|
24
|
+
name: string;
|
|
25
|
+
provide(state: OpenBotState, storage?: Storage): Promise<ContextItem[]>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ContextProcessor {
|
|
29
|
+
name: string;
|
|
30
|
+
process(items: ContextItem[], state: OpenBotState): Promise<ContextItem[]>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Cheap, dependency-free token estimator. Roughly char/4 — fine for budget
|
|
35
|
+
* enforcement; can be swapped for a tokenizer-backed implementation later
|
|
36
|
+
* without touching providers.
|
|
37
|
+
*/
|
|
38
|
+
export const estimateTokens = (text: string): number =>
|
|
39
|
+
Math.ceil((text?.length ?? 0) / 4);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Hard cap (in characters) on a single context item. Keeps any one provider
|
|
43
|
+
* — typically the recent-events feed — from monopolising the prompt budget.
|
|
44
|
+
*/
|
|
45
|
+
const ITEM_HARD_CHAR_CAP = 6000;
|
|
46
|
+
|
|
47
|
+
const truncate = (text: string, maxChars: number): string =>
|
|
48
|
+
text.length <= maxChars ? text : `${text.slice(0, maxChars)}\n…[truncated]`;
|
|
49
|
+
|
|
50
|
+
export class ContextEngine {
|
|
51
|
+
private providers: ContextProvider[] = [];
|
|
52
|
+
private processors: ContextProcessor[] = [];
|
|
53
|
+
|
|
54
|
+
registerProvider(provider: ContextProvider) {
|
|
55
|
+
this.providers.push(provider);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
registerProcessor(processor: ContextProcessor) {
|
|
59
|
+
this.processors.push(processor);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async buildContext(state: OpenBotState, storage?: Storage): Promise<string> {
|
|
63
|
+
let items: ContextItem[] = [];
|
|
64
|
+
for (const provider of this.providers) {
|
|
65
|
+
try {
|
|
66
|
+
const providedItems = await provider.provide(state, storage);
|
|
67
|
+
for (const item of providedItems) {
|
|
68
|
+
items.push({ ...item, content: truncate(item.content, ITEM_HARD_CHAR_CAP) });
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.warn(`[ContextEngine] Provider ${provider.name} failed:`, error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const processor of this.processors) {
|
|
76
|
+
try {
|
|
77
|
+
items = await processor.process(items, state);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.warn(`[ContextEngine] Processor ${processor.name} failed:`, error);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return items
|
|
84
|
+
.sort((a, b) => b.priority - a.priority)
|
|
85
|
+
.map((item) => item.content)
|
|
86
|
+
.join('\n\n');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Default context engine. Order of providers is by emit order; final ordering
|
|
92
|
+
* in the prompt is determined by `priority`. The token-budget processor runs
|
|
93
|
+
* last so dropping happens after every provider has contributed.
|
|
94
|
+
*/
|
|
95
|
+
export function createDefaultContextEngine(): ContextEngine {
|
|
96
|
+
const engine = new ContextEngine();
|
|
97
|
+
|
|
98
|
+
engine.registerProvider(new AgentDetailsProvider());
|
|
99
|
+
engine.registerProvider(new ChannelDetailsProvider());
|
|
100
|
+
engine.registerProvider(new ThreadDetailsProvider());
|
|
101
|
+
engine.registerProvider(new MemoryProvider());
|
|
102
|
+
engine.registerProvider(new RecentEventsProvider());
|
|
103
|
+
|
|
104
|
+
engine.registerProcessor(new TokenBudgetProcessor());
|
|
105
|
+
|
|
106
|
+
return engine;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
class AgentDetailsProvider implements ContextProvider {
|
|
110
|
+
name = 'agent-details';
|
|
111
|
+
async provide(state: OpenBotState): Promise<ContextItem[]> {
|
|
112
|
+
if (!state.agentDetails) return [];
|
|
113
|
+
return [{
|
|
114
|
+
id: 'agent-details',
|
|
115
|
+
type: 'agent',
|
|
116
|
+
priority: 100,
|
|
117
|
+
content: `## AGENT NAME\n${state.agentDetails.name}\n\n## AGENT SPECIFICATION\n${state.agentDetails.instructions}`
|
|
118
|
+
}];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
class ChannelDetailsProvider implements ContextProvider {
|
|
123
|
+
name = 'channel-details';
|
|
124
|
+
async provide(state: OpenBotState): Promise<ContextItem[]> {
|
|
125
|
+
if (!state.channelDetails) return [];
|
|
126
|
+
return [{
|
|
127
|
+
id: 'channel-details',
|
|
128
|
+
type: 'channel',
|
|
129
|
+
priority: 80,
|
|
130
|
+
content: `## CHANNEL NAME\n${state.channelDetails.name}\n\n## CHANNEL SPECIFICATION\n${state.channelDetails.spec}`
|
|
131
|
+
}];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
class ThreadDetailsProvider implements ContextProvider {
|
|
136
|
+
name = 'thread-details';
|
|
137
|
+
async provide(state: OpenBotState): Promise<ContextItem[]> {
|
|
138
|
+
if (!state.threadDetails) return [];
|
|
139
|
+
return [{
|
|
140
|
+
id: 'thread-details',
|
|
141
|
+
type: 'thread',
|
|
142
|
+
priority: 90,
|
|
143
|
+
content: `## THREAD NAME\n${state.threadDetails.name}\n\n## THREAD SPECIFICATION\n${state.threadDetails.spec}`
|
|
144
|
+
}];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Fetches relevant memories (global + active agent + active channel) and
|
|
150
|
+
* surfaces them at high priority so the LLM treats them as ground truth
|
|
151
|
+
* rather than chat history.
|
|
152
|
+
*/
|
|
153
|
+
class MemoryProvider implements ContextProvider {
|
|
154
|
+
name = 'memory';
|
|
155
|
+
async provide(state: OpenBotState, storage?: Storage): Promise<ContextItem[]> {
|
|
156
|
+
if (!storage?.listMemories) return [];
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const scopes = ['global', `agent:${state.agentId}`];
|
|
160
|
+
if (state.channelId) scopes.push(`channel:${state.channelId}`);
|
|
161
|
+
|
|
162
|
+
const records = await storage.listMemories({ scopes, limit: 50 });
|
|
163
|
+
if (records.length === 0) return [];
|
|
164
|
+
|
|
165
|
+
const formatted = records
|
|
166
|
+
.map((r) => {
|
|
167
|
+
const tags = r.tags?.length ? ` [${r.tags.join(', ')}]` : '';
|
|
168
|
+
const scopeLabel = r.scope === 'global' ? 'global' : r.scope;
|
|
169
|
+
return `- (${scopeLabel}${tags}) ${r.content}`;
|
|
170
|
+
})
|
|
171
|
+
.join('\n');
|
|
172
|
+
|
|
173
|
+
return [
|
|
174
|
+
{
|
|
175
|
+
id: 'memory',
|
|
176
|
+
type: 'memory',
|
|
177
|
+
priority: 95,
|
|
178
|
+
content: `## REMEMBERED FACTS\nThese are durable facts you previously stored with the \`remember\` tool. Trust them unless contradicted by the user. Use \`forget\` to remove ones that are stale.\n\n${formatted}`,
|
|
179
|
+
},
|
|
180
|
+
];
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.warn('[ContextEngine] MemoryProvider failed:', error);
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Event types we omit from the recent-events context block. They duplicate
|
|
190
|
+
* information already in the conversation history, are infrastructural
|
|
191
|
+
* noise, or are too large to be useful as a tail summary.
|
|
192
|
+
*/
|
|
193
|
+
const NOISY_EVENT_PREFIXES = [
|
|
194
|
+
'agent:invoke',
|
|
195
|
+
'agent:output',
|
|
196
|
+
'agent:run',
|
|
197
|
+
'agent:active-runs',
|
|
198
|
+
'client:ui',
|
|
199
|
+
'stream:',
|
|
200
|
+
'action:storage:get-',
|
|
201
|
+
'action:storage:patch-',
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
const MAX_RECENT_EVENTS = 20;
|
|
205
|
+
const MAX_EVENT_DATA_CHARS = 300;
|
|
206
|
+
|
|
207
|
+
const isNoisyEvent = (event: OpenBotEvent): boolean =>
|
|
208
|
+
NOISY_EVENT_PREFIXES.some((prefix) => event.type.startsWith(prefix));
|
|
209
|
+
|
|
210
|
+
const summarizeEvent = (event: OpenBotEvent): string => {
|
|
211
|
+
const data = (event as { data?: unknown }).data;
|
|
212
|
+
if (data === undefined) return `- ${event.type}`;
|
|
213
|
+
let payload: string;
|
|
214
|
+
try {
|
|
215
|
+
payload = typeof data === 'string' ? data : JSON.stringify(data);
|
|
216
|
+
} catch {
|
|
217
|
+
payload = '[unserialisable]';
|
|
218
|
+
}
|
|
219
|
+
if (payload.length > MAX_EVENT_DATA_CHARS) {
|
|
220
|
+
payload = `${payload.slice(0, MAX_EVENT_DATA_CHARS)}…`;
|
|
221
|
+
}
|
|
222
|
+
return `- ${event.type}: ${payload}`;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
class RecentEventsProvider implements ContextProvider {
|
|
226
|
+
name = 'recent-events';
|
|
227
|
+
async provide(state: OpenBotState, storage?: Storage): Promise<ContextItem[]> {
|
|
228
|
+
if (!storage) return [];
|
|
229
|
+
|
|
230
|
+
const channelId = state.channelId;
|
|
231
|
+
const threadId = state.threadId;
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const events = await storage.getEvents({ channelId, threadId });
|
|
235
|
+
const filtered = events.filter((e) => !isNoisyEvent(e));
|
|
236
|
+
if (filtered.length === 0) return [];
|
|
237
|
+
|
|
238
|
+
const formatted = filtered.slice(-MAX_RECENT_EVENTS).map(summarizeEvent).join('\n');
|
|
239
|
+
|
|
240
|
+
return [
|
|
241
|
+
{
|
|
242
|
+
id: threadId ? 'thread-events' : 'channel-events',
|
|
243
|
+
type: 'events',
|
|
244
|
+
priority: 70,
|
|
245
|
+
content: `## ${threadId ? 'THREAD' : 'CHANNEL'} RECENT ACTIVITIES (events)\n${formatted}`,
|
|
246
|
+
},
|
|
247
|
+
];
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.warn('[ContextEngine] Failed to fetch events:', error);
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Drops the lowest-priority items until the assembled prompt fits within the
|
|
257
|
+
* token budget. The first item with priority >= `keepFloor` is always kept,
|
|
258
|
+
* so the agent's own instructions can never be evicted. Stable on ties:
|
|
259
|
+
* later-emitted items go first.
|
|
260
|
+
*/
|
|
261
|
+
export class TokenBudgetProcessor implements ContextProcessor {
|
|
262
|
+
name = 'token-budget';
|
|
263
|
+
/** Soft prompt budget in tokens (matches gpt-4o-mini's reasonable system slice). */
|
|
264
|
+
static DEFAULT_BUDGET = 8000;
|
|
265
|
+
/** Items at or above this priority are never dropped. */
|
|
266
|
+
static KEEP_FLOOR = 100;
|
|
267
|
+
|
|
268
|
+
constructor(
|
|
269
|
+
private budget: number = TokenBudgetProcessor.DEFAULT_BUDGET,
|
|
270
|
+
private keepFloor: number = TokenBudgetProcessor.KEEP_FLOOR,
|
|
271
|
+
) {}
|
|
272
|
+
|
|
273
|
+
async process(items: ContextItem[]): Promise<ContextItem[]> {
|
|
274
|
+
const sorted = [...items].sort((a, b) => b.priority - a.priority);
|
|
275
|
+
const out: ContextItem[] = [];
|
|
276
|
+
let used = 0;
|
|
277
|
+
|
|
278
|
+
for (const item of sorted) {
|
|
279
|
+
const cost = estimateTokens(item.content);
|
|
280
|
+
if (item.priority >= this.keepFloor) {
|
|
281
|
+
out.push(item);
|
|
282
|
+
used += cost;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (used + cost <= this.budget) {
|
|
286
|
+
out.push(item);
|
|
287
|
+
used += cost;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { AgentInvokeEvent, OpenBotEvent, OpenBotState } from '../app/types.js';
|
|
2
|
+
import { ensureEventId } from '../app/utils.js';
|
|
3
|
+
import { storageService } from '../services/storage.js';
|
|
4
|
+
|
|
5
|
+
export interface NormalizedEventResult {
|
|
6
|
+
finalEvent: OpenBotEvent;
|
|
7
|
+
finalAgentId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const EventNormalizer = {
|
|
11
|
+
/**
|
|
12
|
+
* Normalizes incoming events, converting raw inputs like user:input to agent:invoke.
|
|
13
|
+
* Also handles initial state storage and event bus propagation for user inputs.
|
|
14
|
+
*/
|
|
15
|
+
normalize: async (
|
|
16
|
+
event: OpenBotEvent,
|
|
17
|
+
options: {
|
|
18
|
+
runId: string;
|
|
19
|
+
agentId?: string;
|
|
20
|
+
channelId: string;
|
|
21
|
+
threadId?: string;
|
|
22
|
+
onEvent: (chunk: OpenBotEvent, state: OpenBotState) => Promise<boolean | void>;
|
|
23
|
+
}
|
|
24
|
+
): Promise<NormalizedEventResult> => {
|
|
25
|
+
const { runId, agentId, channelId, threadId, onEvent } = options;
|
|
26
|
+
|
|
27
|
+
// 0. Ensure the incoming event has a unique ID immediately
|
|
28
|
+
ensureEventId(event);
|
|
29
|
+
|
|
30
|
+
let finalAgentId = agentId || 'system';
|
|
31
|
+
let finalEvent = event;
|
|
32
|
+
|
|
33
|
+
// 1. Convert user:input (or other raw inputs) to agent:invoke
|
|
34
|
+
const rawContent = (event as any).data?.content || '';
|
|
35
|
+
if (event.type === 'user:input' || event.type === 'agent:invoke') {
|
|
36
|
+
const normalizedInvokeEvent: AgentInvokeEvent = {
|
|
37
|
+
type: 'agent:invoke',
|
|
38
|
+
id: event.id,
|
|
39
|
+
data: {
|
|
40
|
+
content: rawContent,
|
|
41
|
+
role: 'user',
|
|
42
|
+
},
|
|
43
|
+
meta: {
|
|
44
|
+
agentId: 'system',
|
|
45
|
+
userId: event.meta?.userId,
|
|
46
|
+
userName: event.meta?.userName,
|
|
47
|
+
userAvatarUrl: event.meta?.userAvatarUrl,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
finalEvent = normalizedInvokeEvent;
|
|
51
|
+
|
|
52
|
+
// 1. Store the user's input in the current context (main channel or existing thread)
|
|
53
|
+
const initialState = await storageService.getOpenBotState({
|
|
54
|
+
runId,
|
|
55
|
+
agentId: 'system',
|
|
56
|
+
channelId,
|
|
57
|
+
threadId: threadId,
|
|
58
|
+
event: finalEvent,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// 2. Propagate the user's input to the event bus
|
|
62
|
+
await onEvent(finalEvent, initialState);
|
|
63
|
+
|
|
64
|
+
// 3. Prepare the event for the target agent
|
|
65
|
+
finalEvent = {
|
|
66
|
+
...event,
|
|
67
|
+
type: 'agent:invoke',
|
|
68
|
+
data: {
|
|
69
|
+
...((event as any).data || {}),
|
|
70
|
+
content: rawContent,
|
|
71
|
+
},
|
|
72
|
+
meta: {
|
|
73
|
+
...(event.meta || {}),
|
|
74
|
+
// The threadId in meta is the anchor for new threads (Slack-style)
|
|
75
|
+
threadId: threadId || finalEvent.id,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { finalEvent, finalAgentId };
|
|
81
|
+
},
|
|
82
|
+
};
|