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.
Files changed (98) hide show
  1. package/README.md +15 -16
  2. package/dist/app/agent-ids.js +4 -0
  3. package/dist/app/cli.js +1 -1
  4. package/dist/app/config.js +0 -19
  5. package/dist/app/server.js +8 -14
  6. package/dist/assets/icon.svg +9 -3
  7. package/dist/bus/services.js +78 -132
  8. package/dist/harness/agent-invoke-run.js +44 -0
  9. package/dist/harness/agent-turn.js +99 -0
  10. package/dist/harness/channel-participants.js +40 -0
  11. package/dist/harness/constants.js +2 -0
  12. package/dist/harness/context-meter.js +97 -0
  13. package/dist/harness/context.js +98 -45
  14. package/dist/harness/dispatch.js +144 -0
  15. package/dist/harness/dispatcher.js +45 -156
  16. package/dist/harness/history.js +177 -0
  17. package/dist/harness/index.js +91 -0
  18. package/dist/harness/orchestration.js +88 -0
  19. package/dist/harness/participants.js +22 -0
  20. package/dist/harness/run-harness.js +154 -0
  21. package/dist/harness/run.js +98 -0
  22. package/dist/harness/runtime-factory.js +0 -34
  23. package/dist/harness/runtime.js +57 -0
  24. package/dist/harness/todo-dispatch.js +51 -0
  25. package/dist/harness/todos.js +5 -0
  26. package/dist/harness/turn.js +79 -0
  27. package/dist/plugins/approval/index.js +105 -149
  28. package/dist/plugins/delegation/index.js +119 -32
  29. package/dist/plugins/memory/index.js +103 -14
  30. package/dist/plugins/memory/service.js +152 -0
  31. package/dist/plugins/openbot/context.js +80 -0
  32. package/dist/plugins/openbot/history.js +98 -0
  33. package/dist/plugins/openbot/index.js +31 -0
  34. package/dist/plugins/openbot/runtime.js +317 -0
  35. package/dist/plugins/openbot/system-prompt.js +5 -0
  36. package/dist/plugins/plugin-manager/index.js +105 -0
  37. package/dist/plugins/storage/index.js +573 -0
  38. package/dist/plugins/storage/service.js +1159 -0
  39. package/dist/plugins/storage-tools/index.js +2 -2
  40. package/dist/plugins/thread-namer/index.js +72 -0
  41. package/dist/plugins/thread-naming/generate-title.js +44 -0
  42. package/dist/plugins/thread-naming/index.js +103 -0
  43. package/dist/plugins/threads/index.js +114 -0
  44. package/dist/plugins/todo/index.js +24 -25
  45. package/dist/plugins/ui/index.js +2 -32
  46. package/dist/registry/plugins.js +3 -9
  47. package/dist/services/plugins/domain.js +1 -0
  48. package/dist/services/plugins/plugin-cache.js +9 -0
  49. package/dist/services/plugins/registry.js +110 -0
  50. package/dist/services/plugins/service.js +177 -0
  51. package/dist/services/plugins/types.js +1 -0
  52. package/dist/services/process.js +29 -0
  53. package/dist/services/storage.js +41 -15
  54. package/dist/services/thread-naming.js +81 -0
  55. package/docs/agents.md +16 -10
  56. package/docs/architecture.md +2 -2
  57. package/docs/plugins.md +6 -15
  58. package/docs/templates/AGENT.example.md +7 -13
  59. package/package.json +1 -2
  60. package/src/app/agent-ids.ts +5 -0
  61. package/src/app/cli.ts +1 -1
  62. package/src/app/config.ts +1 -31
  63. package/src/app/server.ts +8 -16
  64. package/src/app/types.ts +70 -190
  65. package/src/assets/icon.svg +9 -3
  66. package/src/harness/index.ts +145 -0
  67. package/src/plugins/approval/index.ts +91 -189
  68. package/src/plugins/delegation/index.ts +136 -39
  69. package/src/plugins/memory/index.ts +112 -15
  70. package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
  71. package/src/plugins/openbot/context.ts +91 -0
  72. package/src/plugins/openbot/history.ts +107 -0
  73. package/src/plugins/openbot/index.ts +37 -0
  74. package/src/plugins/openbot/runtime.ts +384 -0
  75. package/src/plugins/openbot/system-prompt.ts +7 -0
  76. package/src/plugins/plugin-manager/index.ts +122 -0
  77. package/src/plugins/shell/index.ts +1 -1
  78. package/src/plugins/storage/index.ts +633 -0
  79. package/src/{services/storage.ts → plugins/storage/service.ts} +257 -72
  80. package/src/{bus/types.ts → services/plugins/domain.ts} +20 -7
  81. package/src/services/plugins/plugin-cache.ts +13 -0
  82. package/src/{registry/plugins.ts → services/plugins/registry.ts} +25 -27
  83. package/src/services/{plugins.ts → plugins/service.ts} +96 -2
  84. package/src/{bus/plugin.ts → services/plugins/types.ts} +3 -3
  85. package/src/bus/services.ts +0 -908
  86. package/src/harness/context.ts +0 -356
  87. package/src/harness/dispatcher.ts +0 -379
  88. package/src/harness/mcp.ts +0 -78
  89. package/src/harness/runtime-factory.ts +0 -129
  90. package/src/harness/todo-advance.ts +0 -128
  91. package/src/plugins/ai-sdk/index.ts +0 -41
  92. package/src/plugins/ai-sdk/runtime.ts +0 -468
  93. package/src/plugins/ai-sdk/system-prompt.ts +0 -18
  94. package/src/plugins/mcp/index.ts +0 -128
  95. package/src/plugins/storage-tools/index.ts +0 -90
  96. package/src/plugins/todo/index.ts +0 -64
  97. package/src/plugins/ui/index.ts +0 -227
  98. /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,2 @@
1
+ /** Built-in orchestrator agent id (`~/.openbot/agents/system/AGENT.md` overrides instructions). */
2
+ export const ORCHESTRATOR_AGENT_ID = 'system';
@@ -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
+ }
@@ -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 AgentDetailsProvider {
90
+ class EnvironmentProvider {
68
91
  constructor() {
69
- this.name = 'agent-details';
92
+ this.name = 'environment';
70
93
  }
71
94
  async provide(state) {
72
- if (!state.agentDetails)
73
- return [];
74
- const instructions = state.agentDetails.instructions?.trim();
75
- if (!instructions)
76
- return [];
77
- return [{
78
- id: 'agent-details',
79
- type: 'agent',
80
- priority: 100,
81
- content: `# ${state.agentDetails.name}\n\n${instructions}`,
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
- class ChannelDetailsProvider {
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-details';
132
+ this.name = 'channel-spec';
88
133
  }
89
134
  async provide(state) {
90
- if (!state.channelDetails)
91
- return [];
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
- id: 'channel-details',
97
- type: 'channel',
98
- priority: 80,
99
- content: `# Channel you are in: ${state.channelDetails.name}\n\n Channel Specification: ${spec}`,
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 ThreadDetailsProvider {
152
+ class AgentDetailsProvider {
104
153
  constructor() {
105
- this.name = 'thread-details';
154
+ this.name = 'agent-details';
106
155
  }
107
156
  async provide(state) {
108
- if (!state.threadDetails)
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: 'thread-details',
115
- type: 'thread',
116
- priority: 90,
117
- content: `# Thread you are in: ${state.threadDetails.name}`,
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: `## Shared todo plan (thread state)\n` +
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 >= `keepFloor` is always kept,
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 = TokenBudgetProcessor.DEFAULT_BUDGET, keepFloor = TokenBudgetProcessor.KEEP_FLOOR) {
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 <= this.budget) {
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 = 8000;
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
+ }