From 95e607bea7548e0143830f2df1d7313bd6207512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 8 Oct 2026 05:20:03 -0400 Subject: [PATCH] Q24: knowledge enters the run: search and record tools, relevant entries in prompts Co-Authored-By: Claude Opus 5.5 --- src/main/services/agent-knowledge-tools.ts | 103 ++++++++++++++++++ src/main/services/agent-read-messages.ts | 4 +- src/main/services/agent-tool-args.ts | 16 +++ src/main/services/agent-tools.ts | 6 + src/main/services/relevance.ts | 34 ++++++ src/main/services/skill-relevance.ts | 25 ----- src/main/services/skill-service.ts | 2 +- src/main/services/task-execution-service.ts | 2 + src/main/services/task-prompts.ts | 4 +- tests/integration/task-context-scope.test.ts | 5 + tests/unit/agent-knowledge-tools.test.ts | 62 +++++++++++ ...ll-relevance.test.ts => relevance.test.ts} | 4 +- 12 files changed, 236 insertions(+), 31 deletions(-) create mode 100644 src/main/services/agent-knowledge-tools.ts create mode 100644 src/main/services/relevance.ts delete mode 100644 src/main/services/skill-relevance.ts create mode 100644 tests/unit/agent-knowledge-tools.test.ts rename tests/unit/{skill-relevance.test.ts => relevance.test.ts} (85%) diff --git a/src/main/services/agent-knowledge-tools.ts b/src/main/services/agent-knowledge-tools.ts new file mode 100644 index 00000000..f4c19517 --- /dev/null +++ b/src/main/services/agent-knowledge-tools.ts @@ -0,0 +1,103 @@ +import type { DatabaseSync } from "node:sqlite"; +import { optionalEnum, optionalLimit, requireString } from "./agent-tool-args"; +import type { AgentToolContext, AgentToolDeps } from "./agent-tools"; +import { KnowledgeService } from "./knowledge-service"; +import type { JsonRecord } from "./provider-http"; +import type { ToolDefinition } from "./provider-tool-turn"; +import { relevantItems } from "./relevance"; + +const PROMPT_ENTRY_LIMIT = 5; +const PROMPT_ENTRY_CHARS = 600; +const IMPORTANCE = ["low", "normal", "high"] as const; + +interface KnowledgeRow { + id: string; + topic: string; + category: string; + content: string; + importance: string; + updated_at: string; +} + +export const KNOWLEDGE_TOOLS: ToolDefinition[] = [ + { + name: "search_knowledge", + description: "Search the company knowledge base — decisions, preferences, verified facts, and lessons recorded by the team.", + parameters: { + type: "object", + additionalProperties: false, + properties: { + query: { type: "string", description: "Words to look for." }, + limit: { type: "number", description: "Most entries to return (default 10, max 30)." }, + }, + required: ["query"], + }, + }, + { + name: "record_knowledge", + description: "Record something the company should remember: a decision, a preference, a verified fact, or why something failed.", + parameters: { + type: "object", + additionalProperties: false, + properties: { + topic: { type: "string", description: "Short title." }, + content: { type: "string", description: "The knowledge itself, self-contained." }, + category: { type: "string", description: "e.g. decision, preference, fact, lesson." }, + importance: { type: "string", enum: ["low", "normal", "high"] }, + }, + required: ["topic", "content", "category"], + }, + }, +]; + +export function searchKnowledgeTool(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord): string { + const query = requireString(args.query, "query"); + const limit = optionalLimit(args.limit, "limit", 10, 30); + const found = relevantItems(knowledgeRows(deps.db, ctx.orgId), describeEntry, query, limit); + if (found.length === 0) return `No knowledge matches "${query}".`; + return found.map(formatEntry).join("\n\n"); +} + +export function recordKnowledgeTool(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord): string { + const importance = optionalEnum(args.importance, "importance", IMPORTANCE, "normal"); + const id = new KnowledgeService(deps.db).create({ + orgId: ctx.orgId, + topic: requireString(args.topic, "topic"), + category: requireString(args.category, "category"), + content: requireString(args.content, "content"), + importance, + sourceType: "agent", + sourceId: ctx.agent.id, + }); + return `Recorded knowledge entry ${id}.`; +} + +/** Prompt lines for the entries most relevant to a task. */ +export function relevantKnowledgeLines(db: DatabaseSync, orgId: string, taskText: string): string[] { + const found = relevantItems(knowledgeRows(db, orgId), describeEntry, taskText, PROMPT_ENTRY_LIMIT); + if (found.length === 0) return []; + return [ + "", + "Company knowledge that may apply (search_knowledge for more):", + ...found.map((entry) => `- ${entry.topic} [${entry.category}, ${entry.importance}, id=${entry.id}]: ${clip(entry.content)}`), + ]; +} + +function knowledgeRows(db: DatabaseSync, orgId: string): KnowledgeRow[] { + return db.prepare(` + select id, topic, category, content, importance, updated_at from knowledge_entries + where org_id = ? order by updated_at desc + `).all(orgId) as unknown as KnowledgeRow[]; +} + +function describeEntry(entry: KnowledgeRow): string { + return `${entry.topic} ${entry.category} ${entry.content}`; +} + +function formatEntry(entry: KnowledgeRow): string { + return `${entry.topic} [${entry.category}, ${entry.importance}, id=${entry.id}, updated ${entry.updated_at}]\n${entry.content}`; +} + +function clip(text: string): string { + return text.length > PROMPT_ENTRY_CHARS ? `${text.slice(0, PROMPT_ENTRY_CHARS)}…` : text; +} diff --git a/src/main/services/agent-read-messages.ts b/src/main/services/agent-read-messages.ts index 95f5af92..495be89a 100644 --- a/src/main/services/agent-read-messages.ts +++ b/src/main/services/agent-read-messages.ts @@ -1,5 +1,5 @@ import { attachmentNote } from "./agent-attachment-tools"; -import { optionalString } from "./agent-tool-args"; +import { optionalLimit, optionalString } from "./agent-tool-args"; import type { AgentToolContext, AgentToolDeps } from "./agent-tools"; import type { JsonRecord } from "./provider-http"; @@ -25,7 +25,7 @@ export function readMessagesTool(deps: AgentToolDeps, ctx: AgentToolContext, arg const filter: MessageFilter = { before: beforeCursor(deps, ctx.orgId, optionalString(args.before)), query: optionalString(args.query), - limit: Math.min(Math.max(Number(args.limit) || 10, 1), 30), + limit: optionalLimit(args.limit, "limit", 10, 30), }; if (withTeammate) { const counterpart = deps.db.prepare(` diff --git a/src/main/services/agent-tool-args.ts b/src/main/services/agent-tool-args.ts index 8d94ab4a..8fa20e0a 100644 --- a/src/main/services/agent-tool-args.ts +++ b/src/main/services/agent-tool-args.ts @@ -14,3 +14,19 @@ export function optionalStringArray(value: unknown, field: string): string[] { if (!Array.isArray(value)) throw new Error(`"${field}" must be an array.`); return value.map((item, index) => requireString(item, `${field}[${index}]`)); } + +/** Omitted means the default; anything supplied must be a whole number from 1 to max. */ +export function optionalLimit(value: unknown, field: string, fallback: number, max: number): number { + if (value === undefined) return fallback; + if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > max) { + throw new Error(`"${field}" must be a whole number from 1 to ${max}.`); + } + return value; +} + +/** Omitted means the default; anything supplied must be one of the allowed values. */ +export function optionalEnum(value: unknown, field: string, allowed: readonly string[], fallback: string): string { + if (value === undefined) return fallback; + if (typeof value !== "string" || !allowed.includes(value)) throw new Error(`"${field}" must be one of: ${allowed.join(", ")}.`); + return value; +} diff --git a/src/main/services/agent-tools.ts b/src/main/services/agent-tools.ts index 1d5c8d2c..128bfffb 100644 --- a/src/main/services/agent-tools.ts +++ b/src/main/services/agent-tools.ts @@ -11,6 +11,7 @@ import { bindSkillSlugs, grantSkillTool, listSkillsTool } from "./agent-skill-to import { createDocumentTool, writeDeliverableTool } from "./agent-deliverable-tools"; import { companyStatusTool, GOAL_TOOLS, updateGoalTool } from "./agent-goal-tools"; import { ATTACHMENT_TOOLS, readAttachmentTool } from "./agent-attachment-tools"; +import { KNOWLEDGE_TOOLS, recordKnowledgeTool, searchKnowledgeTool } from "./agent-knowledge-tools"; import { sendMessageTool } from "./agent-send-message"; import { withSavepoint } from "../database/connection"; import { optionalString, optionalStringArray, requireString } from "./agent-tool-args"; @@ -229,6 +230,7 @@ export const AGENT_TOOLS: ToolDefinition[] = [ }, ...GOAL_TOOLS, ...ATTACHMENT_TOOLS, + ...KNOWLEDGE_TOOLS, ]; export function dispatchAgentTool( @@ -259,6 +261,10 @@ export function dispatchAgentTool( return grantConnectorTool(deps, ctx, call.arguments, "grant"); case "revoke_connector": return grantConnectorTool(deps, ctx, call.arguments, "revoke"); + case "search_knowledge": + return searchKnowledgeTool(deps, ctx, call.arguments); + case "record_knowledge": + return recordKnowledgeTool(deps, ctx, call.arguments); case "read_attachment": return readAttachmentTool(deps, ctx, call.arguments); case "company_status": diff --git a/src/main/services/relevance.ts b/src/main/services/relevance.ts new file mode 100644 index 00000000..cb4a906d --- /dev/null +++ b/src/main/services/relevance.ts @@ -0,0 +1,34 @@ +const LATIN_WORD = /[a-z0-9]{3,}/g; +const CJK_RUN = /[\u3400-\u9fff]+/g; +const STOP_WORDS = new Set(["the", "and", "for", "with", "that", "this", "from", "are", "was", "were", "you", "your", "our", "not", "but", "all", "any", "can", "has", "have", "had", "into", "its", "use", "will", "who", "how", "what", "when", "where", "which", "why", "out", "about", "than", "then", "them", "they", "their", "there", "these", "those", "been", "being", "each", "more", "most", "other", "some", "such", "only", "own", "same", "very", "just", "also"]); + +/** Words (latin, 3+ chars) and character pairs (CJK) of a text. */ +function terms(text: string): Set { + const lower = text.toLowerCase(); + const found = new Set((lower.match(LATIN_WORD) ?? []).filter((word) => !STOP_WORDS.has(word))); + for (const run of lower.match(CJK_RUN) ?? []) { + for (let index = 0; index < run.length - 1; index += 1) found.add(run.slice(index, index + 2)); + } + return found; +} + +function scored(items: T[], describe: (item: T) => string, taskText: string) { + const taskTerms = terms(taskText); + return items + .map((item, index) => { + let score = 0; + for (const term of terms(describe(item))) if (taskTerms.has(term)) score += 1; + return { item, index, score }; + }) + .sort((a, b) => b.score - a.score || a.index - b.index); +} + +/** Orders items by how many terms they share with the task; ties keep their order. */ +export function orderByRelevance(items: T[], describe: (item: T) => string, taskText: string): T[] { + return scored(items, describe, taskText).map((entry) => entry.item); +} + +/** Only the items sharing at least one term with the text, best first. */ +export function relevantItems(items: T[], describe: (item: T) => string, text: string, limit: number): T[] { + return scored(items, describe, text).filter((entry) => entry.score > 0).slice(0, limit).map((entry) => entry.item); +} diff --git a/src/main/services/skill-relevance.ts b/src/main/services/skill-relevance.ts deleted file mode 100644 index e00d21a9..00000000 --- a/src/main/services/skill-relevance.ts +++ /dev/null @@ -1,25 +0,0 @@ -const LATIN_WORD = /[a-z0-9]{3,}/g; -const CJK_RUN = /[\u3400-\u9fff]+/g; - -/** Words (latin, 3+ chars) and character pairs (CJK) of a text. */ -function terms(text: string): Set { - const lower = text.toLowerCase(); - const found = new Set(lower.match(LATIN_WORD) ?? []); - for (const run of lower.match(CJK_RUN) ?? []) { - for (let index = 0; index < run.length - 1; index += 1) found.add(run.slice(index, index + 2)); - } - return found; -} - -/** Orders items by how many terms they share with the task; ties keep their order. */ -export function orderByRelevance(items: T[], describe: (item: T) => string, taskText: string): T[] { - const taskTerms = terms(taskText); - return items - .map((item, index) => { - let score = 0; - for (const term of terms(describe(item))) if (taskTerms.has(term)) score += 1; - return { item, index, score }; - }) - .sort((a, b) => b.score - a.score || a.index - b.index) - .map((entry) => entry.item); -} diff --git a/src/main/services/skill-service.ts b/src/main/services/skill-service.ts index bb5f563b..bd22d900 100644 --- a/src/main/services/skill-service.ts +++ b/src/main/services/skill-service.ts @@ -7,7 +7,7 @@ import { AuditService } from "./audit-service"; import { OperationsService } from "./operations-service"; import { copySkillRevision, recursiveSkillChecksum } from "./skill-artifacts"; import { loadSkillManifest, type SkillManifest } from "./skill-manifest"; -import { orderByRelevance } from "./skill-relevance"; +import { orderByRelevance } from "./relevance"; import { runManifestArtifactTest, verifyRevisionStructure, type SkillArtifactRow, type SkillTestRow } from "./skill-tests"; import { estimateTokens } from "./context-budget"; diff --git a/src/main/services/task-execution-service.ts b/src/main/services/task-execution-service.ts index 9f5e60b3..8efc2d6a 100644 --- a/src/main/services/task-execution-service.ts +++ b/src/main/services/task-execution-service.ts @@ -24,6 +24,7 @@ import { type WorkspaceRow, } from "./task-prompts"; import { renderTaskHandoff } from "./agent-task-handoff"; +import { relevantKnowledgeLines } from "./agent-knowledge-tools"; import { createRunContextRecorder, createRunMessagePoller, loadChatWakeContext } from "./chat-wake-context"; import { runningWorkspaceWriter } from "./workspace-write-lock"; import { inputBudgetTokens, skillBudgetTokens } from "./context-budget"; @@ -204,6 +205,7 @@ export class TaskExecutionService { managerName: this.agentName(orgId, agent.manager_agent_id), chatContext, handoffLines: renderTaskHandoff(this.db, orgId, task.handoff_json), + knowledgeLines: relevantKnowledgeLines(this.db, orgId, `${task.title}\n${task.description}`), priorCompletedRuns: this.completedRunCount(task.id, runId), }), toolContext, diff --git a/src/main/services/task-prompts.ts b/src/main/services/task-prompts.ts index 20875096..22e8a270 100644 --- a/src/main/services/task-prompts.ts +++ b/src/main/services/task-prompts.ts @@ -103,7 +103,7 @@ export function buildAgentInstructions(input: AgentInstructionContext): string { `You are ${agent.name}, ${agent.title || agent.role} in ${agent.department} at ${input.companyName}. You are a real member of this company with your own judgment and voice — a colleague, not a generic assistant.`, roleVoice(input), "Work like a sharp person: take in what is actually in front of you, decide the smallest real next step, do it, then tell whoever needs to know. Do not narrate busywork.", - `Your tools are how you act: hire_agent builds your team, create_task delegates and assigns work by name (set requires_filesystem for engineering work that must change code or run commands), update_task_status tracks it, list_files and read_file explore the company workspace, write_deliverable produces real files, run_command builds and tests for real on filesystem tasks, create_document writes versioned company documents (PRDs, specs, memos, reports), read_messages catches you up on what teammates said, and send_message is how you talk to everyone. web_search and fetch_url reach the live web — look up APIs, docs, and facts instead of guessing them.`, + `Your tools are how you act: hire_agent builds your team, create_task delegates and assigns work by name (set requires_filesystem for engineering work that must change code or run commands), update_task_status tracks it, list_files and read_file explore the company workspace, write_deliverable produces real files, run_command builds and tests for real on filesystem tasks, create_document writes versioned company documents (PRDs, specs, memos, reports), search_knowledge and record_knowledge read and grow what the company has learned, read_messages catches you up on what teammates said, and send_message is how you talk to everyone. web_search and fetch_url reach the live web — look up APIs, docs, and facts instead of guessing them.`, ...(input.language === "zh" ? ["公司的工作语言是简体中文:你写的每一条消息、任务标题与描述、文档和交付物都用中文表达(代码、命令、API 名称等技术标识保持原样)。工具名称和工具参数的字段名仍按定义使用英文。"] : []), @@ -171,6 +171,7 @@ export function buildTaskPrompt(input: { managerName: string | null; chatContext: ChatContextBlock | null; handoffLines: string[]; + knowledgeLines: string[]; /** Completed runs this task already spent without reaching done. */ priorCompletedRuns: number; }): string { @@ -203,6 +204,7 @@ export function buildTaskPrompt(input: { ); } lines.push(...input.handoffLines); + lines.push(...input.knowledgeLines); lines.push("", `Secret scope: ${input.isolation.secretScopeJson}`); if (input.skillRuntimeSnapshot) { lines.push( diff --git a/tests/integration/task-context-scope.test.ts b/tests/integration/task-context-scope.test.ts index 2c6e56bd..3afd1cfa 100644 --- a/tests/integration/task-context-scope.test.ts +++ b/tests/integration/task-context-scope.test.ts @@ -7,6 +7,7 @@ import { OnboardingService } from "../../src/main/services/onboarding-service"; import { ProviderService } from "../../src/main/services/provider-service"; import { providerAdapters } from "../../src/main/services/provider-adapters"; import { TaskExecutionService } from "../../src/main/services/task-execution-service"; +import { KnowledgeService } from "../../src/main/services/knowledge-service"; import { OrganizationService } from "../../src/main/services/organization-service"; import { claimNextQueuedRun } from "../../src/main/app/task-queue-worker"; import { canonicalMentionHandle } from "../../src/shared/mention-handles"; @@ -64,6 +65,8 @@ describe("task prompt scope", () => { await execution.executeRun(kickoff!.org_id, kickoff!.id); const task = connection.db.prepare("select id from tasks where org_id = ? and title = 'Build the booking API'").get(orgId) as { id: string }; + new KnowledgeService(connection.db).create({ orgId, topic: "Booking storage", category: "decision", content: "Bookings live in Postgres.", importance: "high", sourceType: "employee", sourceId: null }); + new KnowledgeService(connection.db).create({ orgId, topic: "Offsite venue", category: "fact", content: "Lisbon in May.", importance: "low", sourceType: "employee", sourceId: null }); await execution.runTask({ orgId, taskId: task.id }); const danaId = (connection.db.prepare("select id from agents where org_id = ? and name = 'Dana Lee'").get(orgId) as { id: string }).id; @@ -74,6 +77,8 @@ describe("task prompt scope", () => { expect(connection.db.prepare("select count(*) as n from task_runs where task_id = ? and state = 'running'").get(busyTaskId)).toEqual({ n: 1 }); expect(danaPrompts).toHaveLength(1); expect(danaPrompts[0]).toContain("Build the booking API"); + expect(danaPrompts[0]).toContain("- Booking storage [decision, high"); + expect(danaPrompts[0]).not.toContain("Offsite venue"); expect(danaPrompts[0]).not.toContain(CHANNEL_NOTE); connection.close(); }); diff --git a/tests/unit/agent-knowledge-tools.test.ts b/tests/unit/agent-knowledge-tools.test.ts new file mode 100644 index 00000000..52ef1b5b --- /dev/null +++ b/tests/unit/agent-knowledge-tools.test.ts @@ -0,0 +1,62 @@ +import { randomUUID } from "node:crypto"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, test } from "vitest"; +import { DatabaseConnection } from "../../src/main/database/connection"; +import { OrganizationService } from "../../src/main/services/organization-service"; +import { ChatService } from "../../src/main/services/chat-service"; +import { dispatchAgentTool, type AgentToolContext } from "../../src/main/services/agent-tools"; +import { relevantKnowledgeLines } from "../../src/main/services/agent-knowledge-tools"; + +function harness() { + const connection = new DatabaseConnection(join(tmpdir(), `ac-knowledge-${randomUUID()}.db`)); + const organization = new OrganizationService(connection.db); + let orgId = ""; + let agentId = ""; + connection.transaction(() => { + orgId = organization.createOrganization({ organizationName: "K", employeeName: "Y", mission: "m", goal: "g" }); + agentId = organization.createAgent({ orgId, name: "Ana", role: "Engineer", department: "Engineering", modelProviderId: null, modelId: null, budgetCents: 1000, managerAgentId: null }); + }); + const ctx: AgentToolContext = { + orgId, agent: { id: agentId, modelProviderId: null, modelId: null }, + taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null, + }; + const run = (name: string, args: Record) => + dispatchAgentTool({ db: connection.db, organization, chat: new ChatService(connection.db) }, ctx, { id: `c_${randomUUID()}`, name, arguments: args }); + return { connection, orgId, agentId, run }; +} + +describe("knowledge in the execution chain", () => { + test("agents record knowledge and find it again by search", () => { + const h = harness(); + expect(h.run("record_knowledge", { topic: "Booking storage", content: "The booking API stores orders in Postgres, not MySQL.", category: "decision", importance: "high" })) + .toMatch(/^Recorded knowledge entry kne_/); + h.run("record_knowledge", { topic: "Brand colors", content: "Use teal and sand.", category: "preference" }); + const row = h.connection.db.prepare("select source_type, source_id, importance from knowledge_entries where topic = 'Booking storage'").get(); + expect(row).toEqual({ source_type: "agent", source_id: h.agentId, importance: "high" }); + + const found = h.run("search_knowledge", { query: "which database for bookings postgres" }); + expect(found).toContain("Booking storage [decision, high"); + expect(found).not.toContain("Brand colors"); + expect(h.run("search_knowledge", { query: "kubernetes" })).toBe('No knowledge matches "kubernetes".'); + expect(h.run("record_knowledge", { topic: "x", content: "y", category: "z", importance: "urgent" })).toBe('ERROR: "importance" must be one of: low, normal, high.'); + expect(h.run("record_knowledge", { topic: "x", content: "y", category: "z", importance: "" })).toBe('ERROR: "importance" must be one of: low, normal, high.'); + expect(h.run("search_knowledge", { query: "postgres", limit: "nonsense" })).toBe('ERROR: "limit" must be a whole number from 1 to 30.'); + expect(h.run("search_knowledge", { query: "postgres", limit: 0 })).toBe('ERROR: "limit" must be a whole number from 1 to 30.'); + expect(h.run("read_messages", { channel: "general", limit: 99 })).toBe('ERROR: "limit" must be a whole number from 1 to 30.'); + expect(h.connection.db.prepare("select count(*) as n from knowledge_entries where topic = 'x'").get()).toEqual({ n: 0 }); + h.connection.close(); + }); + + test("only knowledge relevant to the task enters its prompt", () => { + const h = harness(); + h.run("record_knowledge", { topic: "Booking storage", content: "The booking API stores orders in Postgres.", category: "decision" }); + h.run("record_knowledge", { topic: "Brand colors", content: "Use teal and sand.", category: "preference" }); + const lines = relevantKnowledgeLines(h.connection.db, h.orgId, "Build the booking API").join("\n"); + expect(lines).toContain("Company knowledge that may apply (search_knowledge for more):"); + expect(lines).toContain("- Booking storage [decision, normal"); + expect(lines).not.toContain("Brand colors"); + expect(relevantKnowledgeLines(h.connection.db, h.orgId, "Plan the offsite")).toEqual([]); + h.connection.close(); + }); +}); diff --git a/tests/unit/skill-relevance.test.ts b/tests/unit/relevance.test.ts similarity index 85% rename from tests/unit/skill-relevance.test.ts rename to tests/unit/relevance.test.ts index 74893d3c..f6890cb3 100644 --- a/tests/unit/skill-relevance.test.ts +++ b/tests/unit/relevance.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "vitest"; -import { orderByRelevance } from "../../src/main/services/skill-relevance"; +import { orderByRelevance } from "../../src/main/services/relevance"; -describe("skill relevance", () => { +describe("relevance", () => { test("ranks by shared words, keeps order on ties, and matches Chinese by character pairs", () => { const skills = ["Growth Hacker: viral campaigns", "Backend Architect: scalable system design", "财务分析:预算与现金流"]; expect(orderByRelevance(skills, (item) => item, "Design a scalable backend system")).toEqual([skills[1], skills[0], skills[2]]);