Z ZPI 文档 v0.3.8
介绍页

源码

packages/coding-agent/src/core/sdk.ts

typescript268 行查看原文
import { homedir } from "node:os";
import { join, resolve } from "node:path";
import type { AgentTool, ThinkingLevel } from "@zpi-dev/agent";
import type { Context, Model } from "@zpi-dev/ai";
import { assertSupportedOptions, clampThinkingLevel } from "@zpi-dev/ai";
import { assembleSession, type SessionContributions, validatePluginSnapshot } from "../plugins/assembly.ts";
import type { HookRegistration } from "../plugins/hooks.ts";
import { withPluginResources } from "../plugins/resources.ts";
import { AgentSession } from "./agent-session.ts";
import type { CompactionOptions } from "./compaction.ts";
import type { ModelPerformanceTotals } from "./model-performance.ts";
import { ModelRuntime } from "./model-runtime.ts";
import { FileResourceLoader } from "./resources.ts";
import { SessionManager } from "./session-manager.ts";
import { SettingsManager } from "./settings-manager.ts";
import { recoverCanonicalToolCallsAsync } from "./streaming-tool-journal.ts";
import { buildSystemContext, type PromptTemplate } from "./system-prompt.ts";
import { createCodingTools, FileReadState } from "./tools/index.ts";
import type { SessionCurrentState, TranscriptStore } from "./transcript-store.ts";
import type { ResourceLoader, ToolContext } from "./types.ts";
export interface SystemPromptContext {
  tools: readonly AgentTool[];
}
export interface CreateAgentSessionOptions extends SessionContributions {
  /** Build tool definitions and selection once the shared resource catalog is loaded. */
  prepareTools?: (loader: ResourceLoader) => Pick<SessionContributions, "customTools" | "tools">;
  modelRequestOptions?: () => import("@zpi-dev/ai").SimpleStreamOptions;
  onCompaction?: (
    phase: "before" | "after",
    context: { manual: boolean; instructions: string },
    signal?: AbortSignal,
  ) => Promise<void>;
  /** Base instructions only. SDK resources and plugin context remain independently managed. */
  systemPrompt?: string | ((context: SystemPromptContext) => string);
  /** Host policy checked at each foreground model request, including continuation requests. */
  beforeModelRequest?: (model: Model, context: Context) => void;
  hooks?: readonly HookRegistration[];
  modelPerformanceTotals?: ModelPerformanceTotals;
  cwd?: string;
  agentDir?: string;
  modelRuntime?: ModelRuntime;
  model?: Model;
  thinkingLevel?: ThinkingLevel;
  /** Literal additions, recomputed after resource reload when supplied as a function. */
  customInstructions?: readonly string[] | (() => readonly string[]);
  resourceLoader?: ResourceLoader;
  sessionManager?: SessionManager;
  /** Indexed transcript and explicit current product choices. Mutually exclusive with sessionManager. */
  storage?: { transcript: TranscriptStore; currentState: SessionCurrentState };
  homeDir?: string;
  compaction?: CompactionOptions;
  settingsManager?: SettingsManager;
  promptTemplate?: () => PromptTemplate;
  projectName?: string | (() => string);
}
export interface CreateAgentSessionResult {
  session: AgentSession;
  sessionManager: SessionManager;
}
export async function createAgentSession(
  options: CreateAgentSessionOptions = {},
): Promise<CreateAgentSessionResult> {
  assertSupportedOptions(
    options,
    [
      "cwd",
      "prepareTools",
      "beforeModelRequest",
      "systemPrompt",
      "modelRequestOptions",
      "onCompaction",
      "agentDir",
      "modelRuntime",
      "model",
      "thinkingLevel",
      "noTools",
      "tools",
      "excludeTools",
      "customTools",
      "plugins",
      "hooks",
      "customInstructions",
      "toolOverrides",
      "resourceLoader",
      "sessionManager",
      "storage",
      "homeDir",
      "compaction",
      "settingsManager",
      "promptTemplate",
      "projectName",
      "modelPerformanceTotals",
    ],
    "createAgentSession",
  );
  if (options.storage !== undefined) {
    assertSupportedOptions(options.storage, ["transcript", "currentState"], "storage");
    if (options.sessionManager) throw new Error("storage and sessionManager are mutually exclusive");
    if (!options.storage.transcript || !options.storage.currentState)
      throw new Error("storage requires transcript and currentState");
    for (const method of ["get", "setModel", "setThinkingLevel", "setName"] as const)
      if (typeof options.storage.currentState[method] !== "function")
        throw new Error(`Invalid storage.currentState.${method}`);
    for (const method of [
      "getHeader",
      "getFile",
      "snapshot",
      "getStateAt",
      "getTail",
      "readEntryById",
      "readRange",
      "getContextPlan",
      "readUnresolvedToolJournals",
      "append",
      "replace",
    ] as const)
      if (typeof options.storage.transcript[method] !== "function")
        throw new Error(`Invalid storage.transcript.${method}`);
    for (const method of ["appendAsync", "replaceAsync"] as const)
      if (
        options.storage.transcript[method] !== undefined &&
        typeof options.storage.transcript[method] !== "function"
      )
        throw new Error(`Invalid storage.transcript.${method}`);
  }
  if (options.noTools !== undefined && options.noTools !== "all" && options.noTools !== "builtin")
    throw new Error("Invalid noTools");
  const managerOption =
    options.sessionManager ??
    (options.storage
      ? SessionManager.fromStore(options.storage.transcript, options.storage.currentState)
      : undefined);
  const cwd = resolve(options.cwd ?? managerOption?.getCwd() ?? process.cwd());
  const agentDir = options.agentDir ?? join(homedir(), ".zpi", "agent");
  const settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);
  if (options.compaction) settingsManager.applyOverrides({ compaction: options.compaction });
  const runtime = options.modelRuntime ?? (await ModelRuntime.create());
  const manager = managerOption ?? SessionManager.create(cwd, join(agentDir, "sessions"));
  const restored = await manager.materializeContext();
  const selection = manager.getCurrentState();
  const available = await runtime.getAvailable();
  const model =
    options.model ??
    (selection.model ? runtime.getModel(selection.model.provider, selection.model.modelId) : available[0]);
  if (!model)
    throw new Error(selection.model ? "Recorded model is not configured" : "No callable model configured");
  if (!available.some((m) => m.provider === model.provider && m.id === model.id))
    throw new Error("Model authentication or provider is not configured");
  if (!selection.model || selection.model.modelId !== model.id || selection.model.provider !== model.provider)
    manager.appendModelChange(model.provider, model.id);
  await recoverCanonicalToolCallsAsync(manager, restored.messages);
  let session: AgentSession;
  const fileReadState = new FileReadState();
  const builtin = createCodingTools(
    cwd,
    join(agentDir, "tool-output", manager.getSessionId()),
    () => session?.model ?? model,
    () => ({
      cwd,
      model: session?.model ?? model,
      thinkingLevel: session?.thinkingLevel ?? "off",
      sessionManager: manager,
    }),
    fileReadState,
  );
  const toolContext = (signal?: AbortSignal): ToolContext => ({
    cwd,
    model: session.model,
    thinkingLevel: session.thinkingLevel,
    sessionManager: manager,
    signal,
    isIdle: () => session.isIdle,
    getSystemPrompt: () => session.systemPrompt,
    getToolNames: () => session.state.tools.map((tool) => tool.name),
  });
  // The final loader is shared by tool preparation, prompt construction and later refreshes.
  const plugins = options.plugins ?? [];
  for (const plugin of plugins) validatePluginSnapshot(plugin);
  const baseLoader =
    options.resourceLoader ??
    new FileResourceLoader({ projectDir: manager.getCwd(), homeDir: options.homeDir });
  const loader = withPluginResources(
    baseLoader,
    plugins.flatMap((plugin) => [...(plugin.skills ?? [])]),
  );
  await loader.reload?.();
  const prepared = options.prepareTools?.(loader) ?? {};
  assertSupportedOptions(prepared, ["customTools", "tools"], "prepareTools result");
  const assembly = assembleSession({ ...options, ...prepared, builtinTools: builtin, context: toolContext });
  const { allTools: all, activeTools: active } = assembly;
  const buildContext = () => {
    let baseInstructions: string | undefined;
    if (options.systemPrompt !== undefined) {
      baseInstructions =
        typeof options.systemPrompt === "function"
          ? options.systemPrompt({
              tools: session?.state.tools ?? active,
            })
          : options.systemPrompt;
      if (typeof baseInstructions !== "string") throw new Error("Invalid systemPrompt");
    }
    const instructions =
      typeof options.customInstructions === "function"
        ? options.customInstructions()
        : options.customInstructions;
    if (
      instructions !== undefined &&
      (!Array.isArray(instructions) || instructions.some((part) => typeof part !== "string"))
    )
      throw new Error("invalid_input: customInstructions must be an array of strings");
    const context = buildSystemContext({
      cwd,
      projectName: typeof options.projectName === "function" ? options.projectName() : options.projectName,
      template: options.promptTemplate?.(),
      tools: session?.state.tools ?? active,
      loader,
      customInstructions: instructions,
      baseInstructions,
    });
    return { ...context, sections: { ...context.sections, ...assembly.getContextSections() } };
  };
  let requestedThinking = options.thinkingLevel;
  if (requestedThinking == null) {
    const hasExistingSession = restored.messages.length > 0;
    const useRecordedThinking =
      manager.hasCurrentState() || (hasExistingSession && manager.getStateAt().hasThinkingEntry);
    requestedThinking = useRecordedThinking
      ? selection.thinkingLevel
      : ((!hasExistingSession
          ? settingsManager.getModelThinkingLevel(model.provider, model.id)
          : undefined) ??
        settingsManager.getDefaultThinkingLevel() ??
        "medium");
  }
  const thinking = clampThinkingLevel(model, requestedThinking);
  if (thinking !== selection.thinkingLevel) manager.appendThinkingLevelChange(thinking);
  session = await AgentSession.create(
    model,
    runtime,
    manager,
    all,
    active,
    buildContext().base,
    thinking,
    {
      loader,
      buildContext,
      settingsManager,
    },
    options.modelPerformanceTotals,
    {
      commands: assembly.commands,
      hooks: [...assembly.hooks, ...(options.hooks ?? [])],
      beforeModelRequest: options.beforeModelRequest,
      modelRequestOptions: options.modelRequestOptions,
      onCompaction: options.onCompaction,
    },
  );
  session.subscribe((event) => {
    if (
      (event.type === "compaction" && event.status === "completed") ||
      (event.type === "entry_appended" && event.entry.type === "microcompact")
    )
      fileReadState.clear();
  });
  return { session, sessionManager: manager };
}