refactor(x): decouple the new runtime from the legacy engine file

Findings from a fresh post-refactor review. The turn-runtime bridges
imported convertFromMessages and getToolPermissionMetadata from
agents/runtime.ts — the legacy engine file — making it undeletable;
both move to neutral modules (agents/message-encoding.ts,
agents/permission-metadata.ts) that both engines now share. The
abort registry, used by the live tool registry, moves out of the
legacy runs/ cluster into turns/. Dead surface goes: the zero-consumer
CopilotInstructions, skillCatalog, RunLogger, and MappedToolCall
exports are deleted, mapAgentTool/StreamStepMessageBuilder become
module-private, and the mid-file import scar from the composer
extraction is hoisted.

Three review fixes ride along: ITurnEventBus gains
subscribe/subscribeAll so consumers stop resolving the concrete hub;
skill carry-forward becomes its own registry trait instead of
overloading workspaceContext; and connection checks (slack/composio/
code-mode/google) collapse into one shared connections.ts consumed by
both skill availability and the copilot prompt blocks — plus stale
doc pointers (VIDEO_MODE.md to moved prompt text, CLAUDE.md's dangling
AGENTS.md reference).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ramnique Singh 2026-07-10 10:37:15 +05:30
parent 6375704d66
commit fdce639839
26 changed files with 449 additions and 445 deletions

View file

@ -111,7 +111,7 @@ Long-form docs for specific features. Read the relevant file before making chang
| Live Notes — single `live:` frontmatter block (one objective + optional cron / windows / eventMatchCriteria) that turns a note into a self-updating artifact, panel UI, Copilot skill, prompts catalog | `apps/x/LIVE_NOTE.md` |
| Calls (video mode) — one hands-free call engine with four presets (voice / video / share screen / practice coaching), device-derived surfaces (full-screen ⇄ floating popout), frame pipeline, prompts catalog | `apps/x/VIDEO_MODE.md` |
| Analytics — PostHog event catalog, person properties, use-case taxonomy, how to add a new event | `apps/x/ANALYTICS.md` |
| Turn/session runtime — event-sourced storage, reference model, the `npm run inspect` debugger | `AGENTS.md` (repo root), `apps/x/packages/core/docs/turn-runtime-design.md`, `session-design.md` |
| Turn/session runtime — event-sourced storage, reference model, the `npm run inspect` debugger | `apps/x/packages/core/docs/turn-runtime-design.md`, `session-design.md` |
## Common Tasks

View file

@ -108,7 +108,7 @@ is live) to the outgoing message as `UserImagePart`s and sets
(`data`, `mediaType`), `source: 'camera' | 'screen'`, `capturedAt`. Unlike
file attachments (path references read via the `LLMParse` tool), image
parts go to the model as real multimodal image parts.
- `packages/core/src/agents/runtime.ts` `convertFromMessages` (~line 1013):
- `packages/core/src/agents/message-encoding.ts` `convertFromMessages`:
emits a context line (frame counts + time span), then labeled groups —
a `"Webcam frames (oldest to newest):"` text part before camera images and
a `"Screen-share frames (oldest to newest):"` text part before screen
@ -176,8 +176,8 @@ Push-to-talk is disabled while a call owns the mic.
| Prompt | Where |
|--------|-------|
| `# Video Mode (Live Camera)` system section — how to use webcam frames, coaching guidance, screen-share rules ("treat the screen as the primary subject", "last screen frame is current"), etiquette (never comment on appearance) | `packages/core/src/agents/runtime.ts:386` (`composeSystemInstructions`, gated on `videoMode`) |
| `# Practice Session (Coach Mode)` system section — coaching persona: specific/actionable feedback after each take, one-sentence interjections mid-flow, structured debrief on wrap-up | `composeSystemInstructions`, gated on `coachMode` (directly after the video section) |
| `# Video Mode (Live Camera)` system section — how to use webcam frames, coaching guidance, screen-share rules ("treat the screen as the primary subject", "last screen frame is current"), etiquette (never comment on appearance) | `packages/core/src/application/assistant/capabilities/modes.ts` (the `VIDEO_MODE` fragment of the `video-mode` capability, composed by `agents/compose-instructions.ts`) |
| `# Practice Session (Coach Mode)` system section — coaching persona: specific/actionable feedback after each take, one-sentence interjections mid-flow, structured debrief on wrap-up | `capabilities/modes.ts` (the `COACH_MODE` fragment, directly after the video capability) |
| "Driving the app" paragraph in the video-mode section — on calls, prefer app-navigation read-view/open-item (show while telling) over describing or squinting at frames | same `# Video Mode` section; full action docs in the `app-navigation` skill (`application/assistant/skills/app-navigation/skill.ts`) |
| Per-message frame context line `[Video mode: N live webcam frames … and M frames of the user's shared screen …]` + group labels | `packages/core/src/agents/runtime.ts` (`convertFromMessages`) |
| `videoMode` / `coachMode` composition overrides (session-sticky; flips bust prefix cache) | `packages/core/src/turns/bridges/real-agent-resolver.ts` (`CompositionOverrides`); set from `App.tsx` `sendConfig` |

View file

@ -31,7 +31,7 @@ import { ServiceEvent } from '@x/shared/dist/service-events.js';
import type { SessionBusEvent } from '@x/shared/dist/sessions.js';
import { isDurableTurnEvent } from '@x/shared/dist/turns.js';
import type { ISessions, EmitterSessionBus } from '@x/core/dist/sessions/index.js';
import type { TurnEventHub } from '@x/core/dist/turns/event-hub.js';
import type { ITurnEventBus } from '@x/core/dist/turns/event-hub.js';
import container from '@x/core/dist/di/container.js';
import { listOnboardingModels } from '@x/core/dist/models/models-dev.js';
import { testModelConnection, listModelsForProvider, generateOneShot } from '@x/core/dist/models/models.js';
@ -728,7 +728,7 @@ export function startTurnEventsWatcher(): void {
if (turnEventsWatcher) {
return;
}
const hub = container.resolve<TurnEventHub>('turnEventBus');
const hub = container.resolve<ITurnEventBus>('turnEventBus');
turnEventsWatcher = hub.subscribeAll((event) => {
if (isDurableTurnEvent(event.event)) {
broadcastToWindows('turns:events', event);

View file

@ -0,0 +1,187 @@
// Structural→wire message encoding: converts stored conversation messages
// into AI SDK ModelMessages (user-context weaving, attachment rendering,
// tool-result enveloping). Deterministic per message, so composed requests
// stay byte-stable. Shared by both engines — extracted from the legacy
// engine file so the turn-runtime bridges no longer depend on it.
import { ModelMessage } from "ai";
import { Message, UserMessageContext } from "@x/shared/dist/message.js";
import { z } from "zod";
function formatUserMessageContextForLlm(userMessageContext: z.infer<typeof UserMessageContext>): string {
const sections: string[] = [];
if (userMessageContext.currentDateTime) {
sections.push(`Current date and time: ${userMessageContext.currentDateTime}`);
}
if (userMessageContext.middlePane) {
if (userMessageContext.middlePane.kind === 'empty') {
sections.push(`Middle pane:\nState: empty`);
} else if (userMessageContext.middlePane.kind === 'note') {
sections.push(`Middle pane:\nState: note\nPath: ${userMessageContext.middlePane.path}\n\nContent:\n\`\`\`\n${userMessageContext.middlePane.content}\n\`\`\``);
} else {
sections.push(`Middle pane:\nState: browser\nURL: ${userMessageContext.middlePane.url}\nTitle: ${userMessageContext.middlePane.title}`);
}
}
if (sections.length === 0) {
return '';
}
return `# User Context
${sections.join('\n\n')}
# User Message
`;
}
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
export function convertFromMessages(messages: z.infer<typeof Message>[]): ModelMessage[] {
const result: ModelMessage[] = [];
for (const msg of messages) {
const { providerOptions } = msg;
switch (msg.role) {
case "assistant":
if (typeof msg.content === 'string') {
result.push({
role: "assistant",
content: msg.content,
providerOptions,
});
} else {
result.push({
role: "assistant",
content: msg.content.map(part => {
switch (part.type) {
case 'text':
return part;
case 'reasoning':
return part;
case 'tool-call':
return {
type: 'tool-call',
toolCallId: part.toolCallId,
toolName: part.toolName,
input: part.arguments,
providerOptions: part.providerOptions,
};
}
}),
providerOptions,
});
}
break;
case "system":
result.push({
role: "system",
content: msg.content,
providerOptions,
});
break;
case "user": {
const userMessageContextPrefix = msg.userMessageContext ? formatUserMessageContextForLlm(msg.userMessageContext) : '';
if (typeof msg.content === 'string') {
// Legacy string — pass through unchanged
result.push({
role: "user",
content: `${userMessageContextPrefix}${msg.content}`,
providerOptions,
});
} else {
// New content parts array — collapse text/attachments to text
// for the LLM; inline image parts (video-mode webcam and
// screen-share frames) are passed through as real multimodal
// image parts, grouped under labeled text headers so the
// model knows which images show the user vs their screen.
const textSegments: string[] = userMessageContextPrefix ? [userMessageContextPrefix] : [];
const attachmentLines: string[] = [];
type EncodedImagePart = { type: "image"; image: string; mediaType: string };
const cameraParts: EncodedImagePart[] = [];
const screenParts: EncodedImagePart[] = [];
const frameTimes: string[] = [];
for (const part of msg.content) {
if (part.type === "attachment") {
const sizeStr = part.size ? `, ${formatBytes(part.size)}` : '';
const lineStr = part.lineNumber ? ` (line ${part.lineNumber})` : '';
attachmentLines.push(`- ${part.filename} (${part.mimeType}${sizeStr}) at ${part.path}${lineStr}`);
} else if (part.type === "image") {
const target = part.source === "screen" ? screenParts : cameraParts;
target.push({ type: "image", image: part.data, mediaType: part.mediaType });
if (part.capturedAt) frameTimes.push(part.capturedAt);
} else {
textSegments.push(part.text);
}
}
if (attachmentLines.length > 0) {
if (userMessageContextPrefix) {
textSegments.push("User has attached the following files:", ...attachmentLines, "");
} else {
textSegments.unshift("User has attached the following files:", ...attachmentLines, "");
}
}
const imageCount = cameraParts.length + screenParts.length;
if (imageCount > 0) {
const span = frameTimes.length >= 2
? ` spanning ${frameTimes[0]} to ${frameTimes[frameTimes.length - 1]}`
: frameTimes.length === 1
? ` captured at ${frameTimes[0]}`
: '';
const kinds: string[] = [];
if (cameraParts.length > 0) kinds.push(`${cameraParts.length} live webcam frame${cameraParts.length === 1 ? '' : 's'} of the user`);
if (screenParts.length > 0) kinds.push(`${screenParts.length} frame${screenParts.length === 1 ? '' : 's'} of the user's shared screen`);
textSegments.push(`[Video mode: ${kinds.join(' and ')} attached below, each group oldest to newest,${span ? span + ',' : ''} recorded while they composed this message.]`);
const content: Array<{ type: "text"; text: string } | EncodedImagePart> = [
{ type: "text", text: textSegments.join("\n") },
];
if (cameraParts.length > 0) {
content.push({ type: "text", text: "Webcam frames (oldest to newest):" }, ...cameraParts);
}
if (screenParts.length > 0) {
content.push({ type: "text", text: "Screen-share frames (oldest to newest):" }, ...screenParts);
}
result.push({
role: "user",
content,
providerOptions,
});
} else {
result.push({
role: "user",
content: textSegments.join("\n"),
providerOptions,
});
}
}
break;
}
case "tool":
result.push({
role: "tool",
content: [
{
type: "tool-result",
toolCallId: msg.toolCallId,
toolName: msg.toolName,
output: {
type: "text",
value: msg.content,
},
},
],
providerOptions,
});
break;
}
}
// doing this because: https://github.com/OpenRouterTeam/ai-sdk-provider/issues/262
return JSON.parse(JSON.stringify(result));
}

View file

@ -0,0 +1,139 @@
// Deterministic tool-permission metadata: decides whether a builtin tool
// call needs human approval (command allowlist, workspace file boundaries,
// file-access grants). Shared by both engines — extracted from the legacy
// engine file so the turn-runtime bridges no longer depend on it.
import path from "path";
import { WorkDir } from "../config/config.js";
import { ToolAttachment } from "@x/shared/dist/agent.js";
import { ToolCallPart } from "@x/shared/dist/message.js";
import { z } from "zod";
import { ToolPermissionMetadata } from "@x/shared/dist/runs.js";
import { isBlocked, extractCommandNames } from "../application/lib/command-executor.js";
import { getFileAccessAllowList, type FileAccessGrant, type FileAccessOperation } from "../config/security.js";
import { resolveFilePathForPermission } from "../filesystem/files.js";
type ToolPermissionMetadataValue = z.infer<typeof ToolPermissionMetadata>;
function isPathInside(parent: string, child: string): boolean {
const relative = path.relative(parent, child);
return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
}
function fileGrantCoversPath(grant: FileAccessGrant, operation: FileAccessOperation, resolvedPath: string): boolean {
return grant.operation === operation && isPathInside(path.resolve(grant.pathPrefix), path.resolve(resolvedPath));
}
function commonPathPrefix(paths: string[]): string {
if (!paths.length) return path.resolve(WorkDir);
const split = paths.map(p => path.resolve(p).split(path.sep).filter(Boolean));
const first = split[0];
const common: string[] = [];
for (let i = 0; i < first.length; i++) {
if (split.every(parts => parts[i] === first[i])) {
common.push(first[i]);
} else {
break;
}
}
const prefix = `${path.sep}${common.join(path.sep)}`;
return prefix === path.sep ? prefix : path.resolve(prefix);
}
function grantPrefixForTool(toolName: string, resolvedPaths: string[]): string {
if (toolName === 'file-list' || toolName === 'file-glob' || toolName === 'file-grep' || toolName === 'file-mkdir') {
return commonPathPrefix(resolvedPaths);
}
const parentPaths = resolvedPaths.map(p => path.dirname(p));
return commonPathPrefix(parentPaths);
}
function filePermissionTargets(toolName: string, args: Record<string, unknown>): { operation: FileAccessOperation; paths: string[] } | null {
const pathArg = typeof args.path === 'string' ? args.path : undefined;
switch (toolName) {
case 'file-readText':
case 'parseFile':
case 'LLMParse':
case 'file-exists':
case 'file-stat':
return pathArg ? { operation: 'read', paths: [pathArg] } : null;
case 'file-list':
return pathArg ? { operation: 'list', paths: [pathArg || '.'] } : null;
case 'file-glob':
return { operation: 'search', paths: [typeof args.cwd === 'string' && args.cwd ? args.cwd : '.'] };
case 'file-grep':
return { operation: 'search', paths: [typeof args.searchPath === 'string' && args.searchPath ? args.searchPath : '.'] };
case 'file-writeText':
case 'file-editText':
case 'file-mkdir':
return pathArg ? { operation: 'write', paths: [pathArg] } : null;
case 'file-copy':
case 'file-rename': {
const from = typeof args.from === 'string' ? args.from : undefined;
const to = typeof args.to === 'string' ? args.to : undefined;
return from && to ? { operation: 'write', paths: [from, to] } : null;
}
case 'file-remove':
return pathArg ? { operation: 'delete', paths: [pathArg] } : null;
default:
return null;
}
}
export async function getToolPermissionMetadata(
toolCall: z.infer<typeof ToolCallPart>,
underlyingTool: z.infer<typeof ToolAttachment>,
sessionAllowedCommands: Set<string>,
sessionAllowedFileAccess: FileAccessGrant[],
): Promise<ToolPermissionMetadataValue | null> {
if (underlyingTool.type !== 'builtin') {
return null;
}
if (underlyingTool.name === 'executeCommand') {
const args = toolCall.arguments;
if (!args || typeof args !== 'object' || !('command' in args)) {
return null;
}
const command = String((args as { command: unknown }).command);
if (!isBlocked(command, sessionAllowedCommands)) {
return null;
}
return {
kind: 'command',
commandNames: extractCommandNames(command),
};
}
const args = toolCall.arguments && typeof toolCall.arguments === 'object'
? toolCall.arguments as Record<string, unknown>
: {};
const targets = filePermissionTargets(underlyingTool.name, args);
if (!targets) {
return null;
}
const resolvedTargets = await Promise.all(targets.paths.map(p => resolveFilePathForPermission(p)));
const outsideWorkspacePaths = resolvedTargets
.filter(target => !target.isInsideWorkspace)
.map(target => target.canonicalPath);
if (!outsideWorkspacePaths.length) {
return null;
}
const persistentGrants = getFileAccessAllowList();
const allGrants = [...persistentGrants, ...sessionAllowedFileAccess];
const uncovered = outsideWorkspacePaths.filter(resolvedPath =>
!allGrants.some(grant => fileGrantCoversPath(grant, targets.operation, resolvedPath))
);
if (!uncovered.length) {
return null;
}
return {
kind: 'file',
operation: targets.operation,
paths: uncovered,
pathPrefix: grantPrefixForTool(underlyingTool.name, uncovered),
};
}

View file

@ -23,6 +23,10 @@ export interface AgentTraits {
// Receives workspace context — agent notes and the user work directory —
// composed into its system prompt.
workspaceContext?: boolean;
// Session-loaded skills (activeSkills) re-attach their tools on later
// turns. Distinct from workspaceContext: a trait per concern, so neither
// silently inherits the other's meaning.
skillCarryForward?: boolean;
}
interface BuiltinAgentDefinition {
@ -51,7 +55,7 @@ function promptFileAgent(id: string, getRaw: () => string): BuiltinAgentDefiniti
// definition object.
const COPILOT: BuiltinAgentDefinition = {
build: buildCopilotAgent,
traits: { workspaceContext: true },
traits: { workspaceContext: true, skillCarryForward: true },
};
const builtinAgents: Record<string, BuiltinAgentDefinition> = {
@ -71,15 +75,26 @@ export function builtinAgentIds(): string[] {
return Object.keys(builtinAgents);
}
// Trait lookup for assembly decisions. Unknown/user agents have no traits.
export function hasWorkspaceContext(agentId: string | null | undefined): boolean {
// Trait lookups for assembly decisions. Unknown/user agents have no traits.
function hasTrait(
agentId: string | null | undefined,
trait: keyof AgentTraits,
): boolean {
return (
agentId != null &&
Object.hasOwn(builtinAgents, agentId) &&
builtinAgents[agentId].traits?.workspaceContext === true
builtinAgents[agentId].traits?.[trait] === true
);
}
export function hasWorkspaceContext(agentId: string | null | undefined): boolean {
return hasTrait(agentId, "workspaceContext");
}
export function carriesSkillsForward(agentId: string | null | undefined): boolean {
return hasTrait(agentId, "skillCarryForward");
}
export async function loadAgent(id: string): Promise<z.infer<typeof Agent>> {
// Object.hasOwn: a plain lookup would traverse Object.prototype, so a
// user agent named "constructor"/"toString" would hit an inherited

View file

@ -1,21 +1,20 @@
import { jsonSchema, ModelMessage } from "ai";
import fs from "fs";
import path from "path";
import { WorkDir } from "../config/config.js";
import { jsonSchema } from "ai";
import { Agent, ToolAttachment } from "@x/shared/dist/agent.js";
import { AssistantContentPart, AssistantMessage, Message, MessageList, ProviderOptions, ToolCallPart, ToolMessage, UserMessageContext } from "@x/shared/dist/message.js";
import { AssistantContentPart, AssistantMessage, MessageList, ProviderOptions, ToolCallPart, ToolMessage, UserMessageContext } from "@x/shared/dist/message.js";
import { LanguageModel, stepCountIs, streamText, tool, Tool, ToolSet } from "ai";
import { z } from "zod";
import { LlmStepStreamEvent } from "@x/shared/dist/llm-step-events.js";
import { execTool } from "../application/lib/exec-tool.js";
import { TOOL_ADDITIONS_KEY } from "../application/lib/tool-additions.js";
import { AskHumanRequestEvent, RunEvent, ToolPermissionMetadata, ToolPermissionRequestEvent } from "@x/shared/dist/runs.js";
import { AskHumanRequestEvent, RunEvent, ToolPermissionRequestEvent } from "@x/shared/dist/runs.js";
import { BuiltinTools } from "../application/lib/builtin-tools.js";
import { hasWorkspaceContext, loadAgent } from "./registry.js";
import { composeSystemInstructions } from "./compose-instructions.js";
import { convertFromMessages } from "./message-encoding.js";
import { getToolPermissionMetadata } from "./permission-metadata.js";
import { loadWorkspaceContext } from "./workspace-context.js";
import { isBlocked, extractCommandNames } from "../application/lib/command-executor.js";
import { getFileAccessAllowList, type FileAccessGrant, type FileAccessOperation } from "../config/security.js";
import { resolveFilePathForPermission } from "../filesystem/files.js";
import { extractCommandNames } from "../application/lib/command-executor.js";
import { type FileAccessGrant } from "../config/security.js";
import { notifyIfEnabled } from "../application/notification/notifier.js";
import { IModelConfigRepo } from "../models/repo.js";
import { createLanguageModel } from "../models/models.js";
@ -26,137 +25,12 @@ import { IBus } from "../application/lib/bus.js";
import { IMessageQueue, type MiddlePaneContext } from "../application/lib/message-queue.js";
import { IRunsRepo } from "../runs/repo.js";
import { IRunsLock } from "../runs/lock.js";
import { IAbortRegistry } from "../runs/abort-registry.js";
import { IAbortRegistry } from "../turns/abort-registry.js";
import { PrefixLogger } from "@x/shared";
import { captureLlmUsage } from "../analytics/usage.js";
import { enterUseCase, withUseCase, type UseCase } from "../analytics/use_case.js";
import { classifyToolPermissions, type AutoPermissionCandidate } from "../security/auto-permission-classifier.js";
type ToolPermissionMetadataValue = z.infer<typeof ToolPermissionMetadata>;
function isPathInside(parent: string, child: string): boolean {
const relative = path.relative(parent, child);
return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
}
function fileGrantCoversPath(grant: FileAccessGrant, operation: FileAccessOperation, resolvedPath: string): boolean {
return grant.operation === operation && isPathInside(path.resolve(grant.pathPrefix), path.resolve(resolvedPath));
}
function commonPathPrefix(paths: string[]): string {
if (!paths.length) return path.resolve(WorkDir);
const split = paths.map(p => path.resolve(p).split(path.sep).filter(Boolean));
const first = split[0];
const common: string[] = [];
for (let i = 0; i < first.length; i++) {
if (split.every(parts => parts[i] === first[i])) {
common.push(first[i]);
} else {
break;
}
}
const prefix = `${path.sep}${common.join(path.sep)}`;
return prefix === path.sep ? prefix : path.resolve(prefix);
}
function grantPrefixForTool(toolName: string, resolvedPaths: string[]): string {
if (toolName === 'file-list' || toolName === 'file-glob' || toolName === 'file-grep' || toolName === 'file-mkdir') {
return commonPathPrefix(resolvedPaths);
}
const parentPaths = resolvedPaths.map(p => path.dirname(p));
return commonPathPrefix(parentPaths);
}
function filePermissionTargets(toolName: string, args: Record<string, unknown>): { operation: FileAccessOperation; paths: string[] } | null {
const pathArg = typeof args.path === 'string' ? args.path : undefined;
switch (toolName) {
case 'file-readText':
case 'parseFile':
case 'LLMParse':
case 'file-exists':
case 'file-stat':
return pathArg ? { operation: 'read', paths: [pathArg] } : null;
case 'file-list':
return pathArg ? { operation: 'list', paths: [pathArg || '.'] } : null;
case 'file-glob':
return { operation: 'search', paths: [typeof args.cwd === 'string' && args.cwd ? args.cwd : '.'] };
case 'file-grep':
return { operation: 'search', paths: [typeof args.searchPath === 'string' && args.searchPath ? args.searchPath : '.'] };
case 'file-writeText':
case 'file-editText':
case 'file-mkdir':
return pathArg ? { operation: 'write', paths: [pathArg] } : null;
case 'file-copy':
case 'file-rename': {
const from = typeof args.from === 'string' ? args.from : undefined;
const to = typeof args.to === 'string' ? args.to : undefined;
return from && to ? { operation: 'write', paths: [from, to] } : null;
}
case 'file-remove':
return pathArg ? { operation: 'delete', paths: [pathArg] } : null;
default:
return null;
}
}
export async function getToolPermissionMetadata(
toolCall: z.infer<typeof ToolCallPart>,
underlyingTool: z.infer<typeof ToolAttachment>,
sessionAllowedCommands: Set<string>,
sessionAllowedFileAccess: FileAccessGrant[],
): Promise<ToolPermissionMetadataValue | null> {
if (underlyingTool.type !== 'builtin') {
return null;
}
if (underlyingTool.name === 'executeCommand') {
const args = toolCall.arguments;
if (!args || typeof args !== 'object' || !('command' in args)) {
return null;
}
const command = String((args as { command: unknown }).command);
if (!isBlocked(command, sessionAllowedCommands)) {
return null;
}
return {
kind: 'command',
commandNames: extractCommandNames(command),
};
}
const args = toolCall.arguments && typeof toolCall.arguments === 'object'
? toolCall.arguments as Record<string, unknown>
: {};
const targets = filePermissionTargets(underlyingTool.name, args);
if (!targets) {
return null;
}
const resolvedTargets = await Promise.all(targets.paths.map(p => resolveFilePathForPermission(p)));
const outsideWorkspacePaths = resolvedTargets
.filter(target => !target.isInsideWorkspace)
.map(target => target.canonicalPath);
if (!outsideWorkspacePaths.length) {
return null;
}
const persistentGrants = getFileAccessAllowList();
const allGrants = [...persistentGrants, ...sessionAllowedFileAccess];
const uncovered = outsideWorkspacePaths.filter(resolvedPath =>
!allGrants.some(grant => fileGrantCoversPath(grant, targets.operation, resolvedPath))
);
if (!uncovered.length) {
return null;
}
return {
kind: 'file',
operation: targets.operation,
paths: uncovered,
pathPrefix: grantPrefixForTool(underlyingTool.name, uncovered),
};
}
function formatCurrentDateTime(now: Date): string {
return now.toLocaleString('en-US', {
weekday: 'long',
@ -202,35 +76,6 @@ function buildUserMessageContext({
};
}
function formatUserMessageContextForLlm(userMessageContext: z.infer<typeof UserMessageContext>): string {
const sections: string[] = [];
if (userMessageContext.currentDateTime) {
sections.push(`Current date and time: ${userMessageContext.currentDateTime}`);
}
if (userMessageContext.middlePane) {
if (userMessageContext.middlePane.kind === 'empty') {
sections.push(`Middle pane:\nState: empty`);
} else if (userMessageContext.middlePane.kind === 'note') {
sections.push(`Middle pane:\nState: note\nPath: ${userMessageContext.middlePane.path}\n\nContent:\n\`\`\`\n${userMessageContext.middlePane.content}\n\`\`\``);
} else {
sections.push(`Middle pane:\nState: browser\nURL: ${userMessageContext.middlePane.url}\nTitle: ${userMessageContext.middlePane.title}`);
}
}
if (sections.length === 0) {
return '';
}
return `# User Context
${sections.join('\n\n')}
# User Message
`;
}
import { composeSystemInstructions } from "./compose-instructions.js";
export interface IAgentRuntime {
trigger(runId: string): Promise<void>;
@ -406,7 +251,7 @@ export class AgentRuntime implements IAgentRuntime {
}
}
export async function mapAgentTool(t: z.infer<typeof ToolAttachment>): Promise<Tool> {
async function mapAgentTool(t: z.infer<typeof ToolAttachment>): Promise<Tool> {
switch (t.type) {
case "mcp":
return tool({
@ -449,38 +294,8 @@ export async function mapAgentTool(t: z.infer<typeof ToolAttachment>): Promise<T
}
}
export class RunLogger {
private logFile: string;
private fileHandle: fs.WriteStream;
ensureRunsDir() {
const runsDir = path.join(WorkDir, "runs");
if (!fs.existsSync(runsDir)) {
fs.mkdirSync(runsDir, { recursive: true });
}
}
constructor(runId: string) {
this.ensureRunsDir();
this.logFile = path.join(WorkDir, "runs", `${runId}.jsonl`);
this.fileHandle = fs.createWriteStream(this.logFile, {
flags: "a",
encoding: "utf8",
});
}
log(event: z.infer<typeof RunEvent>) {
if (event.type !== "llm-stream-event") {
this.fileHandle.write(JSON.stringify(event) + "\n");
}
}
close() {
this.fileHandle.close();
}
}
export class StreamStepMessageBuilder {
class StreamStepMessageBuilder {
private parts: z.infer<typeof AssistantContentPart>[] = [];
private textBuffer: string = "";
private reasoningBuffer: string = "";
@ -569,156 +384,6 @@ function formatLlmStreamError(rawError: unknown): string {
return lines.length ? lines.join("\n") : "Model stream error";
}
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
export function convertFromMessages(messages: z.infer<typeof Message>[]): ModelMessage[] {
const result: ModelMessage[] = [];
for (const msg of messages) {
const { providerOptions } = msg;
switch (msg.role) {
case "assistant":
if (typeof msg.content === 'string') {
result.push({
role: "assistant",
content: msg.content,
providerOptions,
});
} else {
result.push({
role: "assistant",
content: msg.content.map(part => {
switch (part.type) {
case 'text':
return part;
case 'reasoning':
return part;
case 'tool-call':
return {
type: 'tool-call',
toolCallId: part.toolCallId,
toolName: part.toolName,
input: part.arguments,
providerOptions: part.providerOptions,
};
}
}),
providerOptions,
});
}
break;
case "system":
result.push({
role: "system",
content: msg.content,
providerOptions,
});
break;
case "user": {
const userMessageContextPrefix = msg.userMessageContext ? formatUserMessageContextForLlm(msg.userMessageContext) : '';
if (typeof msg.content === 'string') {
// Legacy string — pass through unchanged
result.push({
role: "user",
content: `${userMessageContextPrefix}${msg.content}`,
providerOptions,
});
} else {
// New content parts array — collapse text/attachments to text
// for the LLM; inline image parts (video-mode webcam and
// screen-share frames) are passed through as real multimodal
// image parts, grouped under labeled text headers so the
// model knows which images show the user vs their screen.
const textSegments: string[] = userMessageContextPrefix ? [userMessageContextPrefix] : [];
const attachmentLines: string[] = [];
type EncodedImagePart = { type: "image"; image: string; mediaType: string };
const cameraParts: EncodedImagePart[] = [];
const screenParts: EncodedImagePart[] = [];
const frameTimes: string[] = [];
for (const part of msg.content) {
if (part.type === "attachment") {
const sizeStr = part.size ? `, ${formatBytes(part.size)}` : '';
const lineStr = part.lineNumber ? ` (line ${part.lineNumber})` : '';
attachmentLines.push(`- ${part.filename} (${part.mimeType}${sizeStr}) at ${part.path}${lineStr}`);
} else if (part.type === "image") {
const target = part.source === "screen" ? screenParts : cameraParts;
target.push({ type: "image", image: part.data, mediaType: part.mediaType });
if (part.capturedAt) frameTimes.push(part.capturedAt);
} else {
textSegments.push(part.text);
}
}
if (attachmentLines.length > 0) {
if (userMessageContextPrefix) {
textSegments.push("User has attached the following files:", ...attachmentLines, "");
} else {
textSegments.unshift("User has attached the following files:", ...attachmentLines, "");
}
}
const imageCount = cameraParts.length + screenParts.length;
if (imageCount > 0) {
const span = frameTimes.length >= 2
? ` spanning ${frameTimes[0]} to ${frameTimes[frameTimes.length - 1]}`
: frameTimes.length === 1
? ` captured at ${frameTimes[0]}`
: '';
const kinds: string[] = [];
if (cameraParts.length > 0) kinds.push(`${cameraParts.length} live webcam frame${cameraParts.length === 1 ? '' : 's'} of the user`);
if (screenParts.length > 0) kinds.push(`${screenParts.length} frame${screenParts.length === 1 ? '' : 's'} of the user's shared screen`);
textSegments.push(`[Video mode: ${kinds.join(' and ')} attached below, each group oldest to newest,${span ? span + ',' : ''} recorded while they composed this message.]`);
const content: Array<{ type: "text"; text: string } | EncodedImagePart> = [
{ type: "text", text: textSegments.join("\n") },
];
if (cameraParts.length > 0) {
content.push({ type: "text", text: "Webcam frames (oldest to newest):" }, ...cameraParts);
}
if (screenParts.length > 0) {
content.push({ type: "text", text: "Screen-share frames (oldest to newest):" }, ...screenParts);
}
result.push({
role: "user",
content,
providerOptions,
});
} else {
result.push({
role: "user",
content: textSegments.join("\n"),
providerOptions,
});
}
}
break;
}
case "tool":
result.push({
role: "tool",
content: [
{
type: "tool-result",
toolCallId: msg.toolCallId,
toolName: msg.toolName,
output: {
type: "text",
value: msg.content,
},
},
],
providerOptions,
});
break;
}
}
// doing this because: https://github.com/OpenRouterTeam/ai-sdk-provider/issues/262
return JSON.parse(JSON.stringify(result));
}
async function buildTools(agent: z.infer<typeof Agent>): Promise<ToolSet> {
const tools: ToolSet = {};
for (const [name, tool] of Object.entries(agent.tools ?? {})) {
@ -1582,7 +1247,3 @@ async function* streamLlm(
}
}
}
export const MappedToolCall = z.object({
toolCall: ToolCallPart,
agentTool: ToolAttachment,
});

View file

@ -0,0 +1,44 @@
// Connection-state checks shared by skill availability (catalog visibility)
// and copilot prompt composition (connection-specific blocks). One source of
// truth per fact — the "is Slack connected" rule must never fork between the
// catalog and the prompt. Repos resolve lazily so this module adds no static
// DI edge; any failure reads as "not connected" (the historical default).
import { lazyResolve } from "../../di/lazy-resolve.js";
export async function isComposioAvailable(): Promise<boolean> {
try {
const { isConfigured } = await import("../../composio/client.js");
return await isConfigured();
} catch {
return false;
}
}
export async function isCodeModeAvailable(): Promise<boolean> {
try {
const repo = await lazyResolve<import("../../code-mode/repo.js").ICodeModeConfigRepo>("codeModeConfigRepo");
return (await repo.getConfig()).enabled;
} catch {
return false;
}
}
export async function isSlackAvailable(): Promise<boolean> {
try {
const repo = await lazyResolve<import("../../slack/repo.js").ISlackConfigRepo>("slackConfigRepo");
const config = await repo.getConfig();
return config.enabled && config.workspaces.length > 0;
} catch {
return false;
}
}
export async function isGoogleConnected(): Promise<boolean> {
try {
const repo = await lazyResolve<import("../../auth/repo.js").IOAuthRepo>("oauthRepo");
const connection = await repo.read("google");
return !!connection.tokens;
} catch {
return false;
}
}

View file

@ -1,12 +1,14 @@
import { skillCatalog, buildAvailableSkillCatalog } from "./skills/index.js";
import { buildAvailableSkillCatalog } from "./skills/index.js";
import { getRuntimeContext, getRuntimeContextPrompt } from "./runtime-context.js";
import {
isCodeModeAvailable,
isComposioAvailable,
isGoogleConnected,
isSlackAvailable,
} from "./connections.js";
import { composioAccountsRepo } from "../../composio/repo.js";
import { isConfigured as isComposioConfigured } from "../../composio/client.js";
import { CURATED_TOOLKITS } from "@x/shared/dist/composio.js";
import container from "../../di/container.js";
import type { ICodeModeConfigRepo } from "../../code-mode/repo.js";
import type { ISlackConfigRepo } from "../../slack/repo.js";
import type { IOAuthRepo } from "../../auth/repo.js";
import { knowledgeSourcesRepo } from "../../knowledge/sources/repo.js";
const runtimeContextPrompt = getRuntimeContextPrompt(getRuntimeContext());
@ -356,7 +358,6 @@ Never output raw file paths in plain text when they could be wrapped in a filepa
}
/** Keep backward-compatible export for any external consumers */
export const CopilotInstructions = buildStaticInstructions(true, skillCatalog);
let cachedInstructions: string | null = null;
@ -366,31 +367,16 @@ export function invalidateCopilotInstructionsCache(): void {
export async function buildCopilotInstructions(): Promise<string> {
if (cachedInstructions !== null) return cachedInstructions;
const composioEnabled = await isComposioConfigured();
let codeModeEnabled = false;
try {
const repo = container.resolve<ICodeModeConfigRepo>('codeModeConfigRepo');
codeModeEnabled = (await repo.getConfig()).enabled;
} catch {
// repo unavailable — default to disabled
}
let slackConnected = false;
// Connection facts come from the shared checks in connections.ts — the
// same source the skill catalog's availability gating uses.
const [composioEnabled, codeModeEnabled, slackConnected, googleConnected] =
await Promise.all([
isComposioAvailable(),
isCodeModeAvailable(),
isSlackAvailable(),
isGoogleConnected(),
]);
let slackChannelsHint = '';
try {
const slackRepo = container.resolve<ISlackConfigRepo>('slackConfigRepo');
const slackConfig = await slackRepo.getConfig();
slackConnected = slackConfig.enabled && slackConfig.workspaces.length > 0;
} catch {
// repo unavailable — default to not connected
}
let googleConnected = false;
try {
const oauthRepo = container.resolve<IOAuthRepo>('oauthRepo');
const googleConnection = await oauthRepo.read('google');
googleConnected = !!googleConnection.tokens;
} catch {
// repo unavailable — default to not connected
}
if (slackConnected) {
try {
// Surface the channels the user selected for sync so the Copilot

View file

@ -5,6 +5,11 @@ import {
type CapabilityDefinition,
type ModelCapability,
} from "../capabilities/types.js";
import {
isCodeModeAvailable,
isComposioAvailable,
isSlackAvailable,
} from "../connections.js";
import { loadDiskSkills } from "./disk-loader.js";
import builtinToolsSkill from "./builtin-tools/skill.js";
import deletionGuardrailsSkill from "./deletion-guardrails/skill.js";
@ -239,39 +244,8 @@ function getSkillEntries(): SkillEntry[] {
}
// ---- Availability checks (catalog visibility) ----
// Connection-state gates for the catalog, evaluated per build. Repos resolve
// lazily so this module keeps no static edge into the DI graph; any failure
// reads as "not available" (the historical default).
async function isComposioAvailable(): Promise<boolean> {
try {
const { isConfigured } = await import("../../../composio/client.js");
return await isConfigured();
} catch {
return false;
}
}
async function isCodeModeAvailable(): Promise<boolean> {
try {
const { lazyResolve } = await import("../../../di/lazy-resolve.js");
const repo = await lazyResolve<import("../../../code-mode/repo.js").ICodeModeConfigRepo>("codeModeConfigRepo");
return (await repo.getConfig()).enabled;
} catch {
return false;
}
}
async function isSlackAvailable(): Promise<boolean> {
try {
const { lazyResolve } = await import("../../../di/lazy-resolve.js");
const repo = await lazyResolve<import("../../../slack/repo.js").ISlackConfigRepo>("slackConfigRepo");
const config = await repo.getConfig();
return config.enabled && config.workspaces.length > 0;
} catch {
return false;
}
}
// One source of truth for connection state: ../connections.ts (shared with
// the copilot prompt's connection blocks in instructions.ts).
// Pure availability filter over an explicit entry list (exported for tests):
// entries without an availability check are kept; checks are evaluated
@ -427,10 +401,6 @@ export function refreshDiskSkills(): number {
// Initial disk load at module init.
refreshDiskSkills();
// Snapshot of the catalog at module load, kept for backward-compatible
// consumers (e.g. the legacy CopilotInstructions export). Runtime consumers
// should call buildSkillCatalog() to get the live set.
export const skillCatalog = buildSkillCatalog();
export function resolveSkill(identifier: string): ResolvedSkill | null {
const normalized = normalizeIdentifier(identifier);

View file

@ -2,7 +2,7 @@ import * as os from "os";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { CodeRunEvent } from "@x/shared/dist/code-mode.js";
import container from "../../di/container.js";
import { InMemoryAbortRegistry } from "../../runs/abort-registry.js";
import { InMemoryAbortRegistry } from "../../turns/abort-registry.js";
import { BuiltinTools, coalesceCodeRunEvents } from "./builtin-tools.js";
import type { ToolContext } from "./exec-tool.js";

View file

@ -3,7 +3,7 @@ import { RunEvent } from "@x/shared/dist/runs.js";
import { z } from "zod";
import { BuiltinTools } from "./builtin-tools.js";
import { executeTool } from "../../mcp/mcp.js";
import { IAbortRegistry } from "../../runs/abort-registry.js";
import { IAbortRegistry } from "../../turns/abort-registry.js";
/**
* Context passed to every tool execution, providing abort signal and run metadata.

View file

@ -4,7 +4,7 @@ import { assistantText, lastAssistantText } from "../agents/headless.js";
import { TurnInputError } from "../turns/api.js";
import { ASK_HUMAN_TOOL } from "../turns/bridges/real-agent-resolver.js";
import { TurnNotSettledError, type ISessions } from "../sessions/api.js";
import type { TurnEventHub } from "../turns/event-hub.js";
import type { ITurnEventBus } from "../turns/event-hub.js";
// Transport-agnostic command layer: inbound texts from a messaging channel
// are parsed into commands (list / resume / new / stop / status) or forwarded
@ -136,7 +136,7 @@ export class ChannelBridge {
constructor(
private readonly deps: {
sessions: ISessions;
turnEventBus: TurnEventHub;
turnEventBus: ITurnEventBus;
listModels: () => Promise<ModelChoice[]>;
},
) {}

View file

@ -5,7 +5,7 @@ import type { ChannelsConfig, ChannelsStatus } from "@x/shared/dist/channels.js"
import container from "../di/container.js";
import { WorkDir } from "../config/config.js";
import type { ISessions } from "../sessions/api.js";
import type { TurnEventHub } from "../turns/event-hub.js";
import type { ITurnEventBus } from "../turns/event-hub.js";
import { isSignedIn } from "../account/account.js";
import { listGatewayModels } from "../models/gateway.js";
import { listOnboardingModels } from "../models/models-dev.js";
@ -98,7 +98,7 @@ function ensureBridge(): ChannelBridge {
if (!bridge) {
bridge = new ChannelBridge({
sessions: container.resolve<ISessions>("sessions"),
turnEventBus: container.resolve<TurnEventHub>("turnEventBus"),
turnEventBus: container.resolve<ITurnEventBus>("turnEventBus"),
listModels: listBridgeModels,
});
}

View file

@ -9,7 +9,7 @@ import type { IRunsRepo } from '../../runs/repo.js';
import type { IRunsLock } from '../../runs/lock.js';
import type { IBus } from '../../application/lib/bus.js';
import type { IMonotonicallyIncreasingIdGenerator } from '../../application/lib/id-gen.js';
import type { IAbortRegistry } from '../../runs/abort-registry.js';
import type { IAbortRegistry } from '../../turns/abort-registry.js';
import type { CodeModeManager } from '../acp/manager.js';
import type { CodePermissionRegistry } from '../acp/permission-registry.js';
import type { ICodeSessionsRepo } from './repo.js';

View file

@ -14,7 +14,7 @@ import { FSOAuthRepo, IOAuthRepo } from "../auth/repo.js";
import { FSClientRegistrationRepo, IClientRegistrationRepo } from "../auth/client-repo.js";
import { FSGranolaConfigRepo, IGranolaConfigRepo } from "../knowledge/granola/repo.js";
import { FSCodeModeConfigRepo, ICodeModeConfigRepo } from "../code-mode/repo.js";
import { IAbortRegistry, InMemoryAbortRegistry } from "../runs/abort-registry.js";
import { IAbortRegistry, InMemoryAbortRegistry } from "../turns/abort-registry.js";
import { FSAgentScheduleRepo, IAgentScheduleRepo } from "../agent-schedule/repo.js";
import { FSAgentScheduleStateRepo, IAgentScheduleStateRepo } from "../agent-schedule/state-repo.js";
import { FSSlackConfigRepo, ISlackConfigRepo } from "../slack/repo.js";

View file

@ -6,7 +6,7 @@ import { IRunsRepo } from "./repo.js";
import { ICodeSessionsRepo } from "../code-mode/sessions/repo.js";
import { IAgentRuntime } from "../agents/runtime.js";
import { IBus } from "../application/lib/bus.js";
import { IAbortRegistry } from "./abort-registry.js";
import { IAbortRegistry } from "../turns/abort-registry.js";
import { IRunsLock } from "./lock.js";
import { forceCloseAllMcpClients } from "../mcp/mcp.js";
import { extractCommandNames } from "../application/lib/command-executor.js";

View file

@ -12,7 +12,7 @@ import {
loadUserWorkDir,
loadWorkspaceContext,
} from "../../agents/workspace-context.js";
import { hasWorkspaceContext, loadAgent } from "../../agents/registry.js";
import { carriesSkillsForward, loadAgent } from "../../agents/registry.js";
import { BuiltinTools } from "../../application/lib/builtin-tools.js";
import { skillToolNames } from "../../application/assistant/skills/index.js";
import { ModeFlags } from "../../application/assistant/capabilities/types.js";
@ -133,7 +133,6 @@ export class RealAgentResolver {
loadNotes: this.loadNotes,
loadWorkDir: this.loadWorkDir,
});
const copilotContext = hasWorkspaceContext(requested.agentId);
const systemPrompt = composeSystemInstructions({
instructions: agent.instructions,
...workspace,
@ -143,7 +142,7 @@ export class RealAgentResolver {
const tools = await this.resolveTools(agent, {
subagent: subagent ?? false,
});
if (copilotContext && activeSkills?.length) {
if (carriesSkillsForward(requested.agentId) && activeSkills?.length) {
await this.appendSkillTools(tools, activeSkills);
}
return ResolvedAgent.parse({

View file

@ -11,7 +11,7 @@ import type { z } from "zod";
import type { LlmProvider } from "@x/shared/dist/models.js";
import type { AssistantContentPart } from "@x/shared/dist/message.js";
import type { JsonValue, ModelDescriptor, TurnUsage } from "@x/shared/dist/turns.js";
import { convertFromMessages } from "../../agents/runtime.js";
import { convertFromMessages } from "../../agents/message-encoding.js";
import { resolveProviderConfig } from "../../models/defaults.js";
import { createProvider } from "../../models/models.js";
import { isReasoningModel } from "../../models/models-dev.js";

View file

@ -1,5 +1,5 @@
import type { JsonValue } from "@x/shared/dist/turns.js";
import { getToolPermissionMetadata } from "../../agents/runtime.js";
import { getToolPermissionMetadata } from "../../agents/permission-metadata.js";
import type {
IPermissionChecker,
PermissionCheckAllowed,

View file

@ -1,5 +1,5 @@
import { ToolPermissionMetadata } from "@x/shared/dist/runs.js";
import { convertFromMessages } from "../../agents/runtime.js";
import { convertFromMessages } from "../../agents/message-encoding.js";
import type { UseCase } from "../../analytics/use_case.js";
import { classifyToolPermissions } from "../../security/auto-permission-classifier.js";
import type {

View file

@ -3,7 +3,7 @@ import type { z } from "zod";
import type { ToolDescriptor } from "@x/shared/dist/turns.js";
import type { execTool } from "../../application/lib/exec-tool.js";
import type { BuiltinTools } from "../../application/lib/builtin-tools.js";
import type { IAbortRegistry } from "../../runs/abort-registry.js";
import type { IAbortRegistry } from "../abort-registry.js";
import { TurnDependencyError } from "../api.js";
import type { SyncRuntimeTool, ToolExecutionContext } from "../tool-registry.js";
import { RealToolRegistry } from "./real-tool-registry.js";

View file

@ -12,7 +12,7 @@ import { TOOL_ADDITIONS_KEY } from "../../application/lib/tool-additions.js";
import {
type IAbortRegistry,
InMemoryAbortRegistry,
} from "../../runs/abort-registry.js";
} from "../abort-registry.js";
import { TurnDependencyError } from "../api.js";
import type {
IToolRegistry,

View file

@ -8,6 +8,9 @@ import type { TurnBusEvent } from "@x/shared/dist/turns.js";
// listeners accurately reflects that no execution is known active.
export interface ITurnEventBus {
publish(event: TurnBusEvent): void;
// Tail one turn's events / tail everything (the IPC bridge, watchers).
subscribe(turnId: string, listener: (event: TurnBusEvent) => void): () => void;
subscribeAll(listener: (event: TurnBusEvent) => void): () => void;
}
type Listener = (event: TurnBusEvent) => void;

View file

@ -27,7 +27,7 @@ import {
} from "@x/shared/dist/turns.js";
import { reduceSession } from "@x/shared/dist/sessions.js";
import type { z } from "zod";
import { convertFromMessages } from "../agents/runtime.js";
import { convertFromMessages } from "../agents/message-encoding.js";
import { WorkDir } from "../config/config.js";
import { FSSessionRepo } from "../sessions/fs-repo.js";
import { composeModelRequest } from "./compose-model-request.js";