Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .tutti/dev-app/run-local-debug.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ const baseEnv = {
GROUP_CHAT_WEB_DIST: webDist,
GROUP_CHAT_SERVER_URL: baseUrl,
GROUP_CHAT_TOOL_BASE_URL: baseUrl,
GROUP_CHAT_WORKSPACE_ROOT: process.env.TUTTI_WORKSPACE_ROOT || projectRoot,
};

function run(command, args, options = {}) {
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/domains/run-file-artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const BINARY_EXTENSIONS = new Map<string, string>([
[".webp", "image/webp"],
]);

const INTERNAL_AGENT_WORKSPACE_ROOT_FILES = new Set([
const INTERNAL_AGENT_WORKSPACE_FILES = new Set([
"AGENTS.MD",
"BOOTSTRAP.MD",
"CLAUDE.MD",
Expand Down Expand Up @@ -76,7 +76,7 @@ export function shouldImportRunFileArtifactPath(
const [firstSegment, ...rest] = relativePath.split("/");
if (!firstSegment) return true;
if (INTERNAL_AGENT_WORKSPACE_DIRS.has(firstSegment.toLowerCase())) return false;
return rest.length > 0 || !INTERNAL_AGENT_WORKSPACE_ROOT_FILES.has(firstSegment.toUpperCase());
return rest.length > 0 || !INTERNAL_AGENT_WORKSPACE_FILES.has(firstSegment.toUpperCase());
}

export function formatFileReferenceMarkdown(artifact: Pick<Artifact, "id" | "filename">) {
Expand Down
147 changes: 112 additions & 35 deletions apps/server/src/runtimes/local-agent-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
createDefaultLocalAgentProviderPlugins,
createDefaultLocalAgentRuntime,
createManagedAgentRunContextFromHeaders,
getManagedAgentInvocationCredentialFromHeaders,
isManagedAgentInvocationProviderId,
type AgentEvent,
type AgentRunMessage,
type LocalAgentMcpServerConfig,
Expand Down Expand Up @@ -95,8 +97,10 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider {

async listLocalAgentTargets(detectContext?: DetectContext): Promise<LocalAgentTargetStatusResponse> {
const env = buildLocalAgentProcessEnv(process.env, tuttiCliEnv());
const cwd = detectContext?.cwd?.trim() || process.cwd();
const catalog = await loadTuttiAgentCatalog({
runtime: this.localAgentRuntime,
cwd,
env,
detectContext,
commandEnvNames: ["GROUP_CHAT_TUTTI_CLI"],
Expand All @@ -112,6 +116,7 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider {
composer = await loadTuttiAgentComposerOptions({
runtime: this.localAgentRuntime,
agentTargetId: agent.agentTargetId,
cwd,
env,
detectContext,
commandEnvNames: ["GROUP_CHAT_TUTTI_CLI"],
Expand Down Expand Up @@ -154,13 +159,10 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider {
}

async *streamReply(context: RuntimeReplyContext) {
const target = await this.resolveExactTargetForRun(context);
if (!context.managedAgentRunContext && context.managedAgentHeaders && context.runId) {
context.managedAgentRunContext = await createManagedAgentRunContextFromHeaders(context.managedAgentHeaders, {
providerId: target.providerId,
runId: context.runId,
});
}
const { target } = await this.prepareEffectiveRunContext(
context,
localAgentRuntimeRunId(context),
);
const command = resolveLocalAgentCommand(target.providerId);
if (command) {
yield* this.streamCommandBridge(context, command);
Expand All @@ -173,6 +175,7 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider {
private async resolveExactTarget(agentTargetId: string, detectContext?: DetectContext) {
const catalog = await loadTuttiAgentCatalog({
runtime: this.localAgentRuntime,
...(detectContext?.cwd ? { cwd: detectContext.cwd } : {}),
env: buildLocalAgentProcessEnv(process.env, tuttiCliEnv()),
detectContext,
commandEnvNames: ["GROUP_CHAT_TUTTI_CLI"],
Expand All @@ -199,11 +202,38 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider {
return target;
}

private async prepareEffectiveRunContext(context: RuntimeReplyContext, runId: string) {
const workspaceRoot = participantWorkspaceRoot(context.conversation.roomId, context.participant.id);
const provisionalProvider = canonicalProviderId(context.runtimeProfile?.provider);
if (provisionalProvider && isManagedAgentInvocationProviderId(provisionalProvider)) {
await ensureManagedAgentRunContext(context, provisionalProvider, runId);
}

let runCwd = effectiveLocalAgentRunCwd(context, workspaceRoot);
context.agentDetectContext = localAgentRunDetectContext(context, runCwd);
let target = await this.resolveExactTargetForRun(context);

if (!context.managedAgentRunContext && hasManagedAgentCredential(context)) {
await ensureManagedAgentRunContext(context, target.providerId, runId);
runCwd = effectiveLocalAgentRunCwd(context, workspaceRoot);
context.agentDetectContext = localAgentRunDetectContext(context, runCwd);
target = await this.resolveExactTargetForRun(context);
}

return {
target,
workspaceRoot,
runCwd,
extraAllowedDirs: localAgentAllowedDirs(runCwd, workspaceRoot),
};
}

private async resolveUniqueLegacyProviderTarget(providerId: string | null | undefined, detectContext?: DetectContext) {
const normalizedProviderId = canonicalProviderId(providerId);
if (!normalizedProviderId) throw new RuntimeProviderUnsupportedError("Exact Agent target is required.");
const catalog = await loadTuttiAgentCatalog({
runtime: this.localAgentRuntime,
...(detectContext?.cwd ? { cwd: detectContext.cwd } : {}),
env: buildLocalAgentProcessEnv(process.env, tuttiCliEnv()),
detectContext,
commandEnvNames: ["GROUP_CHAT_TUTTI_CLI"],
Expand All @@ -230,7 +260,9 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider {
}

async compactContext(context: RuntimeReplyContext) {
const target = await this.resolveExactTargetForRun(context);
const runId = `context-compact-${context.conversation.id}-${context.participant.id}-${Date.now()}`;
const prepared = await this.prepareEffectiveRunContext(context, runId);
const { target, workspaceRoot, runCwd, extraAllowedDirs } = prepared;
const provider = target.providerId;
const agentTargetId = target.agentTargetId;
if (resolveLocalAgentCommand(provider)) {
Expand All @@ -239,7 +271,6 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider {
if (!this.localAgentRuntime.listProviders().some((item) => item.id === provider)) {
throw new Error(`${provider} local agent provider is not registered.`);
}
const workspaceRoot = participantWorkspaceRoot(context.conversation.roomId, context.participant.id);
const sessionStore = new LocalAgentSessionStore(workspaceRoot);
const previousSession = sessionStore.read(context.conversation.id);
if (!previousSession
Expand All @@ -252,13 +283,6 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider {
}

const controller = new AbortController();
const runId = `context-compact-${context.conversation.id}-${context.participant.id}-${Date.now()}`;
if (!context.managedAgentRunContext && context.managedAgentHeaders) {
context.managedAgentRunContext = await createManagedAgentRunContextFromHeaders(context.managedAgentHeaders, {
providerId: target.providerId,
runId,
});
}
const resume = {
mode: "provider" as const,
...(previousSession.providerSessionId ? { providerSessionId: previousSession.providerSessionId } : {}),
Expand All @@ -271,14 +295,14 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider {
provider,
runtimeKind: "local-agent",
runtimeProvider: provider,
cwd: context.managedAgentRunContext?.cwd ?? workspaceRoot,
cwd: runCwd,
prompt: "/compact",
model: localAgentModelIdForAcp(context.runtimeProfile?.model ?? previousSession.model ?? "default", provider),
reasoning: context.participant.reasoningEffort ?? undefined,
env: buildLocalAgentRunEnv({ ...context, runId }, workspaceRoot),
env: buildLocalAgentRunEnv({ ...context, runId }),
managedAgentInvocation: context.managedAgentRunContext?.managedAgentInvocation,
metadata: context.participant.speedMode ? { speedMode: context.participant.speedMode } : undefined,
extraAllowedDirs: [workspaceRoot],
extraAllowedDirs,
resume,
signal: controller.signal,
})) {
Expand Down Expand Up @@ -353,9 +377,10 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider {
envOverrides?: Record<string, string>,
) {
const workspaceRoot = participantWorkspaceRoot(context.conversation.roomId, context.participant.id);
const runCwd = effectiveLocalAgentRunCwd(context, workspaceRoot);
const child = spawn(command, {
cwd: workspaceRoot,
env: buildLocalAgentRunEnv(context, workspaceRoot, envOverrides),
cwd: runCwd,
env: buildLocalAgentRunEnv(context, envOverrides),
shell: true,
stdio: "pipe",
});
Expand Down Expand Up @@ -411,6 +436,8 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider {
legacyProviderUnique: boolean,
) {
const workspaceRoot = participantWorkspaceRoot(context.conversation.roomId, context.participant.id);
const runCwd = effectiveLocalAgentRunCwd(context, workspaceRoot);
const extraAllowedDirs = localAgentAllowedDirs(runCwd, workspaceRoot);
const controller = new AbortController();
if (context.runId) {
this.processes.set(context.runId, {
Expand All @@ -426,7 +453,7 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider {
const previousSession = sessionStore.read(context.conversation.id);
const input = buildLocalAgentInput(context);
const timeoutMs = localAgentTimeoutMs();
const runtimeRunId = context.runId ?? `${context.conversation.id}:${context.participant.id}`;
const runtimeRunId = localAgentRuntimeRunId(context);
let skillFallbackEnv: Record<string, string> | undefined;
let skillContext = emptyTuttiAgentSkillContext(agentTargetId, provider, runtimeRunId);
if (shouldLoadGroupChatAgentSkillContext(context, input)) {
Expand All @@ -435,7 +462,7 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider {
agentTargetId,
provider,
agentSessionId: runtimeRunId,
workspaceRoot,
cwd: runCwd,
});
} catch (error) {
if (provider === "codex" && isSkillLoadFailure(error)) {
Expand All @@ -446,7 +473,7 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider {
agentTargetId,
provider,
agentSessionId: runtimeRunId,
workspaceRoot,
cwd: runCwd,
envOverrides: skillFallbackEnv,
});
} catch (fallbackError) {
Expand Down Expand Up @@ -491,19 +518,19 @@ export class LocalAgentRuntimeProvider implements RuntimeProvider {
provider,
runtimeKind: "local-agent",
runtimeProvider: provider,
cwd: context.managedAgentRunContext?.cwd ?? workspaceRoot,
cwd: runCwd,
prompt,
systemPrompt,
history: buildKitHistory(context, historyLimit),
model: localAgentModelIdForAcp(context.runtimeProfile?.model ?? "default", provider),
reasoning: context.participant.reasoningEffort ?? undefined,
...(mcpServers ? { mcpServers } : {}),
...(skillManifest ? { skillManifest } : {}),
env: buildLocalAgentRunEnv(context, workspaceRoot, skillFallbackEnv),
env: buildLocalAgentRunEnv(context, skillFallbackEnv),
managedAgentInvocation: context.managedAgentRunContext?.managedAgentInvocation,
metadata: context.participant.speedMode ? { speedMode: context.participant.speedMode } : undefined,
...(timeoutMs ? { timeoutMs } : {}),
extraAllowedDirs: [workspaceRoot],
extraAllowedDirs,
resume,
signal: controller.signal,
})) {
Expand Down Expand Up @@ -597,6 +624,62 @@ class LocalAgentCommandError extends Error {
}
}

function localAgentRuntimeRunId(context: RuntimeReplyContext) {
return context.runId ?? `${context.conversation.id}:${context.participant.id}`;
}

async function ensureManagedAgentRunContext(
context: RuntimeReplyContext,
providerId: string,
runId: string,
) {
if (context.managedAgentRunContext || !hasManagedAgentCredential(context)) return;
context.managedAgentRunContext = await createManagedAgentRunContextFromHeaders(
context.managedAgentHeaders,
{ providerId, runId },
);
}

function hasManagedAgentCredential(context: RuntimeReplyContext) {
return Boolean(getManagedAgentInvocationCredentialFromHeaders(context.managedAgentHeaders));
}

function effectiveLocalAgentRunCwd(
context: RuntimeReplyContext,
participantWorkspaceRoot: string,
) {
return context.managedAgentRunContext?.cwd ?? participantWorkspaceRoot;
}

function localAgentRunDetectContext(
context: RuntimeReplyContext,
runCwd: string,
): DetectContext {
const managedAgentInvocation = context.managedAgentRunContext?.managedAgentInvocation;
const credential = managedAgentInvocation?.credential;
return {
...(context.agentDetectContext ?? {}),
cwd: runCwd,
...(managedAgentInvocation ? { managedAgentInvocation } : {}),
...(credential
? {
redactionSecrets: Array.from(new Set([
...(context.agentDetectContext?.redactionSecrets ?? []),
credential,
])),
}
: {}),
};
}

function localAgentAllowedDirs(runCwd: string, participantWorkspaceRoot: string) {
const normalizedRunCwd = resolve(runCwd);
const normalizedParticipantWorkspaceRoot = resolve(participantWorkspaceRoot);
return normalizedRunCwd === normalizedParticipantWorkspaceRoot
? [normalizedRunCwd]
: [normalizedRunCwd, normalizedParticipantWorkspaceRoot];
}

function localAgentTimeoutMs() {
const raw = process.env.GROUP_CHAT_LOCAL_AGENT_TIMEOUT_MS;
if (raw === undefined || raw.trim() === "") return undefined;
Expand All @@ -606,12 +689,10 @@ function localAgentTimeoutMs() {

function buildLocalAgentRunEnv(
context: RuntimeReplyContext,
workspaceRoot: string,
overrides?: Record<string, string>,
): Record<string, string> {
return {
...buildLocalAgentProcessEnv(process.env, { ...tuttiCliEnv(), ...overrides }),
GROUP_CHAT_WORKSPACE: workspaceRoot,
GROUP_CHAT_RUN_ID: context.runId ?? "",
GROUP_CHAT_PARTICIPANT_ID: context.participant.id,
GROUP_CHAT_CONVERSATION_ID: context.conversation.id,
Expand Down Expand Up @@ -864,15 +945,15 @@ async function loadGroupChatAgentSkillContext(input: {
agentTargetId: string;
provider: string;
agentSessionId: string;
workspaceRoot: string;
cwd: string;
envOverrides?: Record<string, string>;
}): Promise<TuttiAgentSkillContext> {
try {
const env = buildLocalAgentProcessEnv(process.env, { ...tuttiCliEnv(), ...input.envOverrides });
return await loadTuttiAgentSkillContext({
agentTargetId: input.agentTargetId,
agentSessionId: input.agentSessionId,
cwd: tuttiWorkspaceCwd(input.workspaceRoot),
cwd: input.cwd,
env,
commandEnvNames: ["GROUP_CHAT_TUTTI_CLI"],
});
Expand All @@ -896,10 +977,6 @@ function emptyTuttiAgentSkillContext(
};
}

function tuttiWorkspaceCwd(fallback: string) {
return process.env.TUTTI_WORKSPACE_ROOT?.trim() || process.env.GROUP_CHAT_WORKSPACE_ROOT?.trim() || fallback;
}

function joinPromptParts(...parts: Array<string | undefined | null>) {
return parts
.map((part) => part?.trim())
Expand Down
1 change: 0 additions & 1 deletion scripts/package-tutti-app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ export PORT="\${TUTTI_APP_PORT:-8788}"
export GROUP_CHAT_APP_VERSION="${version}"
export GROUP_CHAT_WEB_DIST="$package_dir/dist"
export GROUP_CHAT_HOME="\${TUTTI_APP_DATA_DIR:-$package_dir/.data}"
export GROUP_CHAT_WORKSPACE_ROOT="\${TUTTI_WORKSPACE_ROOT:-$GROUP_CHAT_HOME}"

base_url="\${TUTTI_APP_BASE_URL:-http://$HOST:$PORT}"
export GROUP_CHAT_SERVER_URL="$base_url"
Expand Down
Loading