openbot 0.3.3 → 0.3.4

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.
@@ -1,82 +0,0 @@
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
- };
@@ -1,104 +0,0 @@
1
- import { OpenBotEvent, OpenBotState } from '../app/types.js';
2
- import { storageService } from '../services/storage.js';
3
- import { createAgentRuntime } from './runtime-factory.js';
4
- import { EventNormalizer } from './event-normalizer.js';
5
- import { QueueProcessor } from './queue-processor.js';
6
-
7
- export interface ExecuteAgentOptions {
8
- runId: string;
9
- agentId: string;
10
- event: OpenBotEvent;
11
- channelId: string;
12
- threadId?: string;
13
- onEvent: (chunk: OpenBotEvent, state: OpenBotState) => Promise<boolean | void>;
14
- }
15
-
16
- export interface DispatchOptions {
17
- runId: string;
18
- agentId?: string;
19
- event: OpenBotEvent;
20
- channelId: string;
21
- threadId?: string;
22
- onEvent: (chunk: OpenBotEvent, state: OpenBotState) => Promise<boolean | void>;
23
- }
24
-
25
- export const orchestratorService = {
26
- /**
27
- * The primary entry point for all events coming into the system (e.g. from the API).
28
- * Handles routing and initial UI message creation.
29
- */
30
- dispatch: async (options: DispatchOptions): Promise<void> => {
31
- const { runId, channelId, threadId, onEvent } = options;
32
-
33
- // 1. Normalize incoming event
34
- const { finalEvent, finalAgentId } = await EventNormalizer.normalize(options.event, {
35
- runId,
36
- agentId: options.agentId,
37
- channelId,
38
- threadId,
39
- onEvent,
40
- });
41
-
42
- // 2. Initialize Queue Processor
43
- const processor = new QueueProcessor({
44
- runId,
45
- channelId,
46
- threadId,
47
- onEvent,
48
- executeAgent: orchestratorService.executeAgent,
49
- });
50
-
51
- // 3. Enqueue initial event
52
- processor.enqueue({ agentId: finalAgentId, event: finalEvent });
53
-
54
- // 4. Run execution loop
55
- await processor.run();
56
- },
57
-
58
- /**
59
- * Executes a single agent runtime.
60
- */
61
- executeAgent: async (options: ExecuteAgentOptions): Promise<void> => {
62
- const { runId, agentId, event, channelId, threadId, onEvent } = options;
63
-
64
- let agentState: OpenBotState;
65
- try {
66
- agentState = await storageService.getOpenBotState(options);
67
- } catch (error) {
68
- if ((error as Error & { code?: string }).code === 'AGENT_NOT_FOUND') {
69
- const fallbackState = await storageService.getOpenBotState({
70
- runId,
71
- agentId: 'system',
72
- channelId,
73
- threadId,
74
- event,
75
- });
76
- const warning = `⚠️ Agent **${agentId}** does not exist. Please check the agent ID and try again.`;
77
-
78
- await onEvent(
79
- {
80
- type: 'agent:output',
81
- data: { content: warning },
82
- meta: { agentId: 'system', threadId },
83
- },
84
- fallbackState,
85
- );
86
-
87
- return;
88
- }
89
- throw error;
90
- }
91
-
92
- const agentRuntime = await createAgentRuntime(agentState);
93
-
94
- try {
95
- // RUN the agent runtime
96
- for await (const chunk of agentRuntime.run(event, { state: agentState, runId })) {
97
- chunk.meta = { ...chunk.meta, agentId };
98
- await onEvent(chunk, agentState);
99
- }
100
- } catch (error) {
101
- console.error(`[orchestrator] Agent run failed: ${agentId}`, error);
102
- }
103
- },
104
- };
@@ -1,220 +0,0 @@
1
- import {
2
- AgentInvokeEvent,
3
- HandoffRequestEvent,
4
- OpenBotEvent,
5
- OpenBotState,
6
- } from '../app/types.js';
7
- import { ensureEventId } from '../app/utils.js';
8
- import { storageService } from '../services/storage.js';
9
- import { advanceAfterRun } from './todo-advance.js';
10
-
11
- export interface QueueItem {
12
- agentId: string;
13
- event: OpenBotEvent;
14
- }
15
-
16
- export interface QueueProcessorOptions {
17
- runId: string;
18
- channelId: string;
19
- threadId?: string;
20
- onEvent: (chunk: OpenBotEvent, state: OpenBotState) => Promise<boolean | void>;
21
- executeAgent: (options: {
22
- runId: string;
23
- agentId: string;
24
- event: OpenBotEvent;
25
- channelId: string;
26
- threadId?: string;
27
- onEvent: (chunk: OpenBotEvent, state: OpenBotState) => Promise<boolean | void>;
28
- }) => Promise<void>;
29
- }
30
-
31
- export class QueueProcessor {
32
- private currentQueue: QueueItem[] = [];
33
- private currentThreadId?: string;
34
- private readonly MAX_ITERATIONS = 20;
35
-
36
- constructor(private options: QueueProcessorOptions) {
37
- this.currentThreadId = options.threadId;
38
- }
39
-
40
- enqueue(item: QueueItem) {
41
- this.currentQueue.push(item);
42
- }
43
-
44
- async run() {
45
- let iterations = 0;
46
-
47
- while (this.currentQueue.length > 0 && iterations < this.MAX_ITERATIONS) {
48
- iterations++;
49
-
50
- // Group by agentId to avoid parallel state corruption for the same agent.
51
- const groups = new Map<string, QueueItem[]>();
52
- for (const item of this.currentQueue) {
53
- const list = groups.get(item.agentId) || [];
54
- list.push(item);
55
- groups.set(item.agentId, list);
56
- }
57
-
58
- const nextQueue: QueueItem[] = [];
59
-
60
- // Run each agent group in parallel
61
- await Promise.all(
62
- Array.from(groups.entries()).map(async ([agentId, items]) => {
63
- // Run items for the SAME agent sequentially to preserve event order and state consistency.
64
- for (const item of items) {
65
- const { event: currentEvent } = item;
66
-
67
- // Track handoff requests queued in this step to avoid accidental duplicates.
68
- const queuedRequestKeys = new Set<string>();
69
- const queuedItems: QueueItem[] = [];
70
- let lastAgentOutput: string | undefined;
71
-
72
- const runOnEvent = async (chunk: OpenBotEvent, state: OpenBotState) => {
73
- // 0. Filter out echoed input events to prevent duplication in the UI/storage
74
- if (chunk.type === currentEvent.type && chunk.id === currentEvent.id) {
75
- return false;
76
- }
77
-
78
- if (chunk.type === 'agent:output') {
79
- const outMeta = chunk.meta as { agentId?: string } | undefined;
80
- if (outMeta?.agentId === agentId) {
81
- const content = chunk.data?.content;
82
- if (typeof content === 'string' && content.trim()) {
83
- lastAgentOutput = content.trim();
84
- }
85
- }
86
- }
87
-
88
- // 1. Detect if a new thread was created and update the context for the rest of the loop
89
- if (chunk.type === 'action:create_thread:result' && chunk.data.success) {
90
- this.currentThreadId = chunk.data.threadId || this.currentThreadId;
91
- }
92
-
93
- // 2. Internal routing (handoff requests are internal — not forwarded)
94
- if (chunk.type === 'handoff:request') {
95
- const request = chunk as HandoffRequestEvent;
96
- const targetAgentId = request.data?.agentId;
97
- const requestKey = `handoff:${targetAgentId}`;
98
-
99
- if (
100
- targetAgentId &&
101
- targetAgentId !== agentId &&
102
- !queuedRequestKeys.has(requestKey)
103
- ) {
104
- queuedRequestKeys.add(requestKey);
105
- const targetEvent = ensureEventId({
106
- type: 'agent:invoke',
107
- data: {
108
- role: 'user',
109
- content: request.data.content,
110
- },
111
- meta: {
112
- ...(request.meta || {}),
113
- threadId: this.currentThreadId,
114
- },
115
- } satisfies AgentInvokeEvent) as AgentInvokeEvent;
116
-
117
- queuedItems.push({ agentId: targetAgentId, event: targetEvent });
118
- }
119
- return false;
120
- }
121
-
122
- // If we get here, the event is accepted and should be emitted.
123
- await this.options.onEvent(chunk, state);
124
- return true;
125
- };
126
-
127
- const startState = await storageService.getOpenBotState({
128
- runId: this.options.runId,
129
- agentId,
130
- channelId: this.options.channelId,
131
- threadId: this.currentThreadId,
132
- event: currentEvent,
133
- });
134
-
135
- await this.options.onEvent(
136
- {
137
- type: 'agent:run:start',
138
- data: {
139
- runId: this.options.runId,
140
- agentId,
141
- channelId: this.options.channelId,
142
- threadId: this.currentThreadId,
143
- },
144
- },
145
- startState,
146
- );
147
-
148
- try {
149
- await this.options.executeAgent({
150
- runId: this.options.runId,
151
- agentId,
152
- event: currentEvent,
153
- channelId: this.options.channelId,
154
- threadId: this.currentThreadId,
155
- onEvent: runOnEvent,
156
- });
157
- } finally {
158
- const endState = await storageService.getOpenBotState({
159
- runId: this.options.runId,
160
- agentId,
161
- channelId: this.options.channelId,
162
- threadId: this.currentThreadId,
163
- event: currentEvent,
164
- });
165
- await this.options.onEvent(
166
- {
167
- type: 'agent:run:end',
168
- data: {
169
- runId: this.options.runId,
170
- agentId,
171
- channelId: this.options.channelId,
172
- threadId: this.currentThreadId,
173
- },
174
- },
175
- endState,
176
- );
177
-
178
- // Autonomous todo advance: mark this agent's in_progress todo done
179
- // and dispatch the next assignee, if any. Single trigger point,
180
- // no reliance on the LLM remembering to call `todo_update`.
181
- try {
182
- const handoff = await advanceAfterRun({
183
- storage: storageService,
184
- channelId: this.options.channelId,
185
- threadId: this.currentThreadId,
186
- endedAgentId: agentId,
187
- lastAgentOutput,
188
- });
189
- if (handoff) {
190
- const requestKey = `handoff:${handoff.agentId}`;
191
- if (!queuedRequestKeys.has(requestKey)) {
192
- queuedRequestKeys.add(requestKey);
193
- const targetEvent = ensureEventId({
194
- type: 'agent:invoke',
195
- data: { role: 'user', content: handoff.content },
196
- meta: { threadId: this.currentThreadId },
197
- } satisfies AgentInvokeEvent) as AgentInvokeEvent;
198
- queuedItems.push({ agentId: handoff.agentId, event: targetEvent });
199
- }
200
- }
201
- } catch (error) {
202
- console.warn('[queue] todo advance failed', error);
203
- }
204
- }
205
-
206
- nextQueue.push(...queuedItems);
207
- }
208
- }),
209
- );
210
-
211
- this.currentQueue = nextQueue;
212
- }
213
-
214
- if (iterations >= this.MAX_ITERATIONS) {
215
- console.warn(
216
- `[orchestrator] Reached MAX_ITERATIONS (${this.MAX_ITERATIONS}). Stopping execution.`,
217
- );
218
- }
219
- }
220
- }
@@ -1,34 +0,0 @@
1
- import { OpenBotEvent, OpenBotState } from '../app/types.js';
2
-
3
- /**
4
- * The Harness is the environment in which an agent operates.
5
- * it provides the necessary "plumbing" (storage, communication, tools)
6
- * so the agent can focus on reasoning.
7
- */
8
- export interface Harness {
9
- readonly runId: string;
10
- readonly channelId: string;
11
- readonly threadId?: string;
12
-
13
- /**
14
- * Dispatches an event into the harness.
15
- * This is the primary way to interact with the agent.
16
- */
17
- dispatch(event: OpenBotEvent): Promise<void>;
18
-
19
- /**
20
- * Observes events emitted by the harness.
21
- */
22
- onEvent(callback: (event: OpenBotEvent, state: OpenBotState) => Promise<void>): void;
23
- }
24
-
25
- /**
26
- * Options for creating a new Agent Harness instance.
27
- */
28
- export interface HarnessOptions {
29
- runId: string;
30
- agentId: string;
31
- channelId: string;
32
- threadId?: string;
33
- onEvent: (event: OpenBotEvent, state: OpenBotState) => Promise<void>;
34
- }