openbot 0.4.6 → 0.5.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.
Files changed (132) hide show
  1. package/.claude/agents/runtime-security-reviewer.md +32 -0
  2. package/.claude/hooks/format.sh +18 -0
  3. package/.claude/hooks/protect-secrets.sh +23 -0
  4. package/.claude/settings.json +26 -0
  5. package/.claude/skills/deploy/SKILL.md +26 -0
  6. package/.claude/skills/new-plugin/SKILL.md +46 -0
  7. package/.dockerignore +9 -0
  8. package/.mcp.json +8 -0
  9. package/AGENTS.md +2 -2
  10. package/CLAUDE.md +50 -0
  11. package/Dockerfile +41 -0
  12. package/deploy/README.md +61 -0
  13. package/deploy/entrypoint.sh +6 -0
  14. package/deploy/fly.toml +36 -0
  15. package/dist/app/bootstrap.js +29 -0
  16. package/dist/app/cli.js +3 -1
  17. package/dist/app/cloud-mode.js +26 -0
  18. package/dist/app/config.js +61 -16
  19. package/dist/app/responding-agent.js +32 -0
  20. package/dist/app/server.js +47 -9
  21. package/dist/plugins/approval/index.js +1 -1
  22. package/dist/plugins/bash/index.js +386 -164
  23. package/dist/plugins/memory/service.js +2 -5
  24. package/dist/plugins/openbot/context.js +17 -1
  25. package/dist/plugins/openbot/index.js +25 -9
  26. package/dist/plugins/openbot/model.js +59 -0
  27. package/dist/plugins/openbot/runtime.js +39 -56
  28. package/dist/plugins/openbot/system-prompt.js +44 -9
  29. package/dist/plugins/preview/index.js +267 -0
  30. package/dist/plugins/storage/index.js +3 -3
  31. package/dist/plugins/storage/service.js +58 -12
  32. package/dist/plugins/todo/index.js +131 -38
  33. package/dist/plugins/todo/service.js +93 -0
  34. package/dist/plugins/ui/index.js +8 -1
  35. package/dist/services/plugins/model-registry.js +101 -0
  36. package/dist/services/plugins/registry.js +6 -4
  37. package/dist/services/plugins/service.js +2 -6
  38. package/docs/agents.md +20 -2
  39. package/docs/architecture.md +1 -1
  40. package/docs/plugins.md +13 -3
  41. package/docs/templates/AGENT.example.md +1 -1
  42. package/package.json +5 -2
  43. package/pnpm-workspace.yaml +2 -0
  44. package/src/app/bootstrap.ts +43 -0
  45. package/src/app/cli.ts +3 -1
  46. package/src/app/cloud-mode.ts +41 -0
  47. package/src/app/config.ts +71 -19
  48. package/src/app/responding-agent.ts +46 -0
  49. package/src/app/server.ts +55 -9
  50. package/src/app/types.ts +167 -31
  51. package/src/plugins/approval/index.ts +1 -1
  52. package/src/plugins/bash/index.ts +498 -185
  53. package/src/plugins/memory/service.ts +2 -5
  54. package/src/plugins/openbot/context.ts +17 -1
  55. package/src/plugins/openbot/index.ts +27 -11
  56. package/src/plugins/openbot/model.ts +76 -0
  57. package/src/plugins/openbot/runtime.ts +47 -69
  58. package/src/plugins/openbot/system-prompt.ts +44 -9
  59. package/src/plugins/preview/index.ts +323 -0
  60. package/src/plugins/storage/index.ts +5 -8
  61. package/src/plugins/storage/service.ts +79 -15
  62. package/src/plugins/todo/index.ts +166 -0
  63. package/src/plugins/todo/service.ts +123 -0
  64. package/src/plugins/ui/index.ts +8 -1
  65. package/src/services/plugins/domain.ts +2 -0
  66. package/src/services/plugins/model-registry.ts +146 -0
  67. package/src/services/plugins/registry.ts +6 -4
  68. package/src/services/plugins/service.ts +2 -6
  69. package/dist/agents/openbot/index.js +0 -76
  70. package/dist/agents/openbot/middleware/approval.js +0 -132
  71. package/dist/agents/openbot/runtime.js +0 -289
  72. package/dist/agents/openbot/system-prompt.js +0 -32
  73. package/dist/agents/openbot/tools/delegation.js +0 -78
  74. package/dist/agents/openbot/tools/mcp.js +0 -99
  75. package/dist/agents/openbot/tools/shell.js +0 -91
  76. package/dist/agents/openbot/tools/storage.js +0 -75
  77. package/dist/agents/openbot/tools/ui.js +0 -176
  78. package/dist/agents/system.js +0 -33
  79. package/dist/bus/agent-package.js +0 -1
  80. package/dist/bus/plugin.js +0 -1
  81. package/dist/bus/services.js +0 -754
  82. package/dist/bus/types.js +0 -1
  83. package/dist/harness/agent-harness.js +0 -45
  84. package/dist/harness/agent-invoke-run.js +0 -44
  85. package/dist/harness/agent-turn.js +0 -99
  86. package/dist/harness/channel-participants.js +0 -40
  87. package/dist/harness/constants.js +0 -2
  88. package/dist/harness/context-meter.js +0 -97
  89. package/dist/harness/context.js +0 -363
  90. package/dist/harness/dispatch.js +0 -144
  91. package/dist/harness/dispatcher.js +0 -156
  92. package/dist/harness/event-normalizer.js +0 -59
  93. package/dist/harness/history.js +0 -177
  94. package/dist/harness/mcp.js +0 -61
  95. package/dist/harness/orchestration.js +0 -88
  96. package/dist/harness/orchestrator.js +0 -149
  97. package/dist/harness/participants.js +0 -22
  98. package/dist/harness/process.js +0 -29
  99. package/dist/harness/queue-processor.js +0 -187
  100. package/dist/harness/run-harness.js +0 -154
  101. package/dist/harness/run.js +0 -98
  102. package/dist/harness/runtime-factory.js +0 -73
  103. package/dist/harness/runtime.js +0 -57
  104. package/dist/harness/todo-advance.js +0 -93
  105. package/dist/harness/todo-dispatch.js +0 -51
  106. package/dist/harness/todos.js +0 -5
  107. package/dist/harness/turn.js +0 -79
  108. package/dist/harness/types.js +0 -1
  109. package/dist/plugins/ai-sdk/index.js +0 -34
  110. package/dist/plugins/ai-sdk/runtime.js +0 -395
  111. package/dist/plugins/ai-sdk/system-prompt.js +0 -18
  112. package/dist/plugins/ai-sdk.js +0 -331
  113. package/dist/plugins/approval.js +0 -163
  114. package/dist/plugins/delegation.js +0 -108
  115. package/dist/plugins/mcp/index.js +0 -108
  116. package/dist/plugins/mcp.js +0 -140
  117. package/dist/plugins/shell/index.js +0 -100
  118. package/dist/plugins/shell.js +0 -123
  119. package/dist/plugins/storage-tools/index.js +0 -76
  120. package/dist/plugins/storage.js +0 -737
  121. package/dist/plugins/thread-namer/index.js +0 -72
  122. package/dist/plugins/threads/index.js +0 -114
  123. package/dist/plugins/ui.js +0 -211
  124. package/dist/plugins/workflow/index.js +0 -65
  125. package/dist/registry/agents.js +0 -138
  126. package/dist/registry/plugins.js +0 -110
  127. package/dist/services/agent-packages.js +0 -103
  128. package/dist/services/memory.js +0 -152
  129. package/dist/services/plugins.js +0 -98
  130. package/dist/services/storage.js +0 -1028
  131. package/dist/workflow/service.js +0 -106
  132. package/dist/workflow/types.js +0 -3
package/dist/bus/types.js DELETED
@@ -1 +0,0 @@
1
- export {};
@@ -1,45 +0,0 @@
1
- import { orchestratorService } from './orchestrator.js';
2
- import { ensureEventId } from '../app/utils.js';
3
- /**
4
- * Standard implementation of the Agent Harness.
5
- * It wraps the orchestrator logic into a clean, stateful container.
6
- */
7
- export class AgentHarness {
8
- constructor(options) {
9
- this.eventCallbacks = [];
10
- this.runId = options.runId;
11
- this.agentId = options.agentId;
12
- this.channelId = options.channelId;
13
- this.threadId = options.threadId;
14
- if (options.onEvent) {
15
- this.eventCallbacks.push(options.onEvent);
16
- }
17
- }
18
- /**
19
- * Dispatches an event to the agent within this harness.
20
- */
21
- async dispatch(event) {
22
- ensureEventId(event);
23
- await orchestratorService.dispatch({
24
- runId: this.runId,
25
- agentId: this.agentId,
26
- event,
27
- channelId: this.channelId,
28
- threadId: this.threadId,
29
- onEvent: async (chunk, state) => {
30
- // Update internal thread state if it changes (e.g. thread creation)
31
- if (chunk.type === 'action:create_thread:result' && chunk.data.success) {
32
- this.threadId = chunk.data.threadId || this.threadId;
33
- }
34
- // Notify all observers
35
- await Promise.all(this.eventCallbacks.map(cb => cb(chunk, state)));
36
- }
37
- });
38
- }
39
- /**
40
- * Adds an event listener to the harness.
41
- */
42
- onEvent(callback) {
43
- this.eventCallbacks.push(callback);
44
- }
45
- }
@@ -1,44 +0,0 @@
1
- import { createAgentRuntime } from './runtime-factory.js';
2
- /**
3
- * Shared `runtime.run(agent:invoke)` loop: tags chunks with `agentId`, skips echo of the
4
- * trigger event, optionally polls for stop, tracks last `agent:output` text.
5
- *
6
- * Does not emit `agent:run:start` / `agent:run:end` — callers bracket those.
7
- */
8
- export async function* streamTaggedAgentInvokeRuntime(options) {
9
- const { target, event, state, pollInterrupt } = options;
10
- const { runId, agentId } = target;
11
- let lastAgentOutput;
12
- const runtime = await createAgentRuntime(state);
13
- for await (const chunk of runtime.run(event, { state, runId })) {
14
- const interrupt = pollInterrupt?.();
15
- if (interrupt) {
16
- yield interrupt;
17
- break;
18
- }
19
- if (chunk.id === event.id && chunk.type === event.type)
20
- continue;
21
- if (chunk.type === 'agent:output' &&
22
- chunk.meta?.agentId === agentId) {
23
- const content = chunk.data?.content;
24
- if (typeof content === 'string' && content.trim()) {
25
- lastAgentOutput = content.trim();
26
- }
27
- }
28
- chunk.meta = { ...chunk.meta, agentId };
29
- yield chunk;
30
- }
31
- return { lastAgentOutput };
32
- }
33
- /**
34
- * Drive {@link streamTaggedAgentInvokeRuntime} with a callback per chunk (dispatcher path).
35
- */
36
- export async function consumeAgentInvokeStream(options, onChunk) {
37
- const gen = streamTaggedAgentInvokeRuntime(options);
38
- let step = await gen.next();
39
- while (!step.done) {
40
- await onChunk(step.value);
41
- step = await gen.next();
42
- }
43
- return step.value;
44
- }
@@ -1,99 +0,0 @@
1
- import { ensureEventId } from '../app/utils.js';
2
- import { storageService } from '../services/storage.js';
3
- import { createAgentRuntime } from './runtime-factory.js';
4
- const TODO_RESULT_MAX_CHARS = 12000;
5
- const readThreadState = (state) => state.threadDetails?.state ?? {};
6
- const readTodos = (state) => {
7
- const raw = readThreadState(state).todos;
8
- return Array.isArray(raw) ? raw : [];
9
- };
10
- function truncateTodoResult(text, maxChars = TODO_RESULT_MAX_CHARS) {
11
- const trimmed = text.trim();
12
- if (!trimmed)
13
- return undefined;
14
- if (trimmed.length <= maxChars)
15
- return trimmed;
16
- return `${trimmed.slice(0, maxChars)}\n…[truncated]`;
17
- }
18
- function resolveTodoIdForWorker(todos, workerId, delegationTodoId) {
19
- if (delegationTodoId && todos.some((t) => t.id === delegationTodoId)) {
20
- return delegationTodoId;
21
- }
22
- const inProgress = todos.find((t) => t.status === 'in_progress' && t.assignee === workerId);
23
- if (inProgress)
24
- return inProgress.id;
25
- const assigned = todos.find((t) => (t.status === 'pending' || t.status === 'in_progress') && t.assignee === workerId);
26
- return assigned?.id;
27
- }
28
- export async function recordWorkerTodoResult(state, workerId, output, delegationTodoId) {
29
- if (!state.threadId)
30
- return;
31
- const result = truncateTodoResult(output ?? '');
32
- if (!result)
33
- return;
34
- const todos = readTodos(state);
35
- if (todos.length === 0)
36
- return;
37
- const todoId = resolveTodoIdForWorker(todos, workerId, delegationTodoId);
38
- if (!todoId)
39
- return;
40
- const prior = todos.find((t) => t.id === todoId);
41
- if (prior?.result === result)
42
- return;
43
- const now = Date.now();
44
- const next = todos.map((t) => (t.id === todoId ? { ...t, result, updatedAt: now } : t));
45
- await storageService.patchThreadState({
46
- channelId: state.channelId,
47
- threadId: state.threadId,
48
- state: { todos: next },
49
- });
50
- }
51
- export function makeInternalInvoke(content, threadId) {
52
- return ensureEventId({
53
- type: 'agent:invoke',
54
- data: { role: 'user', content },
55
- meta: { threadId, internal: true },
56
- });
57
- }
58
- /**
59
- * Run one agent turn (no dispatcher chaining). Yields all runtime events for
60
- * persistence/streaming; returns the last non-empty `agent:output` text.
61
- */
62
- export async function* runAgentTurn(options) {
63
- const { runId, channelId, threadId, agentId, event, delegationTodoId } = options;
64
- const target = { runId, agentId, channelId, threadId };
65
- let state;
66
- try {
67
- state = await storageService.getOpenBotState({ ...target, event });
68
- }
69
- catch (error) {
70
- if (error.code === 'AGENT_NOT_FOUND') {
71
- return undefined;
72
- }
73
- throw error;
74
- }
75
- yield { type: 'agent:run:start', data: { ...target } };
76
- let lastAgentOutput;
77
- try {
78
- const runtime = await createAgentRuntime(state);
79
- for await (const chunk of runtime.run(event, { state, runId })) {
80
- if (chunk.id === event.id && chunk.type === event.type)
81
- continue;
82
- if (chunk.type === 'agent:output' &&
83
- chunk.meta?.agentId === agentId) {
84
- const content = chunk.data?.content;
85
- if (typeof content === 'string' && content.trim()) {
86
- lastAgentOutput = content.trim();
87
- }
88
- }
89
- chunk.meta = { ...chunk.meta, agentId };
90
- yield chunk;
91
- }
92
- }
93
- finally {
94
- const stateAfterRun = await storageService.getOpenBotState({ ...target, event });
95
- yield { type: 'agent:run:end', data: { ...target } };
96
- await recordWorkerTodoResult(stateAfterRun, agentId, lastAgentOutput, delegationTodoId);
97
- }
98
- return lastAgentOutput;
99
- }
@@ -1,40 +0,0 @@
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
- }
@@ -1,2 +0,0 @@
1
- /** Built-in orchestrator agent id (`~/.openbot/agents/system/AGENT.md` overrides instructions). */
2
- export const ORCHESTRATOR_AGENT_ID = 'system';
@@ -1,97 +0,0 @@
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,363 +0,0 @@
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';
24
- /**
25
- * Cheap, dependency-free token estimator. Roughly char/4 — fine for budget
26
- * enforcement; can be swapped for a tokenizer-backed implementation later
27
- * without touching providers.
28
- */
29
- export const estimateTokens = (text) => Math.ceil((text?.length ?? 0) / 4);
30
- /**
31
- * Hard cap (in characters) on a single context item. Keeps any one provider
32
- * — typically the recent-events feed — from monopolising the prompt budget.
33
- */
34
- const ITEM_HARD_CHAR_CAP = 6000;
35
- const truncate = (text, maxChars) => text.length <= maxChars ? text : `${text.slice(0, maxChars)}\n…[truncated]`;
36
- export class ContextEngine {
37
- constructor() {
38
- this.providers = [];
39
- this.processors = [];
40
- }
41
- registerProvider(provider) {
42
- this.providers.push(provider);
43
- }
44
- registerProcessor(processor) {
45
- this.processors.push(processor);
46
- }
47
- async buildContext(state, storage) {
48
- let items = [];
49
- for (const provider of this.providers) {
50
- try {
51
- const providedItems = await provider.provide(state, storage);
52
- for (const item of providedItems) {
53
- items.push({ ...item, content: truncate(item.content, ITEM_HARD_CHAR_CAP) });
54
- }
55
- }
56
- catch (error) {
57
- console.warn(`[ContextEngine] Provider ${provider.name} failed:`, error);
58
- }
59
- }
60
- for (const processor of this.processors) {
61
- try {
62
- items = await processor.process(items, state);
63
- }
64
- catch (error) {
65
- console.warn(`[ContextEngine] Processor ${processor.name} failed:`, error);
66
- }
67
- }
68
- return items
69
- .sort((a, b) => b.priority - a.priority)
70
- .map((item) => item.content)
71
- .join('\n\n');
72
- }
73
- }
74
- /**
75
- * Default context engine. Order of providers is by emit order; final ordering
76
- * in the prompt is determined by `priority`. The token-budget processor runs
77
- * last so dropping happens after every provider has contributed.
78
- */
79
- export function createDefaultContextEngine() {
80
- const engine = new ContextEngine();
81
- engine.registerProvider(new EnvironmentProvider());
82
- engine.registerProvider(new ChannelSpecProvider());
83
- engine.registerProvider(new AgentDetailsProvider());
84
- engine.registerProvider(new TodoProvider());
85
- engine.registerProvider(new MemoryProvider());
86
- // engine.registerProvider(new RecentEventsProvider());
87
- engine.registerProcessor(new TokenBudgetProcessor());
88
- return engine;
89
- }
90
- class EnvironmentProvider {
91
- constructor() {
92
- this.name = 'environment';
93
- }
94
- async provide(state) {
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
- ];
123
- }
124
- }
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 {
131
- constructor() {
132
- this.name = 'channel-spec';
133
- }
134
- async provide(state) {
135
- const raw = state.channelDetails?.spec;
136
- const spec = typeof raw === 'string' ? raw.trim() : '';
137
- if (!spec)
138
- return [];
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
- ];
150
- }
151
- }
152
- class AgentDetailsProvider {
153
- constructor() {
154
- this.name = 'agent-details';
155
- }
156
- async provide(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)
163
- return [];
164
- return [
165
- {
166
- id: 'agent-details',
167
- type: 'agent',
168
- priority: 100,
169
- content: `## AGENT: ${state.agentDetails.name}\n\n${instructions}`,
170
- },
171
- ];
172
- }
173
- }
174
- /**
175
- * Surfaces the shared per-thread todo list. The list lives in
176
- * `threadDetails.state.todos` and is owned by bus services — every agent in
177
- * the thread reads from the same canonical source, which is how multi-agent
178
- * autonomous flows stay coordinated.
179
- */
180
- class TodoProvider {
181
- constructor() {
182
- this.name = 'todos';
183
- }
184
- async provide(state) {
185
- if (state.agentId !== ORCHESTRATOR_AGENT_ID)
186
- return [];
187
- const raw = state.threadDetails?.state?.todos;
188
- const todos = Array.isArray(raw) ? raw : [];
189
- if (todos.length === 0)
190
- return [];
191
- const DISPLAY_RESULT_CAP = 2500;
192
- const marker = {
193
- pending: '[ ]',
194
- in_progress: '[~]',
195
- done: '[x]',
196
- cancelled: '[-]',
197
- };
198
- const formatted = todos
199
- .map((t) => {
200
- const assignee = t.assignee ? ` @${t.assignee}` : '';
201
- let line = `- ${marker[t.status]} (${t.id})${assignee} ${t.content}`;
202
- if (t.status === 'done' && t.result?.trim()) {
203
- let snippet = t.result.trim();
204
- if (snippet.length > DISPLAY_RESULT_CAP) {
205
- snippet = `${snippet.slice(0, DISPLAY_RESULT_CAP)}…[truncated]`;
206
- }
207
- line += `\n Result: ${snippet}`;
208
- }
209
- return line;
210
- })
211
- .join('\n');
212
- return [
213
- {
214
- id: 'todos',
215
- type: 'todos',
216
- priority: 92,
217
- content: `## SHARED TODO PLAN (thread state)\n` +
218
- `${formatted}`,
219
- },
220
- ];
221
- }
222
- }
223
- /**
224
- * Fetches relevant memories (global + active agent + active channel) and
225
- * surfaces them at high priority so the LLM treats them as ground truth
226
- * rather than chat history.
227
- */
228
- class MemoryProvider {
229
- constructor() {
230
- this.name = 'memory';
231
- }
232
- async provide(state, storage) {
233
- if (!storage?.listMemories)
234
- return [];
235
- try {
236
- const scopes = ['global', `agent:${state.agentId}`];
237
- if (state.channelId)
238
- scopes.push(`channel:${state.channelId}`);
239
- const records = await storage.listMemories({ scopes, limit: 50 });
240
- if (records.length === 0)
241
- return [];
242
- const formatted = records
243
- .map((r) => {
244
- const tags = r.tags?.length ? ` [${r.tags.join(', ')}]` : '';
245
- const scopeLabel = r.scope === 'global' ? 'global' : r.scope;
246
- return `- (${scopeLabel}${tags}) ${r.content}`;
247
- })
248
- .join('\n');
249
- return [
250
- {
251
- id: 'memory',
252
- type: 'memory',
253
- priority: 95,
254
- content: `## Remembered global facts\nTrust these unless the user contradicts them. Use \`forget\` to remove stale ones.\n\n${formatted}`,
255
- },
256
- ];
257
- }
258
- catch (error) {
259
- console.warn('[ContextEngine] MemoryProvider failed:', error);
260
- return [];
261
- }
262
- }
263
- }
264
- /**
265
- * Event types we omit from the recent-events context block. They duplicate
266
- * information already in the conversation history, are infrastructural
267
- * noise, or are too large to be useful as a tail summary.
268
- */
269
- const NOISY_EVENT_PREFIXES = [
270
- 'agent:invoke',
271
- 'agent:output',
272
- 'agent:run',
273
- 'agent:active-runs',
274
- 'client:ui',
275
- 'stream:',
276
- 'action:storage:get-',
277
- 'action:storage:patch-',
278
- ];
279
- const MAX_RECENT_EVENTS = 20;
280
- const MAX_EVENT_DATA_CHARS = 300;
281
- const isNoisyEvent = (event) => NOISY_EVENT_PREFIXES.some((prefix) => event.type.startsWith(prefix));
282
- const summarizeEvent = (event) => {
283
- const data = event.data;
284
- if (data === undefined)
285
- return `- ${event.type}`;
286
- let payload;
287
- try {
288
- payload = typeof data === 'string' ? data : JSON.stringify(data);
289
- }
290
- catch {
291
- payload = '[unserialisable]';
292
- }
293
- if (payload.length > MAX_EVENT_DATA_CHARS) {
294
- payload = `${payload.slice(0, MAX_EVENT_DATA_CHARS)}…`;
295
- }
296
- return `- ${event.type}: ${payload}`;
297
- };
298
- class RecentEventsProvider {
299
- constructor() {
300
- this.name = 'recent-events';
301
- }
302
- async provide(state, storage) {
303
- if (!storage)
304
- return [];
305
- const channelId = state.channelId;
306
- const threadId = state.threadId;
307
- try {
308
- const events = await storage.getEvents({ channelId, threadId });
309
- const filtered = events.filter((e) => !isNoisyEvent(e));
310
- if (filtered.length === 0)
311
- return [];
312
- const formatted = filtered.slice(-MAX_RECENT_EVENTS).map(summarizeEvent).join('\n');
313
- return [
314
- {
315
- id: threadId ? 'thread-events' : 'channel-events',
316
- type: 'events',
317
- priority: 70,
318
- content: `## ${threadId ? 'THREAD' : 'CHANNEL'} RECENT ACTIVITIES (events)\n${formatted}`,
319
- },
320
- ];
321
- }
322
- catch (error) {
323
- console.warn('[ContextEngine] Failed to fetch events:', error);
324
- return [];
325
- }
326
- }
327
- }
328
- /**
329
- * Drops the lowest-priority items until the assembled prompt fits within the
330
- * token budget. The first item with priority >= \`keepFloor\` is always kept,
331
- * so the agent's own instructions can never be evicted. Stable on ties:
332
- * later-emitted items go first.
333
- */
334
- export class TokenBudgetProcessor {
335
- constructor(budget = undefined, keepFloor = TokenBudgetProcessor.KEEP_FLOOR) {
336
- this.budget = budget;
337
- this.keepFloor = keepFloor;
338
- this.name = 'token-budget';
339
- }
340
- async process(items, state) {
341
- const sorted = [...items].sort((a, b) => b.priority - a.priority);
342
- const out = [];
343
- let used = 0;
344
- const activeBudget = this.budget ?? (state.model ? getContextBudgetForModel(state.model) : TokenBudgetProcessor.DEFAULT_BUDGET);
345
- for (const item of sorted) {
346
- const cost = estimateTokens(item.content);
347
- if (item.priority >= this.keepFloor) {
348
- out.push(item);
349
- used += cost;
350
- continue;
351
- }
352
- if (used + cost <= activeBudget) {
353
- out.push(item);
354
- used += cost;
355
- }
356
- }
357
- return out;
358
- }
359
- }
360
- /** Soft prompt budget in tokens (matches gpt-4o-mini's reasonable system slice). */
361
- TokenBudgetProcessor.DEFAULT_BUDGET = DEFAULT_CONTEXT_BUDGET;
362
- /** Items at or above this priority are never dropped. */
363
- TokenBudgetProcessor.KEEP_FLOOR = 100;