@meetopenbot/openbot 1.2.14 → 1.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/context.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { formatGitIdentity, readGitIdentity, } from "@meetopenbot/plugin-sdk";
1
2
  import { formatAvailableAgentsSection, formatInstalledAgentsSection, loadAgentModelCatalogs, availableMarketplaceAgents, } from "./agent-models.js";
2
3
  import { GENERAL_SPACE_ID } from "./space-id.js";
3
4
  import { todoService } from "./tools/todo-service.js";
@@ -47,6 +48,16 @@ export async function buildContext(state, storage, host) {
47
48
  : "- Role: dedicated Space — do the work here with `ask_agent`\n";
48
49
  if (channelDetails?.cwd) {
49
50
  env += `- Workspace: ${channelDetails.cwd}\n`;
51
+ if (!isGeneral) {
52
+ try {
53
+ const git = readGitIdentity(channelDetails.cwd);
54
+ if (git)
55
+ env += `- Git: ${formatGitIdentity(git)}\n`;
56
+ }
57
+ catch (error) {
58
+ console.warn("[context] Failed to read git identity:", error);
59
+ }
60
+ }
50
61
  }
51
62
  if (threadId) {
52
63
  env += `- Thread: ${threadDetails?.name || threadId}\n`;
package/dist/images.js CHANGED
@@ -1,17 +1,32 @@
1
1
  import { BYOK_ENV_KEYS, shouldUseCreditsAuth } from '@meetopenbot/plugin-sdk';
2
+ import { expandModelString, isAutoModel, listAutoModelCandidates, } from './auto-model.js';
2
3
  export function isImageAttachment(file) {
3
4
  if (file.mimeType?.startsWith('image/'))
4
5
  return true;
5
6
  return /\.(png|jpe?g|gif|webp)$/i.test(file.name);
6
7
  }
7
- export function eventsHaveImages(events) {
8
- return events.some((event) => {
9
- if (event.meta?.parentToolCallId)
10
- return false;
11
- if (event.type !== 'agent:invoke')
12
- return false;
13
- return Boolean(event.data?.attachments?.some(isImageAttachment));
14
- });
8
+ function topLevelUserInvokes(events) {
9
+ return events.filter((event) => !event.meta?.parentToolCallId &&
10
+ event.type === 'agent:invoke' &&
11
+ Boolean(event.data?.role) &&
12
+ invokeRole(event.data.role) === 'user');
13
+ }
14
+ /** True when the newest top-level user message carries an image. */
15
+ export function latestUserMessageHasImages(events) {
16
+ const invokes = topLevelUserInvokes(events);
17
+ const latest = invokes[invokes.length - 1];
18
+ return Boolean(latest?.data?.attachments?.some(isImageAttachment));
19
+ }
20
+ /** True when any top-level user message carries an attachment. */
21
+ export function eventsHaveAttachments(events) {
22
+ return topLevelUserInvokes(events).some((event) => Boolean(event.data?.attachments?.length));
23
+ }
24
+ /** Providers whose models in the picker cannot take image input. */
25
+ const NO_IMAGE_INPUT_PROVIDERS = new Set(['deepseek']);
26
+ /** Whether the model for this turn can see images. Auto counts as its concrete pool model. */
27
+ export function modelSupportsImages(modelString, ctx) {
28
+ const provider = expandModelString(modelString, ctx).split('/')[0] ?? '';
29
+ return !NO_IMAGE_INPUT_PROVIDERS.has(provider);
15
30
  }
16
31
  function hasAnyKey(env, keys) {
17
32
  return Boolean(keys?.some((key) => Boolean(env[key]?.trim())));
@@ -33,8 +48,57 @@ export function pickVisionModel(authMode, env = process.env) {
33
48
  function invokeRole(role) {
34
49
  return role === 'agent' ? 'user' : role;
35
50
  }
36
- /** Put image files on matching user messages as AI SDK `file` parts. */
37
- export async function addImageParts(messages, events, read) {
51
+ /**
52
+ * Models to try this turn. Auto switches to a cheap vision model only when the
53
+ * newest user message has an image and a vision model is available. Never throws.
54
+ */
55
+ export function chooseTurnCandidates({ model, events, authMode, env = process.env, }) {
56
+ if (!isAutoModel(model))
57
+ return [model.trim()];
58
+ if (latestUserMessageHasImages(events)) {
59
+ const vision = pickVisionModel(authMode, env);
60
+ if (vision)
61
+ return [vision];
62
+ }
63
+ return listAutoModelCandidates({ authMode, env });
64
+ }
65
+ function formatSize(bytes) {
66
+ if (bytes < 1024)
67
+ return `${bytes} B`;
68
+ if (bytes < 1024 * 1024)
69
+ return `${Math.round(bytes / 1024)} KB`;
70
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
71
+ }
72
+ function fileType(file) {
73
+ if (file.mimeType)
74
+ return file.mimeType;
75
+ const match = /\.([a-z0-9]+)$/i.exec(file.name);
76
+ return match ? match[1].toLowerCase() : 'file';
77
+ }
78
+ /** Text note listing attached files with their full paths. */
79
+ export function attachmentNote(files) {
80
+ const lines = files.map((file) => {
81
+ const details = [fileType(file)];
82
+ if (typeof file.size === 'number' && file.size > 0)
83
+ details.push(formatSize(file.size));
84
+ const where = file.path ?? "in this thread's attachments";
85
+ const suffix = file.imageNotShown
86
+ ? ' — not shown to you; the current model cannot view images'
87
+ : '';
88
+ return `- ${file.name} (${details.join(', ')}): ${where}${suffix}`;
89
+ });
90
+ return [
91
+ '[Attached files, saved on this computer:',
92
+ ...lines,
93
+ 'You cannot open these files yourself. If a coding agent should use one, give them its full path.]',
94
+ ].join('\n');
95
+ }
96
+ /**
97
+ * Put attachments on matching user messages. Images go as AI SDK `file` parts
98
+ * when `includeImages` is set and the read succeeds; every other file (and any
99
+ * image not shown) becomes one text note with its full path.
100
+ */
101
+ export async function addAttachmentParts(messages, events, { includeImages, read, locate, }) {
38
102
  const result = [...messages];
39
103
  let userCursor = 0;
40
104
  const takeNextUserMessageIndex = () => {
@@ -46,6 +110,23 @@ export async function addImageParts(messages, events, read) {
46
110
  }
47
111
  return -1;
48
112
  };
113
+ const safeRead = async (name) => {
114
+ try {
115
+ const loaded = await read(name);
116
+ return loaded?.data.byteLength ? loaded : null;
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ };
122
+ const safeLocate = async (name) => {
123
+ try {
124
+ return (await locate(name)) || null;
125
+ }
126
+ catch {
127
+ return null;
128
+ }
129
+ };
49
130
  for (const event of events) {
50
131
  if (event.meta?.parentToolCallId)
51
132
  continue;
@@ -58,27 +139,39 @@ export async function addImageParts(messages, events, read) {
58
139
  if (invokeRole(event.data.role) !== 'user')
59
140
  continue;
60
141
  const index = takeNextUserMessageIndex();
61
- const files = attachments.filter(isImageAttachment);
62
- if (index === -1 || files.length === 0)
142
+ if (index === -1 || attachments.length === 0)
63
143
  continue;
64
144
  const current = result[index];
65
145
  const parts = [];
66
146
  if (typeof current.content === 'string' && current.content) {
67
147
  parts.push({ type: 'text', text: current.content });
68
148
  }
69
- for (const file of files) {
70
- const loaded = await read(file.name);
71
- if (!loaded?.data.byteLength)
72
- continue;
73
- parts.push({
74
- type: 'file',
75
- data: loaded.data,
76
- mediaType: loaded.mediaType || 'image/png',
149
+ const noted = [];
150
+ for (const file of attachments) {
151
+ const isImage = isImageAttachment(file);
152
+ if (isImage && includeImages) {
153
+ const loaded = await safeRead(file.name);
154
+ if (loaded) {
155
+ parts.push({
156
+ type: 'file',
157
+ data: loaded.data,
158
+ mediaType: loaded.mediaType || 'image/png',
159
+ });
160
+ continue;
161
+ }
162
+ }
163
+ noted.push({
164
+ name: file.name,
165
+ mimeType: file.mimeType,
166
+ size: file.size,
167
+ path: await safeLocate(file.name),
168
+ imageNotShown: isImage,
77
169
  });
78
170
  }
79
- if (parts.some((part) => part.type === 'file')) {
171
+ if (noted.length > 0)
172
+ parts.push({ type: 'text', text: attachmentNote(noted) });
173
+ if (parts.length > 0)
80
174
  result[index] = { role: 'user', content: parts };
81
- }
82
175
  }
83
176
  return result;
84
177
  }
package/dist/runtime.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { streamText, stepCountIs } from 'ai';
2
2
  import { BYOK_ENV_KEYS, isAuthErrorMessage, isCreditsErrorMessage, fetchModelRegistry, getProviderModelOptions, listApiKeyProviders, PROVIDER_API_KEY_LINKS, } from '@meetopenbot/plugin-sdk';
3
3
  import { eventsToModelMessages } from './history.js';
4
- import { addImageParts, eventsHaveImages, pickVisionModel } from './images.js';
4
+ import { addAttachmentParts, chooseTurnCandidates, eventsHaveAttachments, modelSupportsImages, } from './images.js';
5
5
  import { buildContext } from './context.js';
6
6
  import { OPENBOT_SYSTEM_PROMPT, withGithubGitInstructions, withUserInstructions, } from './system-prompt.js';
7
- import { AUTO_MODEL_ID, isAutoModel, isRetryableModelError, listAutoModelCandidates, missingByokApiKeyMessage, } from './auto-model.js';
7
+ import { AUTO_MODEL_ID, isAutoModel, isRetryableModelError, missingByokApiKeyMessage, } from './auto-model.js';
8
8
  import { reasoningForModel, resolveModel } from './model.js';
9
9
  import { errorFromStreamPart } from './stream-error.js';
10
10
  import { createAgentOutputBuffer, isOutputBoundaryPart } from './output-buffer.js';
@@ -100,18 +100,39 @@ export async function* runOpenbotTurn(args, options) {
100
100
  channelId: state.channelId ?? '',
101
101
  threadId,
102
102
  });
103
- const hasImages = eventsHaveImages(events);
104
- let candidates = isAutoModel(configuredModelString)
105
- ? listAutoModelCandidates({ authMode })
106
- : [configuredModelString];
107
- try {
108
- if (hasImages && isAutoModel(configuredModelString)) {
109
- const vision = pickVisionModel(authMode);
110
- if (!vision) {
111
- throw new Error('Looking at images needs an OpenAI, Google, or Anthropic API key. Auto\'s usual model cannot see pictures.');
112
- }
113
- candidates = [vision];
103
+ const hasAttachments = eventsHaveAttachments(events);
104
+ const candidates = chooseTurnCandidates({ model: configuredModelString, events, authMode });
105
+ const readImage = async (name) => {
106
+ try {
107
+ const file = await storage.readThreadAttachment({
108
+ channelId: state.channelId,
109
+ threadId,
110
+ name,
111
+ });
112
+ return { data: new Uint8Array(file.body), mediaType: file.mimeType };
113
+ }
114
+ catch (error) {
115
+ console.warn('[openbot] could not read attachment', name, error);
116
+ return null;
117
+ }
118
+ };
119
+ const locateAttachment = async (name) => {
120
+ if (typeof storage.getThreadAttachmentStat !== 'function')
121
+ return null;
122
+ try {
123
+ const stat = await storage.getThreadAttachmentStat({
124
+ channelId: state.channelId,
125
+ threadId,
126
+ name,
127
+ });
128
+ return typeof stat?.abs === 'string' ? stat.abs : null;
129
+ }
130
+ catch (error) {
131
+ console.warn('[openbot] could not locate attachment', name, error);
132
+ return null;
114
133
  }
134
+ };
135
+ try {
115
136
  const missingKey = missingByokApiKeyMessage(candidates[0], { authMode });
116
137
  if (missingKey)
117
138
  throw new Error(missingKey);
@@ -128,22 +149,14 @@ export async function* runOpenbotTurn(args, options) {
128
149
  const tools = toAiTools(toolDefinitions, args, flushPendingOutput);
129
150
  try {
130
151
  let messages = eventsToModelMessages(events);
131
- if (hasImages) {
132
- messages = await addImageParts(messages, events, async (name) => {
133
- try {
134
- const file = await storage.readThreadAttachment({
135
- channelId: state.channelId,
136
- threadId,
137
- name,
138
- });
139
- return { data: new Uint8Array(file.body), mediaType: file.mimeType };
140
- }
141
- catch (error) {
142
- console.warn('[openbot] could not read attachment', name, error);
143
- return null;
144
- }
152
+ if (hasAttachments) {
153
+ const includeImages = modelSupportsImages(candidate, { authMode });
154
+ messages = await addAttachmentParts(messages, events, {
155
+ includeImages,
156
+ read: readImage,
157
+ locate: locateAttachment,
145
158
  });
146
- console.info('[openbot] attached images for', candidate);
159
+ console.info('[openbot] attached files for', candidate, { images: includeImages });
147
160
  }
148
161
  const result = streamText({
149
162
  model: resolveModel(candidate, { authMode }),
@@ -50,6 +50,13 @@ export const OPENBOT_SYSTEM_PROMPT = [
50
50
  "- **Hub-and-spoke**: Specialists cannot talk to each other. You pass context from one to the next.",
51
51
  "- **Durable Memory**: Use the `remember` tool to store important facts, preferences, or project details that should persist across sessions.",
52
52
  "",
53
+ "# ATTACHMENTS",
54
+ "- Files the human attaches are saved on this computer. Their message lists each one with its full path.",
55
+ "- You see an image only when it is shown to you. You cannot open other files (PDF, documents, spreadsheets, archives, code).",
56
+ "- Never claim you read a file or saw an image you were not shown.",
57
+ "- When a coding agent should use a file, `ask_agent` them with the exact full path(s) and ask them to open the file there. Do not paraphrase or shorten the path.",
58
+ "- If an image was not shown to you and no specialist fits, say the current model cannot view images and suggest picking a vision-capable model.",
59
+ "",
53
60
  "# CLOUD COMPUTER",
54
61
  "This runtime is a remote cloud computer. The human is in the browser at openbot.one and cannot open localhost, 127.0.0.1, or any port on this machine.",
55
62
  "- Never tell them to visit localhost. They need a public HTTPS URL.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meetopenbot/openbot",
3
- "version": "1.2.14",
3
+ "version": "1.2.16",
4
4
  "description": "OpenBot coordinator runtime: ask specialists, route work into Spaces, and track todos.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,7 +25,7 @@
25
25
  "@ai-sdk/openai": "^4.0.72",
26
26
  "ai": "^7.0.111",
27
27
  "zod": "^4.3.5",
28
- "@meetopenbot/plugin-sdk": "^1.2.4"
28
+ "@meetopenbot/plugin-sdk": "^1.2.5"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^20.10.1",