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.
- package/.claude/agents/runtime-security-reviewer.md +32 -0
- package/.claude/hooks/format.sh +18 -0
- package/.claude/hooks/protect-secrets.sh +23 -0
- package/.claude/settings.json +26 -0
- package/.claude/skills/deploy/SKILL.md +26 -0
- package/.claude/skills/new-plugin/SKILL.md +46 -0
- package/.dockerignore +9 -0
- package/.mcp.json +8 -0
- package/AGENTS.md +2 -2
- package/CLAUDE.md +50 -0
- package/Dockerfile +41 -0
- package/deploy/README.md +61 -0
- package/deploy/entrypoint.sh +6 -0
- package/deploy/fly.toml +36 -0
- package/dist/app/bootstrap.js +29 -0
- package/dist/app/cli.js +3 -1
- package/dist/app/cloud-mode.js +26 -0
- package/dist/app/config.js +61 -16
- package/dist/app/responding-agent.js +32 -0
- package/dist/app/server.js +47 -9
- package/dist/plugins/approval/index.js +1 -1
- package/dist/plugins/bash/index.js +386 -164
- package/dist/plugins/memory/service.js +2 -5
- package/dist/plugins/openbot/context.js +17 -1
- package/dist/plugins/openbot/index.js +25 -9
- package/dist/plugins/openbot/model.js +59 -0
- package/dist/plugins/openbot/runtime.js +39 -56
- package/dist/plugins/openbot/system-prompt.js +44 -9
- package/dist/plugins/preview/index.js +267 -0
- package/dist/plugins/storage/index.js +3 -3
- package/dist/plugins/storage/service.js +58 -12
- package/dist/plugins/todo/index.js +131 -38
- package/dist/plugins/todo/service.js +93 -0
- package/dist/plugins/ui/index.js +8 -1
- package/dist/services/plugins/model-registry.js +101 -0
- package/dist/services/plugins/registry.js +6 -4
- package/dist/services/plugins/service.js +2 -6
- package/docs/agents.md +20 -2
- package/docs/architecture.md +1 -1
- package/docs/plugins.md +13 -3
- package/docs/templates/AGENT.example.md +1 -1
- package/package.json +5 -2
- package/pnpm-workspace.yaml +2 -0
- package/src/app/bootstrap.ts +43 -0
- package/src/app/cli.ts +3 -1
- package/src/app/cloud-mode.ts +41 -0
- package/src/app/config.ts +71 -19
- package/src/app/responding-agent.ts +46 -0
- package/src/app/server.ts +55 -9
- package/src/app/types.ts +167 -31
- package/src/plugins/approval/index.ts +1 -1
- package/src/plugins/bash/index.ts +498 -185
- package/src/plugins/memory/service.ts +2 -5
- package/src/plugins/openbot/context.ts +17 -1
- package/src/plugins/openbot/index.ts +27 -11
- package/src/plugins/openbot/model.ts +76 -0
- package/src/plugins/openbot/runtime.ts +47 -69
- package/src/plugins/openbot/system-prompt.ts +44 -9
- package/src/plugins/preview/index.ts +323 -0
- package/src/plugins/storage/index.ts +5 -8
- package/src/plugins/storage/service.ts +79 -15
- package/src/plugins/todo/index.ts +166 -0
- package/src/plugins/todo/service.ts +123 -0
- package/src/plugins/ui/index.ts +8 -1
- package/src/services/plugins/domain.ts +2 -0
- package/src/services/plugins/model-registry.ts +146 -0
- package/src/services/plugins/registry.ts +6 -4
- package/src/services/plugins/service.ts +2 -6
- package/dist/agents/openbot/index.js +0 -76
- package/dist/agents/openbot/middleware/approval.js +0 -132
- package/dist/agents/openbot/runtime.js +0 -289
- package/dist/agents/openbot/system-prompt.js +0 -32
- package/dist/agents/openbot/tools/delegation.js +0 -78
- package/dist/agents/openbot/tools/mcp.js +0 -99
- package/dist/agents/openbot/tools/shell.js +0 -91
- package/dist/agents/openbot/tools/storage.js +0 -75
- package/dist/agents/openbot/tools/ui.js +0 -176
- package/dist/agents/system.js +0 -33
- package/dist/bus/agent-package.js +0 -1
- package/dist/bus/plugin.js +0 -1
- package/dist/bus/services.js +0 -754
- package/dist/bus/types.js +0 -1
- package/dist/harness/agent-harness.js +0 -45
- package/dist/harness/agent-invoke-run.js +0 -44
- package/dist/harness/agent-turn.js +0 -99
- package/dist/harness/channel-participants.js +0 -40
- package/dist/harness/constants.js +0 -2
- package/dist/harness/context-meter.js +0 -97
- package/dist/harness/context.js +0 -363
- package/dist/harness/dispatch.js +0 -144
- package/dist/harness/dispatcher.js +0 -156
- package/dist/harness/event-normalizer.js +0 -59
- package/dist/harness/history.js +0 -177
- package/dist/harness/mcp.js +0 -61
- package/dist/harness/orchestration.js +0 -88
- package/dist/harness/orchestrator.js +0 -149
- package/dist/harness/participants.js +0 -22
- package/dist/harness/process.js +0 -29
- package/dist/harness/queue-processor.js +0 -187
- package/dist/harness/run-harness.js +0 -154
- package/dist/harness/run.js +0 -98
- package/dist/harness/runtime-factory.js +0 -73
- package/dist/harness/runtime.js +0 -57
- package/dist/harness/todo-advance.js +0 -93
- package/dist/harness/todo-dispatch.js +0 -51
- package/dist/harness/todos.js +0 -5
- package/dist/harness/turn.js +0 -79
- package/dist/harness/types.js +0 -1
- package/dist/plugins/ai-sdk/index.js +0 -34
- package/dist/plugins/ai-sdk/runtime.js +0 -395
- package/dist/plugins/ai-sdk/system-prompt.js +0 -18
- package/dist/plugins/ai-sdk.js +0 -331
- package/dist/plugins/approval.js +0 -163
- package/dist/plugins/delegation.js +0 -108
- package/dist/plugins/mcp/index.js +0 -108
- package/dist/plugins/mcp.js +0 -140
- package/dist/plugins/shell/index.js +0 -100
- package/dist/plugins/shell.js +0 -123
- package/dist/plugins/storage-tools/index.js +0 -76
- package/dist/plugins/storage.js +0 -737
- package/dist/plugins/thread-namer/index.js +0 -72
- package/dist/plugins/threads/index.js +0 -114
- package/dist/plugins/ui.js +0 -211
- package/dist/plugins/workflow/index.js +0 -65
- package/dist/registry/agents.js +0 -138
- package/dist/registry/plugins.js +0 -110
- package/dist/services/agent-packages.js +0 -103
- package/dist/services/memory.js +0 -152
- package/dist/services/plugins.js +0 -98
- package/dist/services/storage.js +0 -1028
- package/dist/workflow/service.js +0 -106
- 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,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
|
-
}
|
package/dist/harness/context.js
DELETED
|
@@ -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;
|