diff --git a/src/agents/cli-runner.ts b/src/agents/cli-runner.ts index 30a456606..0f1b27e86 100644 --- a/src/agents/cli-runner.ts +++ b/src/agents/cli-runner.ts @@ -102,6 +102,7 @@ export async function runCliAgent(params: { tools: [], contextFiles, modelDisplay, + agentId: sessionAgentId, }); const { sessionId: cliSessionIdToSend, isNew } = resolveSessionIdToSend({ diff --git a/src/agents/cli-runner/helpers.ts b/src/agents/cli-runner/helpers.ts index e20602e0f..e079044b3 100644 --- a/src/agents/cli-runner/helpers.ts +++ b/src/agents/cli-runner/helpers.ts @@ -9,8 +9,8 @@ import type { ThinkLevel } from "../../auto-reply/thinking.js"; import type { ClawdbotConfig } from "../../config/config.js"; import type { CliBackendConfig } from "../../config/types.js"; import { runExec } from "../../process/exec.js"; -import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../date-time.js"; import type { EmbeddedContextFile } from "../pi-embedded-helpers.js"; +import { buildSystemPromptParams } from "../system-prompt-params.js"; import { buildAgentSystemPrompt } from "../system-prompt.js"; const CLI_RUN_QUEUE = new Map>(); @@ -172,10 +172,19 @@ export function buildSystemPrompt(params: { tools: AgentTool[]; contextFiles?: EmbeddedContextFile[]; modelDisplay: string; + agentId?: string; }) { - const userTimezone = resolveUserTimezone(params.config?.agents?.defaults?.userTimezone); - const userTimeFormat = resolveUserTimeFormat(params.config?.agents?.defaults?.timeFormat); - const userTime = formatUserTime(new Date(), userTimezone, userTimeFormat); + const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({ + config: params.config, + agentId: params.agentId, + runtime: { + host: "clawdbot", + os: `${os.type()} ${os.release()}`, + arch: os.arch(), + node: process.version, + model: params.modelDisplay, + }, + }); return buildAgentSystemPrompt({ workspaceDir: params.workspaceDir, defaultThinkLevel: params.defaultThinkLevel, @@ -184,13 +193,7 @@ export function buildSystemPrompt(params: { reasoningTagHint: false, heartbeatPrompt: params.heartbeatPrompt, docsPath: params.docsPath, - runtimeInfo: { - host: "clawdbot", - os: `${os.type()} ${os.release()}`, - arch: os.arch(), - node: process.version, - model: params.modelDisplay, - }, + runtimeInfo, toolNames: params.tools.map((tool) => tool.name), modelAliasLines: buildModelAliasLines(params.config), userTimezone, diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index e89cd32b4..06272d7ce 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -64,7 +64,7 @@ import { prewarmSessionFile, trackSessionManagerAccess } from "../session-manage import { prepareSessionManagerForRun } from "../session-manager-init.js"; import { buildEmbeddedSystemPrompt, createSystemPromptOverride } from "../system-prompt.js"; import { splitSdkTools } from "../tool-split.js"; -import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../../date-time.js"; +import { buildSystemPromptParams } from "../../system-prompt-params.js"; import { describeUnknownError, mapThinkingLevel } from "../utils.js"; import { resolveSandboxRuntimeStatus } from "../../sandbox/runtime-status.js"; import { isTimeoutError } from "../../failover-error.js"; @@ -196,25 +196,25 @@ export async function runEmbeddedAttempt( return level ? { level, channel: "Telegram" } : undefined; })() : undefined; - const runtimeInfo = { - host: machineName, - os: `${os.type()} ${os.release()}`, - arch: os.arch(), - node: process.version, - model: `${params.provider}/${params.modelId}`, - channel: runtimeChannel, - capabilities: runtimeCapabilities, - }; - - const sandboxInfo = buildEmbeddedSandboxInfo(sandbox, params.bashElevated); - const reasoningTagHint = isReasoningTagProvider(params.provider); - const userTimezone = resolveUserTimezone(params.config?.agents?.defaults?.userTimezone); - const userTimeFormat = resolveUserTimeFormat(params.config?.agents?.defaults?.timeFormat); - const userTime = formatUserTime(new Date(), userTimezone, userTimeFormat); const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({ sessionKey: params.sessionKey, config: params.config, }); + const sandboxInfo = buildEmbeddedSandboxInfo(sandbox, params.bashElevated); + const reasoningTagHint = isReasoningTagProvider(params.provider); + const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({ + config: params.config, + agentId: sessionAgentId, + runtime: { + host: machineName, + os: `${os.type()} ${os.release()}`, + arch: os.arch(), + node: process.version, + model: `${params.provider}/${params.modelId}`, + channel: runtimeChannel, + capabilities: runtimeCapabilities, + }, + }); const isDefaultAgent = sessionAgentId === defaultAgentId; const promptMode = isSubagentSessionKey(params.sessionKey) ? "minimal" : "full"; const docsPath = await resolveClawdbotDocsPath({ diff --git a/src/agents/pi-embedded-runner/system-prompt.ts b/src/agents/pi-embedded-runner/system-prompt.ts index 3cbbed6db..40f3e7a4b 100644 --- a/src/agents/pi-embedded-runner/system-prompt.ts +++ b/src/agents/pi-embedded-runner/system-prompt.ts @@ -23,6 +23,7 @@ export function buildEmbeddedSystemPrompt(params: { /** Controls which hardcoded sections to include. Defaults to "full". */ promptMode?: PromptMode; runtimeInfo: { + agentId?: string; host: string; os: string; arch: string; diff --git a/src/agents/system-prompt-params.ts b/src/agents/system-prompt-params.ts new file mode 100644 index 000000000..7a076a139 --- /dev/null +++ b/src/agents/system-prompt-params.ts @@ -0,0 +1,44 @@ +import type { ClawdbotConfig } from "../config/config.js"; +import { + formatUserTime, + resolveUserTimeFormat, + resolveUserTimezone, + type ResolvedTimeFormat, +} from "./date-time.js"; + +export type RuntimeInfoInput = { + agentId?: string; + host: string; + os: string; + arch: string; + node: string; + model: string; + channel?: string; + capabilities?: string[]; +}; + +export type SystemPromptRuntimeParams = { + runtimeInfo: RuntimeInfoInput; + userTimezone: string; + userTime?: string; + userTimeFormat?: ResolvedTimeFormat; +}; + +export function buildSystemPromptParams(params: { + config?: ClawdbotConfig; + agentId?: string; + runtime: Omit; +}): SystemPromptRuntimeParams { + const userTimezone = resolveUserTimezone(params.config?.agents?.defaults?.userTimezone); + const userTimeFormat = resolveUserTimeFormat(params.config?.agents?.defaults?.timeFormat); + const userTime = formatUserTime(new Date(), userTimezone, userTimeFormat); + return { + runtimeInfo: { + agentId: params.agentId, + ...params.runtime, + }, + userTimezone, + userTime, + userTimeFormat, + }; +} diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index 06570f6ff..17303d0e9 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { buildAgentSystemPrompt } from "./system-prompt.js"; +import { buildAgentSystemPrompt, buildRuntimeLine } from "./system-prompt.js"; describe("buildAgentSystemPrompt", () => { it("includes owner numbers when provided", () => { @@ -252,6 +252,22 @@ describe("buildAgentSystemPrompt", () => { expect(prompt).toContain("capabilities=inlineButtons"); }); + it("includes agent id in runtime when provided", () => { + const prompt = buildAgentSystemPrompt({ + workspaceDir: "/tmp/clawd", + runtimeInfo: { + agentId: "work", + host: "host", + os: "macOS", + arch: "arm64", + node: "v20", + model: "anthropic/claude", + }, + }); + + expect(prompt).toContain("agent=work"); + }); + it("includes reasoning visibility hint", () => { const prompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/clawd", @@ -263,6 +279,31 @@ describe("buildAgentSystemPrompt", () => { expect(prompt).toContain("/status shows Reasoning"); }); + it("builds runtime line with agent and channel details", () => { + const line = buildRuntimeLine( + { + agentId: "work", + host: "host", + os: "macOS", + arch: "arm64", + node: "v20", + model: "anthropic/claude", + }, + "telegram", + ["inlineButtons"], + "low", + ); + + expect(line).toContain("agent=work"); + expect(line).toContain("host=host"); + expect(line).toContain("os=macOS (arm64)"); + expect(line).toContain("node=v20"); + expect(line).toContain("model=anthropic/claude"); + expect(line).toContain("channel=telegram"); + expect(line).toContain("capabilities=inlineButtons"); + expect(line).toContain("thinking=low"); + }); + it("describes sandboxed runtime and elevated when allowed", () => { const prompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/clawd", diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 19ba1bfc6..40ecd8607 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -149,6 +149,7 @@ export function buildAgentSystemPrompt(params: { /** Controls which hardcoded sections to include. Defaults to "full". */ promptMode?: PromptMode; runtimeInfo?: { + agentId?: string; host?: string; os?: string; arch?: string; @@ -546,27 +547,40 @@ export function buildAgentSystemPrompt(params: { ); } - lines.push( - "## Runtime", - `Runtime: ${[ - runtimeInfo?.host ? `host=${runtimeInfo.host}` : "", - runtimeInfo?.os - ? `os=${runtimeInfo.os}${runtimeInfo?.arch ? ` (${runtimeInfo.arch})` : ""}` - : runtimeInfo?.arch - ? `arch=${runtimeInfo.arch}` - : "", - runtimeInfo?.node ? `node=${runtimeInfo.node}` : "", - runtimeInfo?.model ? `model=${runtimeInfo.model}` : "", - runtimeChannel ? `channel=${runtimeChannel}` : "", - runtimeChannel - ? `capabilities=${runtimeCapabilities.length > 0 ? runtimeCapabilities.join(",") : "none"}` - : "", - `thinking=${params.defaultThinkLevel ?? "off"}`, - ] - .filter(Boolean) - .join(" | ")}`, - `Reasoning: ${reasoningLevel} (hidden unless on/stream). Toggle /reasoning; /status shows Reasoning when enabled.`, - ); + lines.push("## Runtime", buildRuntimeLine(runtimeInfo, runtimeChannel, runtimeCapabilities, params.defaultThinkLevel), `Reasoning: ${reasoningLevel} (hidden unless on/stream). Toggle /reasoning; /status shows Reasoning when enabled.`); return lines.filter(Boolean).join("\n"); } + +export function buildRuntimeLine( + runtimeInfo: { + agentId?: string; + host?: string; + os?: string; + arch?: string; + node?: string; + model?: string; + }, + runtimeChannel?: string, + runtimeCapabilities: string[] = [], + defaultThinkLevel?: ThinkLevel, +): string { + return `Runtime: ${[ + runtimeInfo?.agentId ? `agent=${runtimeInfo.agentId}` : "", + runtimeInfo?.host ? `host=${runtimeInfo.host}` : "", + runtimeInfo?.os + ? `os=${runtimeInfo.os}${runtimeInfo?.arch ? ` (${runtimeInfo.arch})` : ""}` + : runtimeInfo?.arch + ? `arch=${runtimeInfo.arch}` + : "", + runtimeInfo?.node ? `node=${runtimeInfo.node}` : "", + runtimeInfo?.model ? `model=${runtimeInfo.model}` : "", + runtimeChannel ? `channel=${runtimeChannel}` : "", + runtimeChannel + ? `capabilities=${runtimeCapabilities.length > 0 ? runtimeCapabilities.join(",") : "none"}` + : "", + `thinking=${defaultThinkLevel ?? "off"}`, + ] + .filter(Boolean) + .join(" | ")}`; +}