diff --git a/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js b/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js index b5393fb465..7746c45e37 100644 --- a/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js +++ b/backend/__tests__/unit/config/nativeAgents.scoutKnowledge.test.js @@ -66,8 +66,25 @@ describe('Scout onboarding knowledge', () => { // Nothing on the board backs a Gemini promise. expect(prompt).not.toMatch(/coming/i); // The hosted agent's model is whatever the runtime defaults to, so the - // prompt names none. - expect(prompt).not.toMatch(/deepseek/i); + // prompt names none; the hosted offer's description carries it. + expect(prompt).not.toMatch(/v4[ -]?flash/i); + }); + + // 2026-10-19: a new person asked to connect DeepSeek Harness, heard that + // DeepSeek models could not be used here, and left within two minutes. + test('says yes to DeepSeek models and names both paths, without promising dsh', () => { + expect(prompt).toMatch(/DeepSeek models: yes, two ways\. "Run it here"/); + expect(prompt).toMatch(/their own OpenCode pointed at DeepSeek's API, through the OpenCode guide/); + expect(prompt).toMatch(/DeepSeek Harness \(dsh\), cannot connect yet; do not say when or whether it will/); + expect(prompt).toMatch(/lead with the yes: its models can work here today/); + expect(prompt).toMatch(/name it only when someone asks which model or runtime it uses/); + }); + + test('connects OpenCode through its guide, never through the connect card', () => { + expect(prompt).toMatch(/OpenCode, on a model provider they choose .* connects from the command line, not from this page/); + expect(prompt).toMatch(/do not propose a connect card for it: the card sets up Claude Code or Codex/); + expect(prompt).toMatch(/Qwen Code and pi are not ready for outside use/); + expect(prompt).toMatch(/Do not claim any other tool connects/); }); // Linux run 38982219348: a Claude Code listener cannot run on Linux, WSL or @@ -92,7 +109,7 @@ describe('Scout onboarding knowledge', () => { test('cites developer docs that exist in the repository', () => { const repo = path.resolve(__dirname, '../../../..'); - for (const doc of ['docs/agents/BUILDING_AN_AGENT.md', 'docs/api/openapi.yaml']) { + for (const doc of ['docs/agents/BUILDING_AN_AGENT.md', 'docs/api/openapi.yaml', 'docs/cli/opencode.md']) { expect(prompt).toContain(doc); expect(fs.existsSync(path.join(repo, doc))).toBe(true); } @@ -115,3 +132,36 @@ describe('Scout onboarding knowledge', () => { expect(prompt).toMatch(/with no invitation code/); }); }); + +// The hosted model reaches Scout through the offer's description, built from +// the same default a hosted turn runs on, so it follows the deployment the way +// the limits line does (#2237) instead of drifting in a prompt. +describe('the hosted offer names the model a hosted turn runs on', () => { + const offerDescription = (model) => { + const saved = process.env.LITELLM_CHAT_MODEL; + if (model === undefined) delete process.env.LITELLM_CHAT_MODEL; + else process.env.LITELLM_CHAT_MODEL = model; + let tools; + try { + jest.isolateModules(() => { + ({ TOOLS: tools } = require('../../../services/nativeRuntimeService')); + }); + } finally { + if (saved === undefined) delete process.env.LITELLM_CHAT_MODEL; + else process.env.LITELLM_CHAT_MODEL = saved; + } + return tools.find((tool) => tool.function.name === 'commonly_offer_hosted_agent').function.description; + }; + + test('names DeepSeek V4 Flash for the default model, and says it cannot be changed', () => { + const description = offerDescription(undefined); + expect(description).toMatch(/A hosted agent runs on DeepSeek V4 Flash, and the person cannot choose another model today/); + expect(description).toMatch(/name the model only when they ask which model or runtime it uses/); + }); + + test('follows LITELLM_CHAT_MODEL when the deployment changes it', () => { + const description = offerDescription('some-other-model'); + expect(description).toMatch(/A hosted agent runs on some-other-model,/); + expect(description).not.toMatch(/DeepSeek/); + }); +}); diff --git a/backend/__tests__/unit/scripts/docsSiteFacts.test.js b/backend/__tests__/unit/scripts/docsSiteFacts.test.js index 2d9196146a..6bad439e41 100644 --- a/backend/__tests__/unit/scripts/docsSiteFacts.test.js +++ b/backend/__tests__/unit/scripts/docsSiteFacts.test.js @@ -75,8 +75,9 @@ describe('docs.commonly.me says what the product does', () => { }); test('the listener claims only the adapters Scout is allowed to name', () => { - // Scout's verified facts: the listener runs Claude Code or Codex. Other - // adapters exist in the CLI but are not verified for public seats yet. + // Scout's verified facts: the connect page's listener runs Claude Code or + // Codex. OpenCode connects through its own guide (docs/cli/opencode.md), + // and the other CLI adapters are not ready for outside use. [quickstart, connect].forEach((page) => { expect(page).toMatch(/Claude Code if it\s+is\s+installed,\s+otherwise\s+Codex/); expect(page).not.toMatch(/--adapter (qwen|opencode|pi)\b/); diff --git a/backend/config/native-agents/scout.ts b/backend/config/native-agents/scout.ts index 0b9f3ae6d9..908ba3b322 100644 --- a/backend/config/native-agents/scout.ts +++ b/backend/config/native-agents/scout.ts @@ -86,7 +86,9 @@ export const scoutApp = { + 'seconds after the click. When someone wants it, answer with ' + 'commonly_offer_hosted_agent: your reply arrives with a button that opens ' + 'this choice already made. Its limits are in that tool\'s description; ' - + 'quote them from there, never from memory.\n' + + 'quote them from there, never from memory. The model it runs on is named ' + + 'there too: name it only when someone asks which model or runtime it uses, ' + + 'or asks for that model, and never lead with it.\n' + ' * "Use my own agent": their own Claude Code or Codex, on their own ' + 'computer. The page\'s "Get my connect command" button shows one command ' + 'to run in Terminal, with what it needs written above it:\n' @@ -106,6 +108,16 @@ export const scoutApp = { + 'sessions. MCP alone does NOT answer @mentions; only the listener does. ' + 'Say exactly this when someone set up MCP and asks why their agent is ' + 'silent.\n' + // Walked 2026-10-19 (CLI 0.1.108, OpenCode 1.19.3, macOS, a non-admin + // account): an attached OpenCode agent on a DeepSeek model answered a + // mention through the Commonly tool. The page offers no OpenCode path, so + // the guide is the only route. + + ' * OpenCode, on a model provider they choose (for example DeepSeek, with ' + + 'their own API key), connects from the command line, not from this page. ' + + 'The guide is docs/cli/opencode.md in the github.com/Team-Commonly/commonly ' + + 'repository; it has been walked on a Mac only. Point them to the guide and ' + + 'do not propose a connect card for it: the card sets up Claude Code or ' + + 'Codex.\n' + ' * "Add a computer": `npm i -g @commonlyai/cli@latest && commonly login ' + '&& commonly daemon register && commonly daemon install` registers their ' + 'machine once; after that they add agents to it from this page with no ' @@ -133,8 +145,19 @@ export const scoutApp = { + 'that Commonly runs, on a model Commonly picks. Gemini can\'t join yet; do ' + 'not say when or whether it will. So when someone asks for Claude and GPT ' + 'together, the answer is yes: Claude Code plus Codex.\n' - + '- The listener runs Claude Code or Codex. Do not claim it runs any other ' - + 'tool.\n' + // 2026-10-19: a new person asked to connect DeepSeek Harness and heard + // that DeepSeek models could not be used here; hosted agents already ran + // on DeepSeek, and OpenCode on DeepSeek had just been walked. + + '- DeepSeek models: yes, two ways. "Run it here" (the model is named in ' + + 'commonly_offer_hosted_agent\'s description), or their own OpenCode pointed ' + + 'at DeepSeek\'s API, through the OpenCode guide. DeepSeek\'s own command-line ' + + 'tool, DeepSeek Harness (dsh), cannot connect yet; do not say when or ' + + 'whether it will. When someone asks for it, lead with the yes: its models ' + + 'can work here today.\n' + + '- What connects as their own agent: Claude Code or Codex from the Bring ' + + 'your own page, and OpenCode through its guide. Qwen Code and pi are not ' + + 'ready for outside use; say so if asked. Do not claim any other tool ' + + 'connects.\n' + '- Developers building their own agent: Commonly agents use a small HTTP ' + 'protocol (read context, post messages, memory, events). In the ' + 'github.com/Team-Commonly/commonly repository, the guide is ' diff --git a/backend/services/nativeRuntimeService.ts b/backend/services/nativeRuntimeService.ts index 3065597f40..7872ed0dbf 100644 --- a/backend/services/nativeRuntimeService.ts +++ b/backend/services/nativeRuntimeService.ts @@ -145,6 +145,11 @@ export const HOSTED_LIMITS_LINE = `${HOSTED_FREE_CAPS.agentsPerUser} hosted ` + `agent${HOSTED_FREE_CAPS.agentsPerUser === 1 ? '' : 's'} per account and ` + `${HOSTED_FREE_CAPS.turnsPerDay} turns a day (${HOSTED_PRO_CAPS.turnsPerDay} on Pro)`; export const HOSTED_OFFER_LABEL = 'Run it here'; +// The model a hosted turn runs on. hostedNativeRunConfig leaves `model` unset, +// so it is DEFAULT_MODEL. Named here for the same reason as the limits: Scout +// answers "can I use DeepSeek?" from the deployment, not from its own copy. +const MODEL_DISPLAY_NAMES: Record = { 'deepseek-v4-flash': 'DeepSeek V4 Flash' }; +export const HOSTED_MODEL_NAME = MODEL_DISPLAY_NAMES[DEFAULT_MODEL] || DEFAULT_MODEL; // --- tool schema (OpenAI function-calling format) ------------------------- // @@ -319,7 +324,9 @@ export const TOOLS = [ + `"${HOSTED_OFFER_LABEL}" button that opens Bring your own with hosting already chosen; ` + 'one more click there and their agent is live in a few seconds. Open to every ' + 'signed-in account during beta, no invitation code needed. The current limits are ' - + `${HOSTED_LIMITS_LINE}; quote these, never a number from memory. The content is your ` + + `${HOSTED_LIMITS_LINE}; quote these, never a number from memory. A hosted agent runs ` + + `on ${HOSTED_MODEL_NAME}, and the person cannot choose another model today; name the ` + + 'model only when they ask which model or runtime it uses. The content is your ' + 'whole reply: do not post another message about it. If they already have a hosted ' + 'agent, or this instance cannot host, nothing is posted and the result says why; ' + 'tell them in your own words.',