diff --git a/src/agent/progress.ts b/src/agent/progress.ts index 62fc43dc0..ccaca5aca 100644 --- a/src/agent/progress.ts +++ b/src/agent/progress.ts @@ -234,7 +234,9 @@ export type AgentProgress = /** The handoff document the agent published (`WizardUI.setHandoffText`). */ | { kind: 'handoff'; text: string } /** The run's final outro payload (`WizardUI.setOutroData`). */ - | { kind: 'completion'; outro: OutroData }; + | { kind: 'completion'; outro: OutroData } + /** One short line per agent step, only from a run that collects its transcript. */ + | { kind: 'activity'; line: string }; export type ProgressEmitter = (event: AgentProgress) => void; diff --git a/src/agent/runner/shared/types.ts b/src/agent/runner/shared/types.ts index ecde56600..dd029b275 100644 --- a/src/agent/runner/shared/types.ts +++ b/src/agent/runner/shared/types.ts @@ -51,6 +51,9 @@ export interface AgentRunDefinition { skillId?: string; /** Additional program-specific prompt instructions. Appended after the default project prompt. */ customPrompt?: (ctx: PromptContext) => string; + prompt?: (ctx: PromptContext) => string; // replaces the assembled project prompt; linear + collectTranscript?: boolean; // keep a 256K-character transcript tail; linear, Anthropic + requestRemark?: boolean; // false skips the closing remark; linear, Anthropic /** Additional MCP servers (e.g. Svelte MCP) */ additionalMcpServers?: Record; /** Package manager detector. Defaults to detectNodePackageManagers. */ @@ -178,6 +181,7 @@ export interface RunConfig { seedTasks?: () => SeedTaskEntry[]; /** Completion hooks, bound by the caller. */ hooks?: RunHooks; + scanReport?: 'flush' | 'defer'; // defer leaves the scan report to the outer run } /** Invocation flags the agent reads. */ @@ -286,6 +290,7 @@ export interface RunSnapshot { notebookUrl?: string; /** The handoff document the agent published, when it did. */ handoffText?: string; + transcriptTail?: string; // set when the run definition asks for collectTranscript } /** A sequence decides an outcome; the dispatcher owns its snapshot. */ diff --git a/src/agent/types.ts b/src/agent/types.ts index b23200cfb..179401617 100644 --- a/src/agent/types.ts +++ b/src/agent/types.ts @@ -12,6 +12,8 @@ export type { AgentRunDefinition, PromptContext, RunConfig, + ResolvedBinding, + RunHooks, RunInput, RunResult, } from './runner'; diff --git a/src/lib/wizard-session.ts b/src/lib/wizard-session.ts index 548eacb01..71d8b2ff0 100644 --- a/src/lib/wizard-session.ts +++ b/src/lib/wizard-session.ts @@ -28,12 +28,15 @@ import type { // entry would form a module cycle here. // eslint-disable-next-line @typescript-eslint/no-restricted-imports -- B2: the session becomes a TUI projection import { OutroKind } from '@agent/progress'; +import { DiscoveredFeature } from '@shared/discovered-feature'; // These shapes moved to their owners; re-exported so every session reader -// keeps its import path. `Credentials` sits with the API types, and the -// outro, question and task-notice shapes are the agent's contract. +// keeps its import path. `Credentials` sits with the API types, +// `DiscoveredFeature` sits in shared so programs can name it without the +// session, and the outro, question and task-notice shapes are the agent's +// contract. export type { Credentials, CloudRegion }; -export { OutroKind }; +export { OutroKind, DiscoveredFeature }; export type { AskAnswers, AskQuestion, OutroData, PendingQuestion, TaskNotice }; function parseProjectIdArg(value: string | undefined): number | undefined { @@ -54,12 +57,6 @@ export enum RunPhase { Error = 'error', } -/** Features discovered by the feature-discovery subagent */ -export enum DiscoveredFeature { - Stripe = 'stripe', - LLM = 'llm', -} - /** Consent to report what local detection found (see `scanConsent` below). */ export enum ScanConsent { Undecided = 'undecided', diff --git a/src/programs/credentials.ts b/src/programs/credentials.ts new file mode 100644 index 000000000..a344b6a0b --- /dev/null +++ b/src/programs/credentials.ts @@ -0,0 +1,17 @@ +/** Resolved credentials runProgram passes to one agent run. */ + +import type { ApiProject, ApiUser, Credentials } from '@shared/api'; + +export type ResolvedProgramCredentials = { + posthog: Credentials; // token, project API key, project ID and host + project: ApiProject | null; // the project, when known + apiUser: ApiUser | null; // the user, when known +}; + +/** The caller authenticates once per scope; the signal aborts with the invocation. */ +export type CredentialsProvider = { + resolve( + programId: string, + context: { signal: AbortSignal }, + ): Promise; +}; diff --git a/src/programs/index.ts b/src/programs/index.ts index 68a7e3d1e..e3f438250 100644 --- a/src/programs/index.ts +++ b/src/programs/index.ts @@ -1,5 +1,6 @@ /** Public runtime entry for the programs surface. */ export type * from './types'; +export { runProgram } from './run-program'; export { Program, PROGRAM_REGISTRY, diff --git a/src/programs/program-store.ts b/src/programs/program-store.ts new file mode 100644 index 000000000..67ffa533a --- /dev/null +++ b/src/programs/program-store.ts @@ -0,0 +1,95 @@ +/* eslint-disable @typescript-eslint/no-unused-vars -- A shell: B3 fills in the bodies. */ +import type { AgentProgress, ResolvedBinding, RunResult } from '@agent/types'; +import type { ApiProject, ApiUser, Credentials } from '@shared/api'; + +/** One agent run's progress event, attributed to its run. */ +export type ProgramRunProgress = { + kind: 'run'; // one agent event + runId: string; // the run it came from + event: AgentProgress; // status, tasks, links, completion and more +}; + +/** A copy of the invocation's data, sent after each write. */ +export type ProgramDataProgress = { + kind: 'program'; // the program's data changed + data: ProgramInvocationData; // a copy of it after the change +}; + +export type ProgramProgress = ProgramRunProgress | ProgramDataProgress; + +/** What a diagnostic is about: one run's progress event, or a data snapshot. */ +type DiagnosticSource = + | { runId: string; eventKind: AgentProgress['kind'] } + | { eventKind: 'data' }; + +/** An observer failure or a late event, kept instead of breaking the run. */ +export type ProgramDiagnostic = DiagnosticSource & { message: string }; + +/** Data owned by one program invocation, independent of its progress feed. */ +export type ProgramInvocationData = { + credentials: Credentials | null; // the login; holds tokens, don't log it + apiProject: ApiProject | null; // the login's project + apiUser: ApiUser | null; // the login's user + detection: { frameworkContext: Record }; // always {} here + binding: ResolvedBinding | null; // the route; null until it resolves + aiSdkStampReported: boolean; // true once the AI SDK stamp was considered +}; + +/** An agent run's final result. */ +export type SettledProgramRun = { + runId: string; // the run's label + result: RunResult; // what runAgent returned +}; + +export type AgentProgressAdapter = { + onProgress(event: AgentProgress): void; + finish(result: RunResult): void; +}; + +export class ProgramStore { + constructor( + options: { + aiSdkStampReported?: boolean; + onData?: (progress: ProgramDataProgress) => void; + } = {}, + ) { + throw new Error('ProgramStore: not implemented'); + } + + readData(): ProgramInvocationData { + throw new Error('ProgramStore: not implemented'); + } + + setAuthenticated( + auth: Pick, + ): void { + throw new Error('ProgramStore: not implemented'); + } + + setFrameworkContext(key: string, value: unknown): void { + throw new Error('ProgramStore: not implemented'); + } + + setBinding(binding: ResolvedBinding): void { + throw new Error('ProgramStore: not implemented'); + } + + setAiSdkStampReported(): void { + throw new Error('ProgramStore: not implemented'); + } + + beginRun( + runId: string, + observer?: (progress: ProgramRunProgress) => void, + ): AgentProgressAdapter { + throw new Error('ProgramStore: not implemented'); + } + + settledRuns(): SettledProgramRun[] { + throw new Error('ProgramStore: not implemented'); + } + + readDiagnostics(): ProgramDiagnostic[] { + throw new Error('ProgramStore: not implemented'); + } +} diff --git a/src/programs/run-program.ts b/src/programs/run-program.ts new file mode 100644 index 000000000..4c8c79280 --- /dev/null +++ b/src/programs/run-program.ts @@ -0,0 +1,107 @@ +/** A caller-owned program invocation. No TUI store or session is required. */ +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/require-await -- A shell: B3 fills in the body. */ +import type { RunOutcome } from '@agent'; +import type { + AgentInteraction, + AgentRunDefinition, + RunConfig, + RunHooks, + RunInput, + RunResult, +} from '@agent/types'; +import type { Harness, Integration, Sequence } from '@shared/constants'; +import type { DiscoveredFeature } from '@shared/discovered-feature'; +import type { DetectedSource } from './warehouse-sources/types'; +import type { + CredentialsProvider, + ResolvedProgramCredentials, +} from './credentials'; +import type { + ProgramDiagnostic, + ProgramInvocationData, + ProgramProgress, + SettledProgramRun, +} from './program-store'; + +/** Launch-time routing choices, such as the CLI's --harness, --sequence and --model. */ +export type ProgramOverrides = { + harness?: Harness; // --harness + sequence?: Sequence; // --sequence + model?: string; // --model +}; + +/** Feature flags and their payloads from one evaluation. */ +export type WizardFlagSnapshot = { + flags: Record; // flag key to variant + payloads: Record; // flag key to payload +}; + +/** Program-level settings the caller reads from the program's `ProgramConfig`. */ +export type ProgramSettings = { + requiresAi?: boolean; // false skips the AI-processing approval + agentFlow?: string; // context-mill flow; defaults to the program ID + allowedTools?: RunConfig['allowedTools']; // added to the base tools + disallowedTools?: RunConfig['disallowedTools']; // removed from the base tools + excludedTaskTypes?: RunConfig['excludedTaskTypes']; // task types to skip for these flags + postAuthGates?: readonly string[]; // steps settled after login, before the agent +}; + +/** Copied when runProgram receives it; credentials, run, program, hooks and seedTasks stay by reference. */ +export interface ProgramInput { + installDir: string; // the project the agent works in + run: AgentRunDefinition; // built from the program's ProgramConfig + program?: ProgramSettings; // from the same ProgramConfig + credentials?: ResolvedProgramCredentials; // a login you hold; else options.credentials + runId?: string; // labels progress and the outcome; generated when absent + overrides?: ProgramOverrides; // launch overrides; dev and test builds only + composed?: boolean; // true for a sub-run inside another program + skillId?: string; // labels the run; defaults to run.skillId, then integrationLabel + integration?: Integration | null; // the detected framework + frameworkDocsUrl?: string; // the framework's docs page + flags?: Partial; // run flags such as ci and signup + host?: RunInput['host']; // where PostHog is + wizardFlags?: Record; // a flag snapshot; else options.featureFlags + wizardFlagPayloads?: Record; // payloads for wizardFlags + seedTasks?: RunConfig['seedTasks']; // tasks queued before the planner runs + hooks?: RunHooks; // the program's completion hooks + warehouseSources?: readonly DetectedSource[]; // AI SDK stamp evidence + mayReportScanResults?: boolean; // consent to send the AI SDK stamp + discoveredFeatures?: readonly DiscoveredFeature[]; // AI SDK stamp evidence + aiSdkStampReported?: boolean; // true skips the AI SDK stamp +} + +export interface ProgramOptions { + credentials?: CredentialsProvider; // resolves the login when input has none + interaction?: AgentInteraction; // answers the agent's questions and notices + onProgress?: (progress: ProgramProgress) => void; // run events and data snapshots + awaitAiApproval?: (context: { + programId: string; + signal: AbortSignal; + }) => Promise; // asks for AI-processing approval; false aborts + awaitPostAuthGates?: (context: { + programId: string; + gates: readonly string[]; + signal: AbortSignal; + }) => Promise; // waits while the caller settles the gates + featureFlags?: () => Promise; // loads flags when input has none + signal?: AbortSignal; // cancels the run +} + +export interface ProgramRunOutcome { + programId: string; // the program that ran + outcome: RunOutcome; // success, aborted, failed or crashed + data: ProgramInvocationData; // the final login and route + settledRuns: SettledProgramRun[]; // the agent run's result, once it ran + diagnostics: ProgramDiagnostic[]; // observer failures and late events + artifacts: { reportFile?: string }; // where the agent writes its report + failure?: RunResult['failure']; // code and message on any non-success +} + +/** Run an existing program from explicit inputs, with invocation-owned state. */ +export async function runProgram( + programId: string, + callerInput: ProgramInput, + options: ProgramOptions = {}, +): Promise { + throw new Error('runProgram: not implemented'); +} diff --git a/src/programs/types.ts b/src/programs/types.ts index 620e94d18..fb3306d58 100644 --- a/src/programs/types.ts +++ b/src/programs/types.ts @@ -7,3 +7,19 @@ export type { StoreInitContext, } from './program-step'; export type { FrameworkConfig, SetupQuestion } from './framework-config'; +export type { + ProgramInput, + ProgramOptions, + ProgramOverrides, + ProgramRunOutcome, + ProgramSettings, + WizardFlagSnapshot, +} from './run-program'; +export type { + ProgramDataProgress, + ProgramDiagnostic, + ProgramInvocationData, + ProgramProgress, + ProgramRunProgress, + SettledProgramRun, +} from './program-store'; diff --git a/src/shared/discovered-feature.ts b/src/shared/discovered-feature.ts new file mode 100644 index 000000000..da12707e0 --- /dev/null +++ b/src/shared/discovered-feature.ts @@ -0,0 +1,5 @@ +/** Features discovered by the feature-discovery subagent */ +export enum DiscoveredFeature { + Stripe = 'stripe', + LLM = 'llm', +} diff --git a/src/ui/agent-progress.ts b/src/ui/agent-progress.ts index c3525af7a..4f398fc0a 100644 --- a/src/ui/agent-progress.ts +++ b/src/ui/agent-progress.ts @@ -54,6 +54,9 @@ export function createUiReducer(ui: WizardUI): (event: AgentProgress) => void { case 'completion': ui.setOutroData(event.outro); break; + case 'activity': + // Step lines belong to the caller that asked for them, not the run UI. + break; default: { const unhandled: never = event; throw new Error(