diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 634b0fd..dd22304 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -537,12 +537,18 @@ importers: ../../rigs/heft-rig: dependencies: + '@microsoft/api-extractor': + specifier: ~7.57.7 + version: 7.57.8(@types/node@22.19.19) '@rushstack/eslint-config': specifier: 4.6.4 version: 4.6.4(eslint@9.39.4)(typescript@5.9.3) '@rushstack/heft': specifier: 1.2.7 version: 1.2.7(@types/node@22.19.19) + '@rushstack/heft-api-extractor-plugin': + specifier: 1.3.7 + version: 1.3.7(@rushstack/heft@1.2.7(@types/node@22.19.19))(@types/node@22.19.19) '@rushstack/heft-lint-plugin': specifier: 1.2.7 version: 1.2.7(@rushstack/heft@1.2.7(@types/node@22.19.19))(@types/node@22.19.19) @@ -884,6 +890,13 @@ packages: peerDependencies: jsep: ^0.4.0||^1.0.0 + '@microsoft/api-extractor-model@7.33.5': + resolution: {integrity: sha512-Xh4dXuusndVQqVz4nEN9xOp0DyzsKxeD2FFJkSPg4arAjDSKPcy6cAc7CaeBPA7kF2wV1fuDlo2p/bNMpVr8yg==} + + '@microsoft/api-extractor@7.57.8': + resolution: {integrity: sha512-RI0TxUGA3T0zwuyMIg86aHxAqJJNCjoFnIO/oVzyofxyxqokrXXiLenSBTVHGRyh6vUHg1mKGlT+LhKRF+oczg==} + hasBin: true + '@microsoft/tsdoc-config@0.18.1': resolution: {integrity: sha512-9brPoVdfN9k9g0dcWkFeA7IH9bbcttzDJlXvkf8b2OBzd5MueR1V2wkKBL0abn0otvmkHJC6aapBOTJDDeMCZg==} @@ -1094,6 +1107,11 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + '@rushstack/heft-api-extractor-plugin@1.3.7': + resolution: {integrity: sha512-1XOJVF40o8gY3dYxdZXYAcyh3G1uKosdfKSM+KGD+bsVk/yosOvUu71Y4CSkVhCgnjf3n1Hmg9G1pwikwJVcSQ==} + peerDependencies: + '@rushstack/heft': 1.2.7 + '@rushstack/heft-config-file@0.20.3': resolution: {integrity: sha512-kVIBNxwtgV4wPQrqk4PCcaU3DKkmDmiULkELmb7RmhYKAYqR6XyA9dnyXdu/HgmF3zPn8EnuBT8/RhlKmbF8Zg==} engines: {node: '>=10.13.0'} @@ -1121,6 +1139,14 @@ packages: '@types/node': optional: true + '@rushstack/node-core-library@5.21.0': + resolution: {integrity: sha512-LFzN+1lyWROit/P8Md6yxAth7lLYKn37oCKJHirEE2TQB25NDUM7bALf0ar+JAtwFfRCH+D+DGOA7DAzIi2r+g==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + '@rushstack/operation-graph@0.6.3': resolution: {integrity: sha512-HuC0N33aZ82p/eLMKpme8fzhY+L3JMejbRc7fUcli9VT5sXrgv0VOQBqWf6XeXnzoyhCSZCqrKlGPJAAb/R5Rg==} peerDependencies: @@ -1148,12 +1174,23 @@ packages: '@types/node': optional: true + '@rushstack/terminal@0.22.4': + resolution: {integrity: sha512-fhtLjnXCc/4WleVbVl6aoc7jcWnU6yqjS1S8WoaNREG3ycu/viZ9R/9QM7Y/b4CDvcXoiDyMNIay7JMwBptM3g==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + '@rushstack/tree-pattern@0.4.1': resolution: {integrity: sha512-eFuLBUWUfWQ42u5i25qO1VpTOg6nW2PXaLVwpmjm5tHpPREht0k0L2jsYht8iVvQ732odEeVnkXVcf2nIwbGxA==} '@rushstack/ts-command-line@5.3.3': resolution: {integrity: sha512-c+ltdcvC7ym+10lhwR/vWiOhsrm/bP3By2VsFcs5qTKv+6tTmxgbVrtJ5NdNjANiV5TcmOZgUN+5KYQ4llsvEw==} + '@rushstack/ts-command-line@5.3.4': + resolution: {integrity: sha512-MLkVKVEN6/2clKTrjN2B2KqKCuPxRwnNsWY7a+FCAq2EMdkj10cM8YgiBSMeGFfzM0mDMzargpHNnNzaBi9Whg==} + '@scelar/nodepod@1.7.4': resolution: {integrity: sha512-ItC2jeSbQczmCt7vfFOFAWtvz5lxDnMdqArXLfSslYlxqudbawC7NwY1MXYaSI1WDieFcB5UThbgmOfQy9DTEg==} engines: {node: '>=20.0.0'} @@ -1711,6 +1748,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + dnd-core@14.0.1: resolution: {integrity: sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==} @@ -2398,6 +2439,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + loglevel@1.9.2: resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} engines: {node: '>= 0.6.0'} @@ -2465,6 +2509,10 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} + minimatch@10.2.3: + resolution: {integrity: sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==} + engines: {node: 18 || 20 || >=22} + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -3137,6 +3185,11 @@ packages: resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} engines: {node: '>= 0.4'} + typescript@5.8.2: + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} + engines: {node: '>=14.17'} + hasBin: true + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -3593,6 +3646,33 @@ snapshots: dependencies: jsep: 1.4.0 + '@microsoft/api-extractor-model@7.33.5(@types/node@22.19.19)': + dependencies: + '@microsoft/tsdoc': 0.16.0 + '@microsoft/tsdoc-config': 0.18.1 + '@rushstack/node-core-library': 5.21.0(@types/node@22.19.19) + transitivePeerDependencies: + - '@types/node' + + '@microsoft/api-extractor@7.57.8(@types/node@22.19.19)': + dependencies: + '@microsoft/api-extractor-model': 7.33.5(@types/node@22.19.19) + '@microsoft/tsdoc': 0.16.0 + '@microsoft/tsdoc-config': 0.18.1 + '@rushstack/node-core-library': 5.21.0(@types/node@22.19.19) + '@rushstack/rig-package': 0.7.2 + '@rushstack/terminal': 0.22.4(@types/node@22.19.19) + '@rushstack/ts-command-line': 5.3.4(@types/node@22.19.19) + diff: 8.0.4 + lodash: 4.17.23 + minimatch: 10.2.3 + resolve: 1.22.12 + semver: 7.5.4 + source-map: 0.6.1 + typescript: 5.8.2 + transitivePeerDependencies: + - '@types/node' + '@microsoft/tsdoc-config@0.18.1': dependencies: '@microsoft/tsdoc': 0.16.0 @@ -3786,6 +3866,14 @@ snapshots: - supports-color - typescript + '@rushstack/heft-api-extractor-plugin@1.3.7(@rushstack/heft@1.2.7(@types/node@22.19.19))(@types/node@22.19.19)': + dependencies: + '@rushstack/heft': 1.2.7(@types/node@22.19.19) + '@rushstack/node-core-library': 5.20.3(@types/node@22.19.19) + semver: 7.5.4 + transitivePeerDependencies: + - '@types/node' + '@rushstack/heft-config-file@0.20.3(@types/node@22.19.19)': dependencies: '@rushstack/node-core-library': 5.20.3(@types/node@22.19.19) @@ -3846,6 +3934,19 @@ snapshots: optionalDependencies: '@types/node': 22.19.19 + '@rushstack/node-core-library@5.21.0(@types/node@22.19.19)': + dependencies: + ajv: 8.18.0 + ajv-draft-04: 1.0.0(ajv@8.18.0) + ajv-formats: 3.0.1 + fs-extra: 11.3.5 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.12 + semver: 7.5.4 + optionalDependencies: + '@types/node': 22.19.19 + '@rushstack/operation-graph@0.6.3(@types/node@22.19.19)': dependencies: '@rushstack/node-core-library': 5.20.3(@types/node@22.19.19) @@ -3870,6 +3971,14 @@ snapshots: optionalDependencies: '@types/node': 22.19.19 + '@rushstack/terminal@0.22.4(@types/node@22.19.19)': + dependencies: + '@rushstack/node-core-library': 5.21.0(@types/node@22.19.19) + '@rushstack/problem-matcher': 0.2.1(@types/node@22.19.19) + supports-color: 8.1.1 + optionalDependencies: + '@types/node': 22.19.19 + '@rushstack/tree-pattern@0.4.1': {} '@rushstack/ts-command-line@5.3.3(@types/node@22.19.19)': @@ -3881,6 +3990,15 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@rushstack/ts-command-line@5.3.4(@types/node@22.19.19)': + dependencies: + '@rushstack/terminal': 0.22.4(@types/node@22.19.19) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + '@scelar/nodepod@1.7.4(vite@7.3.5(@types/node@22.19.19)(terser@5.48.0))': dependencies: acorn: 8.16.0 @@ -4513,6 +4631,8 @@ snapshots: depd@2.0.0: {} + diff@8.0.4: {} + dnd-core@14.0.1: dependencies: '@react-dnd/asap': 4.0.1 @@ -5339,6 +5459,8 @@ snapshots: lodash.merge@4.6.2: {} + lodash@4.17.23: {} + loglevel@1.9.2: {} loose-envify@1.4.0: @@ -5365,7 +5487,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.5.4 + semver: 7.8.1 marked@14.0.0: {} @@ -5392,6 +5514,10 @@ snapshots: dependencies: mime-db: 1.54.0 + minimatch@10.2.3: + dependencies: + brace-expansion: 5.0.6 + minimatch@10.2.5: dependencies: brace-expansion: 5.0.6 @@ -6132,6 +6258,8 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 + typescript@5.8.2: {} + typescript@5.9.3: {} unbox-primitive@1.1.0: diff --git a/common/reviews/acp-agent.public.api.md b/common/reviews/acp-agent.public.api.md new file mode 100644 index 0000000..50d129f --- /dev/null +++ b/common/reviews/acp-agent.public.api.md @@ -0,0 +1,7 @@ +## Public API Report File for "@fledgling/acp-agent" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +``` diff --git a/common/reviews/acp-host-log.public.api.md b/common/reviews/acp-host-log.public.api.md new file mode 100644 index 0000000..1f80860 --- /dev/null +++ b/common/reviews/acp-host-log.public.api.md @@ -0,0 +1,7 @@ +## Public API Report File for "@fledgling/acp-host-log" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +``` diff --git a/common/reviews/agent-core.public.api.md b/common/reviews/agent-core.public.api.md new file mode 100644 index 0000000..c7a41ea --- /dev/null +++ b/common/reviews/agent-core.public.api.md @@ -0,0 +1,207 @@ +## Public API Report File for "@fledgling/agent-core" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import * as acp from '@agentclientprotocol/sdk'; +import type { CoreMessage } from 'ai'; +import { LanguageModel } from 'ai'; +import type { PromptRequest } from '@agentclientprotocol/sdk'; +import type { SessionErrorEvent } from '@fledgling/common'; +import type { SessionEvent } from '@fledgling/common'; +import { ToolSet } from 'ai'; + +// @public +export class AiSdkModelTurnRunner implements IModelTurnRunner { + constructor(options: AiSdkModelTurnRunnerOptions); + runModelTurn(request: ModelTurnRequest): ModelTurnResult; +} + +// @public +export interface AiSdkModelTurnRunnerOptions { + readonly maxSteps?: number; + readonly resolveModel: () => LanguageModel; + readonly resolveSystemPrompt?: () => string | undefined; + readonly resolveToolChoice?: (tools: ToolSet) => AiSdkToolChoice | undefined; +} + +// @public +export type AiSdkToolChoice = "auto" | { + readonly type: "tool"; + readonly toolName: string; +}; + +// @public +export function closeClients(clients: readonly IClosable[], reason: string, sessionId?: string | undefined, logger?: IRuntimeLogger): Promise; + +// @public +export function createPromptRpcError(error: unknown, phase: "model_start" | "model_stream"): Error; + +// @public +export function extractContextHint(output: unknown): unknown; + +// @public +export function extractPromptText(params: PromptRequest): string; + +// @public +export class FledglingAgent implements acp.Agent { + constructor(connection: acp.AgentSideConnection, dependencies: FledglingAgentDependencies); + authenticate(_params: acp.AuthenticateRequest): Promise; + cancel(_params: acp.CancelNotification): Promise; + closeAllSessions(reason: string): Promise; + initialize(_params: acp.InitializeRequest): Promise; + loadSession(params: acp.LoadSessionRequest): Promise; + newSession(params: acp.NewSessionRequest): Promise; + prompt(params: acp.PromptRequest): Promise; + setSessionMode(_params: acp.SetSessionModeRequest): Promise; +} + +// @public +export interface FledglingAgentDependencies { + readonly debugStream?: boolean; + readonly logger?: IRuntimeLogger; + readonly modelTurnRunner: IModelTurnRunner; + readonly sessionManager: ISessionManager; + readonly toolProvider: IToolProvider; +} + +// @public +export function formatPromptErrorKind(kind: PromptErrorKind): string; + +// @public +export interface IClosable { + close(): Promise | void; +} + +// @public +export interface IModelTurnRunner { + runModelTurn(request: ModelTurnRequest): ModelTurnResult; +} + +// @public +export interface IRuntimeLogger { + debug?(record: unknown): void; + error(record: unknown): void; + warn(record: unknown): void; +} + +// @public +export interface ISessionManager { + appendEvent(event: SessionEvent): Promise; + createEventBase(sessionId: string): SessionEventBase; + createSessionId(): string; + loadEvents(sessionId: string): Promise; +} + +// @public +export interface IToolProvider { + createSessionTools(request: { + readonly cwd: string | undefined; + readonly mcpServers: acp.McpServer[]; + }): Promise; +} + +// @public +export interface McpCloseFailureRecord { + readonly error: string; + readonly event: "mcp_close_failed"; + readonly level: "warn"; + readonly reason: string; + readonly sessionId: string | undefined; +} + +// @public +export function messageContentToText(content: CoreMessage["content"]): string; + +// @public +export type ModelStreamPart = { + readonly type: "text-delta"; + readonly text: string; +} | { + readonly type: "tool-call"; + readonly toolCallId: string; + readonly toolName: string; + readonly input: unknown; +} | { + readonly type: "tool-result"; + readonly toolCallId: string; + readonly output: unknown; +} | { + readonly type: "tool-error"; + readonly toolCallId: string; + readonly error: unknown; +}; + +// @public +export interface ModelTurnRequest { + readonly abortSignal: AbortSignal; + readonly messages: CoreMessage[]; + readonly tools: ToolSet; +} + +// @public +export interface ModelTurnResult { + readonly fullStream: AsyncIterable; +} + +// @public +export interface NormalizedPromptError { + readonly errorCode: string | undefined; + readonly errorName: string | undefined; + readonly kind: PromptErrorKind; + readonly message: string; + readonly phase: PromptErrorPhase; + readonly recoverable: boolean; +} + +// @public +export function normalizePromptError(error: unknown, kind: PromptErrorKind, phase: PromptErrorPhase): NormalizedPromptError; + +// @public +export interface PromptAbortControllerLike { + abort(): void; +} + +// @public +export type PromptErrorKind = SessionErrorEvent["kind"]; + +// @public +export type PromptErrorPhase = SessionErrorEvent["phase"]; + +// @public +export function sanitizeErrorMessage(message: string): string; + +// @public +export function serializeError(error: unknown): string; + +// @public +export class SessionCleanup { + constructor(getSessions: () => Iterable, clearSessions: () => void, logger?: IRuntimeLogger); + closeAll(reason: string): Promise; + closeSession(session: SessionCleanupState, reason: string): Promise; +} + +// @public +export interface SessionCleanupState { + readonly clients: readonly IClosable[]; + readonly id: string; + readonly pendingPrompt: PromptAbortControllerLike | undefined; +} + +// @public +export type SessionEventBase = Pick; + +// @public +export interface SessionTools { + readonly clients: IClosable[]; + readonly tools: ToolSet; +} + +// @public +export function stringifyToolOutput(output: unknown): string; + +// @public +export function toRawObject(value: unknown): Record; + +``` diff --git a/common/reviews/common.public.api.md b/common/reviews/common.public.api.md new file mode 100644 index 0000000..c8fca9f --- /dev/null +++ b/common/reviews/common.public.api.md @@ -0,0 +1,129 @@ +## Public API Report File for "@fledgling/common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public +export type AssistantMessageEvent = SessionEventBase & { + readonly type: "message.assistant"; + readonly text: string; +}; + +// @public +export type CompactionEvent = SessionEventBase & { + readonly type: "context.compacted"; + readonly targetTokens: number; + readonly estimatedTokensBefore: number; + readonly estimatedTokensAfter: number; + readonly droppedEventIds: readonly string[]; +}; + +// @public +export const CONTEXT_HINT_META_KEY: string; + +// @public +export interface ContextHint { + readonly contentHash?: string; + readonly identity?: string; + readonly kind: ContextKind; + readonly placement: ContextPlacement; + readonly priority?: number; + readonly retention: ContextRetention; + readonly routingTags?: string[]; + readonly tokenEstimate?: number; +} + +// @public +export type ContextKind = "ephemeral_observation" | "durable_resource" | "workspace_map" | "diagnostic" | "command_output" | "user_memory"; + +// @public +export interface ContextMessage { + readonly content: string; + readonly role: "user" | "assistant" | "system"; +} + +// @public +export type ContextPlacement = "stable_prefix" | "session_context" | "turn_context" | "latest_evidence" | "do_not_inline"; + +// @public +export type ContextRetention = "discard_after_turn" | "summarize_after_turn" | "retain_until_changed" | "retain_for_session"; + +// @public +export function estimateTokens(text: string): number; + +// @public +export function hashText(text: string): string; + +// @public +export function inferRoutingTag(filePath: string): string; + +// @public +export type SessionCreatedEvent = SessionEventBase & { + readonly type: "session.created"; + readonly cwd: string | undefined; + readonly mcpServers: readonly string[]; +}; + +// @public +export type SessionErrorEvent = SessionEventBase & { + readonly type: "session.error"; + readonly kind: "model_start_failed" | "model_stream_failed" | "prompt_cleanup_failed"; + readonly phase: "model_start" | "model_stream" | "cleanup"; + readonly message: string; + readonly recoverable: boolean; + readonly assistantTextPersisted: boolean; + readonly errorName: string | undefined; + readonly errorCode: string | undefined; +}; + +// @public +export type SessionEvent = SessionCreatedEvent | SessionLoadedEvent | UserMessageEvent | AssistantMessageEvent | ToolCallEvent | ToolResultEvent | SessionErrorEvent | CompactionEvent; + +// @public +export interface SessionEventBase { + readonly eventId: string; + readonly sessionId: string; + readonly timestamp: string; +} + +// @public +export type SessionLoadedEvent = SessionEventBase & { + readonly type: "session.loaded"; + readonly cwd: string | undefined; + readonly source: string; +}; + +// @public +export const TOOL_META_KEY: string; + +// @public +export type ToolCallEvent = SessionEventBase & { + readonly type: "tool.call"; + readonly toolCallId: string; + readonly toolName: string; + readonly title: string; + readonly rawInput: unknown; +}; + +// @public +export type ToolResultEvent = SessionEventBase & { + readonly type: "tool.result"; + readonly toolCallId: string; + readonly toolName: string | undefined; + readonly status: "completed" | "failed"; + readonly text: string; + readonly rawOutput: unknown; + readonly contextHint: unknown; +}; + +// @public +export type UserMessageEvent = SessionEventBase & { + readonly type: "message.user"; + readonly text: string; +}; + +// @public +export type VolatileEventType = "tool.call" | "tool.result"; + +``` diff --git a/common/reviews/context-builder.public.api.md b/common/reviews/context-builder.public.api.md new file mode 100644 index 0000000..81a8f2e --- /dev/null +++ b/common/reviews/context-builder.public.api.md @@ -0,0 +1,128 @@ +## Public API Report File for "@fledgling/context-builder" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ContextMessage } from '@fledgling/common'; +import type { SessionEvent } from '@fledgling/common'; + +// @public +export function buildCompactedContext(options: BuildCompactedContextOptions): BuiltContext; + +// @public +export interface BuildCompactedContextOptions { + readonly retainedEvents: readonly SessionEvent[]; + readonly summary: string; +} + +// @public +export function buildContext(events: readonly SessionEvent[], _options: BuildReplayContextOptions): BuiltContext; + +// @public +export interface BuildReplayContextOptions { + readonly mode: "replay"; +} + +// @public +export interface BuiltContext { + readonly dropped: DroppedEvent[]; + readonly events: SessionEvent[]; + readonly messages: ContextMessage[]; + readonly tokenEstimate: number; +} + +// @public +export function compactContext(events: readonly SessionEvent[], options: CompactContextOptions): Promise; + +// @public +export interface CompactContextOptions extends PrepareCompactionOptions { + readonly compact: CompactionFunction; + readonly instructions?: string; + readonly targetTokens?: number; +} + +// @public +export interface CompactedContextResult extends BuiltContext { + readonly prepared: PreparedCompaction; + readonly summary: string | undefined; +} + +// @public +export type CompactionFunction = (request: CompactionModelRequest) => Promise; + +// @public +export interface CompactionModelRequest { + readonly events: SessionEvent[]; + readonly inputTokenEstimate: number; + readonly instructions: string; + readonly targetTokens: number | undefined; +} + +// @public +export interface CompactionModelResult { + readonly summary: string; + readonly tokenEstimate?: number; +} + +// @public +export const DEFAULT_COMPACTION_INSTRUCTIONS: string; + +// @public +export interface DroppedEvent { + readonly eventId: string; + readonly reason: string; + readonly type: SessionEvent["type"]; +} + +// @public +export function estimateEventTokens(event: SessionEvent): number; + +// @public +export function estimateMessagesTokens(messages: readonly ContextMessage[]): number; + +// @public +export function estimateMessageTokens(message: ContextMessage): number; + +// @public +export function estimateTextTokens(text: string): number; + +// @public +export function eventsToMessages(events: readonly SessionEvent[]): ContextMessage[]; + +// @public +export function prepareCompaction(events: readonly SessionEvent[], options: PrepareCompactionOptions): PreparedCompaction; + +// @public +export interface PrepareCompactionOptions { + readonly keepLatestTurns: number; + readonly maxCompactionInputTokens?: number; + readonly prune?: PruneEventsOptions; +} + +// @public +export interface PreparedCompaction { + readonly compactionInputTokenEstimate: number; + readonly dropped: DroppedEvent[]; + readonly eventsToCompact: SessionEvent[]; + readonly needsCompaction: boolean; + readonly retainedEvents: SessionEvent[]; + readonly retainedTokenEstimate: number; +} + +// @public +export interface PrunedEvents { + readonly dropped: DroppedEvent[]; + readonly events: SessionEvent[]; +} + +// @public +export interface PruneEventsOptions { + readonly keepLatestToolCallsPerGroup?: number; + readonly keepLatestToolResultsPerGroup?: number; +} + +// @public +export function pruneOldVolatileEvents(events: readonly SessionEvent[], options?: PruneEventsOptions): PrunedEvents; + +``` diff --git a/common/reviews/mcp-workspace-browser.public.api.md b/common/reviews/mcp-workspace-browser.public.api.md new file mode 100644 index 0000000..9b2cd78 --- /dev/null +++ b/common/reviews/mcp-workspace-browser.public.api.md @@ -0,0 +1,81 @@ +## Public API Report File for "@fledgling/mcp-workspace-browser" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; + +// @public +export function appendSearchMatches(matches: SearchMatch[], path: string, content: string, query: string): void; + +// @public +export interface CommandResult { + readonly exitCode: number; + readonly stderr: string; + readonly stdout: string; + readonly timedOut: boolean; + readonly truncated: boolean; +} + +// @public +export function createWebWorkspaceSidecar(runtime: IWorkspaceRuntime): Promise; + +// @public +export interface IWorkspaceRuntime { + dispose?(): Promise | void; + listDirectory(path: string): Promise; + readFile(path: string): Promise; + runCommand(command: string, cwd: string, timeoutMs: number, maxOutputBytes: number): Promise; + searchText(query: string, path: string): Promise; + writeFile(path: string, content: string): Promise; +} + +// @public +export function joinAbsoluteWorkspacePath(parent: string, child: string): string; + +// @public +export function joinWorkspacePath(parent: string, child: string): string; + +// @public +export function normalizeAbsoluteWorkspacePath(path: string): string; + +// @public +export function normalizeWorkspacePath(path: string): string; + +// @public +export function registerWebWorkspaceTools(server: McpServer, runtime: IWorkspaceRuntime): void; + +// @public +export interface SearchMatch { + readonly line: number; + readonly path: string; + readonly text: string; +} + +// @public +export interface WebWorkspaceSidecar { + readonly clientTransport: InMemoryTransport; + close(): Promise; +} + +// @public +export interface WorkspaceDirectoryEntry { + readonly name: string; + readonly path: string; + readonly type: "directory"; +} + +// @public +export type WorkspaceEntry = WorkspaceFileEntry | WorkspaceDirectoryEntry; + +// @public +export interface WorkspaceFileEntry { + readonly name: string; + readonly path: string; + readonly sizeBytes: number; + readonly type: "file"; +} + +``` diff --git a/common/reviews/mcp-workspace-webcontainer.public.api.md b/common/reviews/mcp-workspace-webcontainer.public.api.md new file mode 100644 index 0000000..2308979 --- /dev/null +++ b/common/reviews/mcp-workspace-webcontainer.public.api.md @@ -0,0 +1,26 @@ +## Public API Report File for "@fledgling/mcp-workspace-webcontainer" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { CommandResult } from '@fledgling/mcp-workspace-browser'; +import { IWorkspaceRuntime } from '@fledgling/mcp-workspace-browser'; +import { SearchMatch } from '@fledgling/mcp-workspace-browser'; +import type { WebContainer } from '@webcontainer/api'; +import { WorkspaceEntry } from '@fledgling/mcp-workspace-browser'; + +// @public +export class WebContainerWorkspaceRuntime implements IWorkspaceRuntime { + constructor(container: WebContainer); + listDirectory(path: string): Promise; + readFile(path: string): Promise; + runCommand(command: string, cwd: string, timeoutMs: number, maxOutputBytes: number): Promise; + searchText(query: string, path: string): Promise; + writeFile(path: string, content: string): Promise; +} + + +export * from "@fledgling/mcp-workspace-browser"; + +``` diff --git a/common/reviews/mcp-workspace.public.api.md b/common/reviews/mcp-workspace.public.api.md new file mode 100644 index 0000000..71c714d --- /dev/null +++ b/common/reviews/mcp-workspace.public.api.md @@ -0,0 +1,7 @@ +## Public API Report File for "@fledgling/mcp-workspace" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +``` diff --git a/common/reviews/session-file-system.public.api.md b/common/reviews/session-file-system.public.api.md new file mode 100644 index 0000000..0017ba3 --- /dev/null +++ b/common/reviews/session-file-system.public.api.md @@ -0,0 +1,23 @@ +## Public API Report File for "@fledgling/session-file-system" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ISessionManager } from '@fledgling/agent-core'; +import type { SessionEvent } from '@fledgling/common'; +import type { SessionEventBase } from '@fledgling/agent-core'; + +// @public +export function defaultSessionStoreRoot(): string; + +// @public +export class FileSystemSessionManager implements ISessionManager { + constructor(root?: string, sessionFile?: string | undefined); + appendEvent(event: SessionEvent): Promise; + createEventBase(sessionId: string): SessionEventBase; + createSessionId(): string; + loadEvents(sessionId: string): Promise; +} + +``` diff --git a/common/reviews/session-local-storage.public.api.md b/common/reviews/session-local-storage.public.api.md new file mode 100644 index 0000000..8a01b10 --- /dev/null +++ b/common/reviews/session-local-storage.public.api.md @@ -0,0 +1,26 @@ +## Public API Report File for "@fledgling/session-local-storage" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ISessionManager } from '@fledgling/agent-core'; +import type { SessionEvent } from '@fledgling/common'; +import type { SessionEventBase } from '@fledgling/agent-core'; + +// @public +export class LocalStorageSessionManager implements ISessionManager { + constructor(options?: LocalStorageSessionManagerOptions); + appendEvent(event: SessionEvent): Promise; + createEventBase(sessionId: string): SessionEventBase; + createSessionId(): string; + loadEvents(sessionId: string): Promise; +} + +// @public +export interface LocalStorageSessionManagerOptions { + readonly keyPrefix?: string; + readonly storage?: Storage; +} + +``` diff --git a/common/reviews/session-log.public.api.md b/common/reviews/session-log.public.api.md new file mode 100644 index 0000000..1c9f647 --- /dev/null +++ b/common/reviews/session-log.public.api.md @@ -0,0 +1,21 @@ +## Public API Report File for "@fledgling/session-log" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { SessionEvent } from '@fledgling/common'; + +// @public +export function defaultSessionStoreRoot(): string; + +// @public +export class SessionStore { + constructor(root?: string, sessionFile?: string | undefined); + append(event: SessionEvent): Promise; + createEventBase(sessionId: string): Pick; + createId(): string; + load(sessionId: string): Promise; +} + +``` diff --git a/common/reviews/session-memory.public.api.md b/common/reviews/session-memory.public.api.md new file mode 100644 index 0000000..d4ec0cb --- /dev/null +++ b/common/reviews/session-memory.public.api.md @@ -0,0 +1,19 @@ +## Public API Report File for "@fledgling/session-memory" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ISessionManager } from '@fledgling/agent-core'; +import type { SessionEvent } from '@fledgling/common'; +import type { SessionEventBase } from '@fledgling/agent-core'; + +// @public +export class MemorySessionManager implements ISessionManager { + appendEvent(event: SessionEvent): Promise; + createEventBase(sessionId: string): SessionEventBase; + createSessionId(): string; + loadEvents(sessionId: string): Promise; +} + +``` diff --git a/common/reviews/tools-mcp-node.public.api.md b/common/reviews/tools-mcp-node.public.api.md new file mode 100644 index 0000000..48356af --- /dev/null +++ b/common/reviews/tools-mcp-node.public.api.md @@ -0,0 +1,40 @@ +## Public API Report File for "@fledgling/tools-mcp-node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import * as acp from '@agentclientprotocol/sdk'; +import { IToolProvider } from '@fledgling/agent-core'; +import { SessionTools } from '@fledgling/agent-core'; + +// @public +export interface FledglingConfig { + readonly mcpServers?: Record; +} + +// @public +export type McpServerConfig = { + readonly type: "firstPartyWorkspace"; +} | { + readonly type: "stdio"; + readonly command: string; + readonly args?: string[]; + readonly cwd?: string; + readonly env?: Record; +} | { + readonly type: "http" | "sse"; + readonly url: string; + readonly headers?: Record; +}; + +// @public +export class NodeMcpToolProvider implements IToolProvider { + constructor(configPromise?: Promise); + createSessionTools(request: { + readonly cwd: string | undefined; + readonly mcpServers: acp.McpServer[]; + }): Promise; +} + +``` diff --git a/common/reviews/web-agent.public.api.md b/common/reviews/web-agent.public.api.md new file mode 100644 index 0000000..2a7e9ef --- /dev/null +++ b/common/reviews/web-agent.public.api.md @@ -0,0 +1,47 @@ +## Public API Report File for "@fledgling/web-agent" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type * as acp from '@agentclientprotocol/sdk'; +import { FledglingAgent } from '@fledgling/agent-core'; +import { FledglingAgentDependencies } from '@fledgling/agent-core'; +import { IModelTurnRunner } from '@fledgling/agent-core'; +import type { IToolProvider } from '@fledgling/agent-core'; +import { IWorkspaceRuntime } from '@fledgling/mcp-workspace-browser'; +import type { SessionTools } from '@fledgling/agent-core'; +import type { WebWorkspaceSidecar } from '@fledgling/mcp-workspace-browser'; + +// @public +export function createWebAgent(connection: acp.AgentSideConnection, options: WebAgentOptions): FledglingAgent; + +// @public +export function createWebAgentDependencies(options: WebAgentOptions): FledglingAgentDependencies; + +// @public +export interface WebAgentOptions { + readonly modelTurnRunner: IModelTurnRunner; + readonly storage?: Storage; + readonly workspaceRuntime: IWorkspaceRuntime; +} + +// @public +export class WebMcpToolProvider implements IToolProvider { + constructor(options: WebMcpToolProviderOptions); + createSessionTools(_request: { + readonly cwd: string | undefined; + readonly mcpServers: acp.McpServer[]; + }): Promise; +} + +// @public +export interface WebMcpToolProviderOptions { + readonly createSidecar: () => Promise; +} + + +export * from "@fledgling/agent-core"; +export * from "@fledgling/mcp-workspace-browser"; + +``` diff --git a/common/reviews/workspace-nodepod.public.api.md b/common/reviews/workspace-nodepod.public.api.md new file mode 100644 index 0000000..076307f --- /dev/null +++ b/common/reviews/workspace-nodepod.public.api.md @@ -0,0 +1,58 @@ +## Public API Report File for "@fledgling/workspace-nodepod" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { CommandResult } from '@fledgling/mcp-workspace-browser'; +import { IWorkspaceRuntime } from '@fledgling/mcp-workspace-browser'; +import { Nodepod } from '@scelar/nodepod'; +import { NodepodOptions } from '@scelar/nodepod'; +import { NodepodProcess } from '@scelar/nodepod'; +import { SearchMatch } from '@fledgling/mcp-workspace-browser'; +import { WorkspaceEntry } from '@fledgling/mcp-workspace-browser'; + +// @public +export function createNodepodWorkspaceRuntime(options?: NodepodWorkspaceRuntimeOptions): Promise; + +// @public +export interface NodepodLike { + readonly fs: Nodepod["fs"]; + run?: (command: string, options?: { + readonly cwd?: string; + readonly signal?: AbortSignal; + readonly onStdout?: (chunk: string) => void; + readonly onStderr?: (chunk: string) => void; + }) => Promise<{ + readonly stdout: string; + readonly stderr: string; + readonly exitCode: number; + }>; + spawn: Nodepod["spawn"]; + teardown(): void; +} + +export { NodepodOptions } + +export { NodepodProcess } + +// @public +export class NodepodWorkspaceRuntime implements IWorkspaceRuntime { + constructor(nodepod: NodepodLike); + dispose(): void; + listDirectory(path: string): Promise; + readFile(path: string): Promise; + runCommand(command: string, cwd: string, timeoutMs: number, maxOutputBytes: number): Promise; + searchText(query: string, path: string): Promise; + writeFile(path: string, content: string): Promise; +} + +// @public +export interface NodepodWorkspaceRuntimeOptions { + readonly files?: Record; + readonly onServerReady?: (port: number, url: string) => void; + readonly serviceWorker?: boolean; + readonly workdir?: string; +} + +``` diff --git a/packages/acp-agent/config/api-extractor.json b/packages/acp-agent/config/api-extractor.json new file mode 100644 index 0000000..abf87f1 --- /dev/null +++ b/packages/acp-agent/config/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "@fledgling/heft-rig/profiles/default/config/api-extractor-base.json" +} diff --git a/packages/acp-agent/src/index.ts b/packages/acp-agent/src/index.ts index cfb5ea2..e514139 100644 --- a/packages/acp-agent/src/index.ts +++ b/packages/acp-agent/src/index.ts @@ -1,3 +1,14 @@ +/** + * ACP stdio executable for the Fledgling agent. + * + * The package starts an Agent Client Protocol connection over standard input and + * output, wires it to the default Fledgling agent dependencies, and registers + * process lifecycle cleanup for active sessions. + * + * @packageDocumentation + */ +export {}; + import { Readable, Writable } from "node:stream"; import * as acp from "@agentclientprotocol/sdk"; diff --git a/packages/acp-host-log/config/api-extractor.json b/packages/acp-host-log/config/api-extractor.json new file mode 100644 index 0000000..abf87f1 --- /dev/null +++ b/packages/acp-host-log/config/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "@fledgling/heft-rig/profiles/default/config/api-extractor-base.json" +} diff --git a/packages/acp-host-log/src/index.ts b/packages/acp-host-log/src/index.ts index 1ee8d67..fd8737d 100644 --- a/packages/acp-host-log/src/index.ts +++ b/packages/acp-host-log/src/index.ts @@ -1,4 +1,11 @@ #!/usr/bin/env node +/** + * Logging ACP host CLI for exercising Fledgling sessions through the real ACP client side. + * + * @packageDocumentation + */ +export {}; + import { spawn, type ChildProcessByStdio } from "node:child_process"; import { existsSync } from "node:fs"; import { readFile } from "node:fs/promises"; diff --git a/packages/agent-core/config/api-extractor.json b/packages/agent-core/config/api-extractor.json new file mode 100644 index 0000000..abf87f1 --- /dev/null +++ b/packages/agent-core/config/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "@fledgling/heft-rig/profiles/default/config/api-extractor-base.json" +} diff --git a/packages/agent-core/src/agent.ts b/packages/agent-core/src/agent.ts index 2ee85f5..1b3fbfd 100644 --- a/packages/agent-core/src/agent.ts +++ b/packages/agent-core/src/agent.ts @@ -29,12 +29,14 @@ interface SessionState { promptQueue: Promise; } +/** ACP agent implementation that manages sessions, model turns, tools, and event persistence. */ export class FledglingAgent implements acp.Agent { readonly #connection: acp.AgentSideConnection; readonly #sessions: Map = new Map(); readonly #dependencies: FledglingAgentDependencies; readonly #sessionCleanup: SessionCleanup; + /** Creates an ACP agent bound to a connection and host-provided dependencies. */ public constructor(connection: acp.AgentSideConnection, dependencies: FledglingAgentDependencies) { this.#connection = connection; this.#dependencies = dependencies; @@ -45,6 +47,7 @@ export class FledglingAgent implements acp.Agent { ); } + /** Reports the ACP protocol version and agent capabilities. */ public async initialize(_params: acp.InitializeRequest): Promise { return { protocolVersion: acp.PROTOCOL_VERSION, @@ -58,6 +61,7 @@ export class FledglingAgent implements acp.Agent { }; } + /** Creates a new ACP session with tools for the requested working directory and MCP servers. */ public async newSession(params: acp.NewSessionRequest): Promise { const { clients, tools } = await this.#dependencies.toolProvider.createSessionTools({ cwd: params.cwd, @@ -87,6 +91,7 @@ export class FledglingAgent implements acp.Agent { }; } + /** Loads an existing session from persisted events and replays visible message history. */ public async loadSession(params: acp.LoadSessionRequest): Promise { const events = await this.#dependencies.sessionManager.loadEvents(params.sessionId); const context = buildContext(events, { mode: "replay" }); @@ -122,14 +127,17 @@ export class FledglingAgent implements acp.Agent { return {}; } + /** Handles ACP authentication requests. */ public async authenticate(_params: acp.AuthenticateRequest): Promise { return {}; } + /** Accepts ACP session mode updates. */ public async setSessionMode(_params: acp.SetSessionModeRequest): Promise { return {}; } + /** Queues and runs a prompt against an active session. */ public async prompt(params: acp.PromptRequest): Promise { const session = this.#sessions.get(params.sessionId); if (!session) { @@ -376,10 +384,12 @@ export class FledglingAgent implements acp.Agent { }); } + /** Cancels the active prompt for the requested session, when one is running. */ public async cancel(_params: acp.CancelNotification): Promise { this.#sessions.get(_params.sessionId)?.pendingPrompt?.abort(); } + /** Closes all active sessions and their backing clients. */ public closeAllSessions(reason: string): Promise { return this.#sessionCleanup.closeAll(reason); } diff --git a/packages/agent-core/src/ai-sdk-model-turn-runner.ts b/packages/agent-core/src/ai-sdk-model-turn-runner.ts index 11d7e0d..08a04ab 100644 --- a/packages/agent-core/src/ai-sdk-model-turn-runner.ts +++ b/packages/agent-core/src/ai-sdk-model-turn-runner.ts @@ -2,22 +2,34 @@ import { stepCountIs, streamText, type LanguageModel, type ToolSet } from "ai"; import type { IModelTurnRunner, ModelStreamPart, ModelTurnRequest, ModelTurnResult } from "./interfaces.js"; +/** Tool choice setting accepted by the AI SDK model turn runner. */ export type AiSdkToolChoice = "auto" | { readonly type: "tool"; readonly toolName: string }; +/** Options for creating an AI SDK backed model turn runner. */ export interface AiSdkModelTurnRunnerOptions { + /** Resolves the language model used for each turn. */ readonly resolveModel: () => LanguageModel; + + /** Resolves an optional system prompt for each turn. */ readonly resolveSystemPrompt?: () => string | undefined; + + /** Resolves the AI SDK tool choice for the current tool set. */ readonly resolveToolChoice?: (tools: ToolSet) => AiSdkToolChoice | undefined; + + /** Maximum number of model steps allowed in one turn. */ readonly maxSteps?: number; } +/** Runs prompt turns through the Vercel AI SDK streaming API. */ export class AiSdkModelTurnRunner implements IModelTurnRunner { readonly #options: AiSdkModelTurnRunnerOptions; + /** Creates a model turn runner with lazily resolved AI SDK options. */ public constructor(options: AiSdkModelTurnRunnerOptions) { this.#options = options; } + /** Starts a model turn and returns the normalized full stream. */ public runModelTurn(request: ModelTurnRequest): ModelTurnResult { const result = streamText({ model: this.#options.resolveModel(), diff --git a/packages/agent-core/src/index.ts b/packages/agent-core/src/index.ts index 0ff8418..efb8be4 100644 --- a/packages/agent-core/src/index.ts +++ b/packages/agent-core/src/index.ts @@ -1,3 +1,9 @@ +/** + * Browser-safe ACP agent loop primitives for Fledgling hosts. + * + * @packageDocumentation + */ + export * from "./ai-sdk-model-turn-runner.js"; export * from "./agent.js"; export * from "./interfaces.js"; diff --git a/packages/agent-core/src/interfaces.ts b/packages/agent-core/src/interfaces.ts index 4655833..4dcd5e2 100644 --- a/packages/agent-core/src/interfaces.ts +++ b/packages/agent-core/src/interfaces.ts @@ -2,77 +2,144 @@ import type * as acp from "@agentclientprotocol/sdk"; import type { SessionEvent } from "@fledgling/common"; import type { CoreMessage, ToolSet } from "ai"; +/** Common fields that identify and timestamp a persisted session event. */ export type SessionEventBase = Pick; +/** A resource that can be closed when an agent session ends. */ export interface IClosable { + /** Releases any resources held by the object. */ close(): Promise | void; } +/** Persists and retrieves the event stream backing ACP sessions. */ export interface ISessionManager { + /** Creates a new unique session identifier. */ createSessionId(): string; + + /** Creates the shared event metadata for a session event. */ createEventBase(sessionId: string): SessionEventBase; + + /** Appends an event to the session event store. */ appendEvent(event: SessionEvent): Promise; + + /** Loads all persisted events for a session. */ loadEvents(sessionId: string): Promise; } +/** Streaming part emitted by a model turn runner. */ export type ModelStreamPart = | { + /** Identifies assistant text output. */ readonly type: "text-delta"; + + /** Text emitted by the model for this stream part. */ readonly text: string; } | { + /** Identifies a model-requested tool call. */ readonly type: "tool-call"; + + /** Model-generated identifier for the tool call. */ readonly toolCallId: string; + + /** Name of the tool to invoke. */ readonly toolName: string; + + /** Raw tool input supplied by the model. */ readonly input: unknown; } | { + /** Identifies a successful tool result. */ readonly type: "tool-result"; + + /** Identifier of the tool call that produced this result. */ readonly toolCallId: string; + + /** Raw output returned by the tool. */ readonly output: unknown; } | { + /** Identifies a failed tool result. */ readonly type: "tool-error"; + + /** Identifier of the tool call that failed. */ readonly toolCallId: string; + + /** Error value reported by the tool invocation. */ readonly error: unknown; }; +/** Inputs supplied to a model for one prompt turn. */ export interface ModelTurnRequest { + /** Conversation history to pass to the model. */ readonly messages: CoreMessage[]; + + /** Tools available to the model for this turn. */ readonly tools: ToolSet; + + /** Abort signal that cancels model work for the prompt. */ readonly abortSignal: AbortSignal; } +/** Streaming model output for one prompt turn. */ export interface ModelTurnResult { + /** Full model event stream normalized to Fledgling stream parts. */ readonly fullStream: AsyncIterable; } +/** Runs one model turn for an agent prompt. */ export interface IModelTurnRunner { + /** Starts the model turn and returns its stream. */ runModelTurn(request: ModelTurnRequest): ModelTurnResult; } +/** Tools and closeable clients created for a session. */ export interface SessionTools { + /** Clients that should be closed when the session is closed. */ readonly clients: IClosable[]; + + /** AI SDK tool set exposed to the model. */ readonly tools: ToolSet; } +/** Creates tools for a Fledgling ACP session. */ export interface IToolProvider { + /** Creates the tool set and backing clients for a session request. */ createSessionTools(request: { + /** Working directory requested by the ACP client, when supplied. */ readonly cwd: string | undefined; + + /** MCP servers requested for the session. */ readonly mcpServers: acp.McpServer[]; }): Promise; } +/** Logger used by the agent core for structured runtime diagnostics. */ export interface IRuntimeLogger { + /** Emits an optional debug diagnostic record. */ debug?(record: unknown): void; + + /** Emits a warning diagnostic record. */ warn(record: unknown): void; + + /** Emits an error diagnostic record. */ error(record: unknown): void; } +/** Dependencies required to construct a Fledgling ACP agent. */ export interface FledglingAgentDependencies { + /** Provides tools and closeable clients for each session. */ readonly toolProvider: IToolProvider; + + /** Persists and loads session event history. */ readonly sessionManager: ISessionManager; + + /** Runs model turns for prompts. */ readonly modelTurnRunner: IModelTurnRunner; + + /** Optional structured logger for diagnostics and cleanup failures. */ readonly logger?: IRuntimeLogger; + + /** Enables debug logging for streamed model part types. */ readonly debugStream?: boolean; } diff --git a/packages/agent-core/src/prompt-content.ts b/packages/agent-core/src/prompt-content.ts index 9cbdfce..e309b11 100644 --- a/packages/agent-core/src/prompt-content.ts +++ b/packages/agent-core/src/prompt-content.ts @@ -1,6 +1,7 @@ import type { PromptRequest } from "@agentclientprotocol/sdk"; import type { CoreMessage } from "ai"; +/** Converts AI SDK message content into plain text for ACP replay. */ export function messageContentToText(content: CoreMessage["content"]): string { if (typeof content === "string") { return content; @@ -25,6 +26,7 @@ export function messageContentToText(content: CoreMessage["content"]): string { return JSON.stringify(content); } +/** Extracts user prompt text from an ACP prompt request. */ export function extractPromptText(params: PromptRequest): string { const prompt = params.prompt; @@ -51,6 +53,7 @@ export function extractPromptText(params: PromptRequest): string { return JSON.stringify(prompt); } +/** Converts tool output into text suitable for ACP content updates. */ export function stringifyToolOutput(output: unknown): string { if (typeof output === "string") { return output; @@ -59,6 +62,7 @@ export function stringifyToolOutput(output: unknown): string { return JSON.stringify(output, null, 2); } +/** Reads a Fledgling context hint from a tool output payload, when present. */ export function extractContextHint(output: unknown): unknown { if (!output || typeof output !== "object") { return undefined; @@ -83,6 +87,7 @@ export function extractContextHint(output: unknown): unknown { return undefined; } +/** Wraps a non-object value so it can be sent through ACP raw object fields. */ export function toRawObject(value: unknown): Record { if (value && typeof value === "object" && !Array.isArray(value)) { return value as Record; diff --git a/packages/agent-core/src/prompt-errors.ts b/packages/agent-core/src/prompt-errors.ts index d7400bd..9356499 100644 --- a/packages/agent-core/src/prompt-errors.ts +++ b/packages/agent-core/src/prompt-errors.ts @@ -1,17 +1,33 @@ import type { SessionErrorEvent } from "@fledgling/common"; +/** Session error kind used for prompt failures. */ export type PromptErrorKind = SessionErrorEvent["kind"]; + +/** Session error phase used for prompt failures. */ export type PromptErrorPhase = SessionErrorEvent["phase"]; +/** Sanitized error details persisted for a failed prompt. */ export interface NormalizedPromptError { + /** Fledgling session error kind. */ readonly kind: PromptErrorKind; + + /** Prompt lifecycle phase where the error occurred. */ readonly phase: PromptErrorPhase; + + /** Sanitized human-readable error message. */ readonly message: string; + + /** Whether a later prompt may continue the session. */ readonly recoverable: boolean; + + /** Sanitized original error name, when available. */ readonly errorName: string | undefined; + + /** Sanitized original error code, when available. */ readonly errorCode: string | undefined; } +/** Normalizes an unknown prompt failure into persisted session error details. */ export function normalizePromptError( error: unknown, kind: PromptErrorKind, @@ -27,6 +43,7 @@ export function normalizePromptError( }; } +/** Creates the RPC error surfaced to ACP clients for model prompt failures. */ export function createPromptRpcError(error: unknown, phase: "model_start" | "model_stream"): Error { const normalized = normalizePromptError( error, @@ -36,6 +53,7 @@ export function createPromptRpcError(error: unknown, phase: "model_start" | "mod return new Error(`Fledgling ${formatPromptErrorKind(normalized.kind)}: ${normalized.message}`); } +/** Formats a prompt error kind for user-visible text. */ export function formatPromptErrorKind(kind: PromptErrorKind): string { switch (kind) { case "model_start_failed": @@ -49,6 +67,7 @@ export function formatPromptErrorKind(kind: PromptErrorKind): string { } } +/** Removes unsafe control characters and redacts common secret forms from an error message. */ export function sanitizeErrorMessage(message: string): string { const withoutControlCharacters = replaceControlCharacters(stripAnsiEscapeSequences(message)); const normalized = withoutControlCharacters.replace(/\s+/g, " ").trim() || "Unknown error"; diff --git a/packages/agent-core/src/session-cleanup.ts b/packages/agent-core/src/session-cleanup.ts index 15a4502..090981f 100644 --- a/packages/agent-core/src/session-cleanup.ts +++ b/packages/agent-core/src/session-cleanup.ts @@ -1,20 +1,38 @@ import type { IClosable, IRuntimeLogger } from "./interfaces.js"; +/** Minimal abort controller contract used for pending prompts. */ export interface PromptAbortControllerLike { + /** Aborts the pending prompt. */ abort(): void; } +/** Session state required by cleanup helpers. */ export interface SessionCleanupState { + /** Session identifier used in cleanup diagnostics. */ readonly id: string; + + /** Closeable clients owned by the session. */ readonly clients: readonly IClosable[]; + + /** Active prompt abort controller, when a prompt is running. */ readonly pendingPrompt: PromptAbortControllerLike | undefined; } +/** Structured warning emitted when an MCP client fails to close. */ export interface McpCloseFailureRecord { + /** Log severity for the record. */ readonly level: "warn"; + + /** Event name for MCP close failures. */ readonly event: "mcp_close_failed"; + + /** Session identifier associated with the close attempt, when known. */ readonly sessionId: string | undefined; + + /** Reason the close operation was requested. */ readonly reason: string; + + /** Serialized close failure. */ readonly error: string; } @@ -27,12 +45,14 @@ const defaultLogger: IRuntimeLogger = { } }; +/** Coordinates prompt cancellation and client cleanup for active sessions. */ export class SessionCleanup { readonly #getSessions: () => Iterable; readonly #clearSessions: () => void; readonly #logger: IRuntimeLogger; #closeAllPromise: Promise | undefined; + /** Creates a cleanup helper over caller-owned session storage. */ public constructor( getSessions: () => Iterable, clearSessions: () => void, @@ -43,11 +63,13 @@ export class SessionCleanup { this.#logger = logger; } + /** Closes all current sessions once, reusing the same promise for concurrent callers. */ public closeAll(reason: string): Promise { this.#closeAllPromise ??= this.#closeAll(reason); return this.#closeAllPromise; } + /** Aborts and closes the resources for a single session. */ public async closeSession(session: SessionCleanupState, reason: string): Promise { session.pendingPrompt?.abort(); await closeClients(session.clients, reason, session.id, this.#logger); @@ -65,6 +87,7 @@ export class SessionCleanup { } } +/** Closes a set of clients and logs individual close failures. */ export async function closeClients( clients: readonly IClosable[], reason: string, @@ -88,6 +111,7 @@ export async function closeClients( ); } +/** Converts an unknown error value into a loggable string. */ export function serializeError(error: unknown): string { if (error instanceof Error) { return error.message; diff --git a/packages/common/config/api-extractor.json b/packages/common/config/api-extractor.json new file mode 100644 index 0000000..abf87f1 --- /dev/null +++ b/packages/common/config/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "@fledgling/heft-rig/profiles/default/config/api-extractor-base.json" +} diff --git a/packages/common/src/context-hints.ts b/packages/common/src/context-hints.ts index 0ae5f96..16a516a 100644 --- a/packages/common/src/context-hints.ts +++ b/packages/common/src/context-hints.ts @@ -1,9 +1,20 @@ import { createHash } from "node:crypto"; import path from "node:path"; +/** + * Metadata key used to attach a {@link ContextHint} to tool results or other + * contextual payloads. + */ export const CONTEXT_HINT_META_KEY: string = "house.pape.fledgling/context-hint"; + +/** + * Metadata key used to identify payloads that came from tool execution. + */ export const TOOL_META_KEY: string = "house.pape.fledgling/tool"; +/** + * Describes the broad class of context represented by a {@link ContextHint}. + */ export type ContextKind = | "ephemeral_observation" | "durable_resource" @@ -12,6 +23,10 @@ export type ContextKind = | "command_output" | "user_memory"; +/** + * Indicates where hinted context should be placed when constructing model + * input. + */ export type ContextPlacement = | "stable_prefix" | "session_context" @@ -19,31 +34,79 @@ export type ContextPlacement = | "latest_evidence" | "do_not_inline"; +/** + * Indicates how long hinted context should be retained. + */ export type ContextRetention = | "discard_after_turn" | "summarize_after_turn" | "retain_until_changed" | "retain_for_session"; +/** + * Routing and retention metadata for contextual content. + */ export interface ContextHint { + /** + * The broad class of context being described. + */ readonly kind: ContextKind; + + /** + * Stable identity for the underlying context, such as a file path or resource + * identifier. + */ readonly identity?: string; + + /** + * Hash of the contextual content, when available. + */ readonly contentHash?: string; + + /** + * Approximate number of tokens represented by the context. + */ readonly tokenEstimate?: number; + + /** + * Preferred placement for the context in model input. + */ readonly placement: ContextPlacement; + + /** + * Retention policy for the context. + */ readonly retention: ContextRetention; + + /** + * Relative priority when selecting among multiple context hints. + */ readonly priority?: number; + + /** + * Tags used to route context to consumers that understand a domain or file + * type. + */ readonly routingTags?: string[]; } +/** + * Computes a SHA-256 content hash with the package's standard prefix. + */ export function hashText(text: string): string { return `sha256:${createHash("sha256").update(text).digest("hex")}`; } +/** + * Estimates token count from text length. + */ export function estimateTokens(text: string): number { return Math.ceil(text.length / 4); } +/** + * Infers a routing tag from a file path extension. + */ export function inferRoutingTag(filePath: string): string { const ext = path.extname(filePath).toLowerCase(); switch (ext) { diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 0fa3652..b73d55a 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,2 +1,8 @@ +/** + * Shared contracts and small utilities used by Fledgling packages. + * + * @packageDocumentation + */ + export * from "./context-hints.js"; export * from "./session-events.js"; diff --git a/packages/common/src/session-events.ts b/packages/common/src/session-events.ts index 96f1d7a..650ed7d 100644 --- a/packages/common/src/session-events.ts +++ b/packages/common/src/session-events.ts @@ -1,8 +1,21 @@ +/** + * A chat-style message included in context sent to a model. + */ export interface ContextMessage { + /** + * The message author role. + */ readonly role: "user" | "assistant" | "system"; + + /** + * Message text content. + */ readonly content: string; } +/** + * Discriminated union of persisted session events. + */ export type SessionEvent = | SessionCreatedEvent | SessionLoadedEvent @@ -13,69 +26,172 @@ export type SessionEvent = | SessionErrorEvent | CompactionEvent; +/** + * Fields shared by every persisted session event. + */ export interface SessionEventBase { + /** + * Unique identifier for this event. + */ readonly eventId: string; + + /** + * Identifier for the session that owns this event. + */ readonly sessionId: string; + + /** + * Event creation time as an ISO timestamp. + */ readonly timestamp: string; } +/** + * Event recorded when a session is created. + */ export type SessionCreatedEvent = SessionEventBase & { + /** Event discriminator. */ readonly type: "session.created"; + + /** Working directory associated with the session, when known. */ readonly cwd: string | undefined; + + /** Names of MCP servers available when the session was created. */ readonly mcpServers: readonly string[]; }; +/** + * Event recorded when an existing session is loaded. + */ export type SessionLoadedEvent = SessionEventBase & { + /** Event discriminator. */ readonly type: "session.loaded"; + + /** Working directory associated with the session, when known. */ readonly cwd: string | undefined; + + /** Source that supplied the loaded session data. */ readonly source: string; }; +/** + * Event containing a user message. + */ export type UserMessageEvent = SessionEventBase & { + /** Event discriminator. */ readonly type: "message.user"; + + /** User-authored message text. */ readonly text: string; }; +/** + * Event containing an assistant message. + */ export type AssistantMessageEvent = SessionEventBase & { + /** Event discriminator. */ readonly type: "message.assistant"; + + /** Assistant-authored message text. */ readonly text: string; }; +/** + * Event recorded when a tool call starts. + */ export type ToolCallEvent = SessionEventBase & { + /** Event discriminator. */ readonly type: "tool.call"; + + /** Identifier used to match this call with its result. */ readonly toolCallId: string; + + /** Tool name requested by the model. */ readonly toolName: string; + + /** Human-readable title for the tool call. */ readonly title: string; + + /** Raw input supplied to the tool. */ readonly rawInput: unknown; }; +/** + * Event recorded when a tool call completes or fails. + */ export type ToolResultEvent = SessionEventBase & { + /** Event discriminator. */ readonly type: "tool.result"; + + /** Identifier matching the corresponding tool call. */ readonly toolCallId: string; + + /** Tool name, when known at result time. */ readonly toolName: string | undefined; + + /** Completion status for the tool call. */ readonly status: "completed" | "failed"; + + /** Textual output produced for display or model context. */ readonly text: string; + + /** Raw output returned by the tool. */ readonly rawOutput: unknown; + + /** Context hint metadata returned with the tool result. */ readonly contextHint: unknown; }; +/** + * Event recorded when a session-level operation fails. + */ export type SessionErrorEvent = SessionEventBase & { + /** Event discriminator. */ readonly type: "session.error"; + + /** Stable category for the error. */ readonly kind: "model_start_failed" | "model_stream_failed" | "prompt_cleanup_failed"; + + /** Session phase in which the error occurred. */ readonly phase: "model_start" | "model_stream" | "cleanup"; + + /** Human-readable error message. */ readonly message: string; + + /** Whether the session can continue after the error. */ readonly recoverable: boolean; + + /** Whether assistant text was persisted before the error. */ readonly assistantTextPersisted: boolean; + + /** Error class or name, when available. */ readonly errorName: string | undefined; + + /** Error code, when available. */ readonly errorCode: string | undefined; }; +/** + * Event recorded when session context is compacted. + */ export type CompactionEvent = SessionEventBase & { + /** Event discriminator. */ readonly type: "context.compacted"; + + /** Target token budget for compaction. */ readonly targetTokens: number; + + /** Estimated token count before compaction. */ readonly estimatedTokensBefore: number; + + /** Estimated token count after compaction. */ readonly estimatedTokensAfter: number; + + /** Event identifiers removed from inline context by compaction. */ readonly droppedEventIds: readonly string[]; }; +/** + * Session event kinds that can be discarded or summarized during compaction. + */ export type VolatileEventType = "tool.call" | "tool.result"; diff --git a/packages/context-builder/config/api-extractor.json b/packages/context-builder/config/api-extractor.json new file mode 100644 index 0000000..abf87f1 --- /dev/null +++ b/packages/context-builder/config/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "@fledgling/heft-rig/profiles/default/config/api-extractor-base.json" +} diff --git a/packages/context-builder/src/build-context.ts b/packages/context-builder/src/build-context.ts index bc7074d..666caed 100644 --- a/packages/context-builder/src/build-context.ts +++ b/packages/context-builder/src/build-context.ts @@ -3,65 +3,200 @@ import type { ContextMessage, SessionEvent } from "@fledgling/common"; import { pruneOldVolatileEvents, type DroppedEvent, type PruneEventsOptions } from "./prune-events.js"; import { estimateEventTokens, estimateMessagesTokens } from "./token-estimator.js"; +/** + * Options for building context directly from replayable events. + */ export interface BuildReplayContextOptions { + /** + * Selects replay mode, which preserves events without compaction. + */ readonly mode: "replay"; } +/** + * Model-ready context derived from session events. + */ export interface BuiltContext { + /** + * Messages ready to send to a chat-style model. + */ readonly messages: ContextMessage[]; + + /** + * Session events retained in the context. + */ readonly events: SessionEvent[]; + + /** + * Events removed while preparing the context. + */ readonly dropped: DroppedEvent[]; + + /** + * Estimated token count for the generated messages. + */ readonly tokenEstimate: number; } +/** + * Options for selecting which events should be compacted. + */ export interface PrepareCompactionOptions { + /** + * Number of latest user turns to retain verbatim. + */ readonly keepLatestTurns: number; + + /** + * Optional pruning settings applied before compaction selection. + */ readonly prune?: PruneEventsOptions; + + /** + * Maximum estimated tokens to pass into the compaction model. + */ readonly maxCompactionInputTokens?: number; } +/** + * Partitioned events and estimates prepared for compaction. + */ export interface PreparedCompaction { + /** + * Older events selected as compaction model input. + */ readonly eventsToCompact: SessionEvent[]; + + /** + * Recent events retained verbatim after compaction. + */ readonly retainedEvents: SessionEvent[]; + + /** + * Events dropped during pruning or input limiting. + */ readonly dropped: DroppedEvent[]; + + /** + * Estimated token count for events selected as compaction input. + */ readonly compactionInputTokenEstimate: number; + + /** + * Estimated token count for retained events after conversion to messages. + */ readonly retainedTokenEstimate: number; + + /** + * Whether any events remain to compact. + */ readonly needsCompaction: boolean; } +/** + * Options for constructing context from an existing compaction summary. + */ export interface BuildCompactedContextOptions { + /** + * Continuity summary to prepend as a system message. + */ readonly summary: string; + + /** + * Recent events to retain after the summary. + */ readonly retainedEvents: readonly SessionEvent[]; } +/** + * Request passed to a compaction model. + */ export interface CompactionModelRequest { + /** + * Events that should be summarized. + */ readonly events: SessionEvent[]; + + /** + * Desired token budget for the summary, when provided. + */ readonly targetTokens: number | undefined; + + /** + * Instructions for producing the continuity summary. + */ readonly instructions: string; + + /** + * Estimated token count for the input events. + */ readonly inputTokenEstimate: number; } +/** + * Result returned by a compaction model. + */ export interface CompactionModelResult { + /** + * Continuity summary of the compacted events. + */ readonly summary: string; + + /** + * Estimated token count for the summary, when known. + */ readonly tokenEstimate?: number; } +/** + * Function that summarizes older session events for compaction. + */ export type CompactionFunction = (request: CompactionModelRequest) => Promise; +/** + * Options for preparing and compacting a session event stream. + */ export interface CompactContextOptions extends PrepareCompactionOptions { + /** + * Desired token budget for the generated summary. + */ readonly targetTokens?: number; + + /** + * Custom instructions for the compaction model. + */ readonly instructions?: string; + + /** + * Function used to generate the compaction summary. + */ readonly compact: CompactionFunction; } +/** + * Context result produced by a compaction pass. + */ export interface CompactedContextResult extends BuiltContext { + /** + * Preparation details used to decide what was compacted. + */ readonly prepared: PreparedCompaction; + + /** + * Generated summary, or undefined when compaction was unnecessary. + */ readonly summary: string | undefined; } +/** + * Default instructions for preserving coding-session continuity during compaction. + */ export const DEFAULT_COMPACTION_INSTRUCTIONS: string = "Write a detailed continuity summary for resuming a coding-agent session. Preserve user goals, decisions, constraints, rejected approaches, files or modules discussed, pending work, and historical observations that may need refresh. Do not treat old command output, file contents, git status, test results, or directory listings as current truth."; +/** + * Builds replay context by converting message events to model messages. + */ export function buildContext(events: readonly SessionEvent[], _options: BuildReplayContextOptions): BuiltContext { const messages = eventsToMessages(events); return { @@ -72,6 +207,9 @@ export function buildContext(events: readonly SessionEvent[], _options: BuildRep }; } +/** + * Builds context, compacting older events into a summary when needed. + */ export async function compactContext( events: readonly SessionEvent[], options: CompactContextOptions @@ -107,6 +245,9 @@ export async function compactContext( }; } +/** + * Prunes volatile events and splits older events from the latest retained turns. + */ export function prepareCompaction( events: readonly SessionEvent[], options: PrepareCompactionOptions @@ -125,6 +266,9 @@ export function prepareCompaction( }; } +/** + * Builds context from a compaction summary plus retained events. + */ export function buildCompactedContext(options: BuildCompactedContextOptions): BuiltContext { const retainedMessages = eventsToMessages(options.retainedEvents); const messages: ContextMessage[] = [ @@ -143,6 +287,9 @@ export function buildCompactedContext(options: BuildCompactedContextOptions): Bu }; } +/** + * Converts user and assistant message events into chat messages. + */ export function eventsToMessages(events: readonly SessionEvent[]): ContextMessage[] { const messages: ContextMessage[] = []; diff --git a/packages/context-builder/src/index.ts b/packages/context-builder/src/index.ts index d97fea7..4e2a668 100644 --- a/packages/context-builder/src/index.ts +++ b/packages/context-builder/src/index.ts @@ -1,3 +1,8 @@ +/** + * Utilities for turning Fledgling session events into model-ready context. + * + * @packageDocumentation + */ export * from "./build-context.js"; export * from "./prune-events.js"; export * from "./token-estimator.js"; diff --git a/packages/context-builder/src/prune-events.ts b/packages/context-builder/src/prune-events.ts index 8e7cba9..5d6bdca 100644 --- a/packages/context-builder/src/prune-events.ts +++ b/packages/context-builder/src/prune-events.ts @@ -1,21 +1,58 @@ import type { SessionEvent, ToolCallEvent, ToolResultEvent } from "@fledgling/common"; +/** + * Options for pruning older volatile tool events. + */ export interface PruneEventsOptions { + /** + * Number of latest tool results to keep for each result group. + */ readonly keepLatestToolResultsPerGroup?: number; + + /** + * Number of latest tool calls to keep for each tool name. + */ readonly keepLatestToolCallsPerGroup?: number; } +/** + * Events retained after pruning, plus records for removed events. + */ export interface PrunedEvents { + /** + * Events retained in their original order. + */ readonly events: SessionEvent[]; + + /** + * Events removed by pruning. + */ readonly dropped: DroppedEvent[]; } +/** + * Description of an event removed from context. + */ export interface DroppedEvent { + /** + * Identifier of the removed event. + */ readonly eventId: string; + + /** + * Type of the removed event. + */ readonly type: SessionEvent["type"]; + + /** + * Machine-readable reason the event was removed. + */ readonly reason: string; } +/** + * Removes older volatile tool calls and results while preserving recent entries per group. + */ export function pruneOldVolatileEvents( events: readonly SessionEvent[], options: PruneEventsOptions = {} diff --git a/packages/context-builder/src/token-estimator.ts b/packages/context-builder/src/token-estimator.ts index d658e57..586e921 100644 --- a/packages/context-builder/src/token-estimator.ts +++ b/packages/context-builder/src/token-estimator.ts @@ -1,17 +1,29 @@ import type { ContextMessage, SessionEvent } from "@fledgling/common"; +/** + * Estimates tokens for plain text using a character-count heuristic. + */ export function estimateTextTokens(text: string): number { return Math.ceil(text.length / 4); } +/** + * Estimates tokens for a chat message, including a small per-message overhead. + */ export function estimateMessageTokens(message: ContextMessage): number { return estimateTextTokens(message.content) + 4; } +/** + * Estimates total tokens for a sequence of chat messages. + */ export function estimateMessagesTokens(messages: readonly ContextMessage[]): number { return messages.reduce((total, message) => total + estimateMessageTokens(message), 0); } +/** + * Estimates tokens for a session event using event-type-specific overheads. + */ export function estimateEventTokens(event: SessionEvent): number { switch (event.type) { case "message.user": diff --git a/packages/mcp-workspace-browser/config/api-extractor.json b/packages/mcp-workspace-browser/config/api-extractor.json new file mode 100644 index 0000000..abf87f1 --- /dev/null +++ b/packages/mcp-workspace-browser/config/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "@fledgling/heft-rig/profiles/default/config/api-extractor-base.json" +} diff --git a/packages/mcp-workspace-browser/src/index.ts b/packages/mcp-workspace-browser/src/index.ts index a70d94f..bcc23bf 100644 --- a/packages/mcp-workspace-browser/src/index.ts +++ b/packages/mcp-workspace-browser/src/index.ts @@ -1,2 +1,8 @@ +/** + * Browser-hosted MCP workspace tools and runtime contracts. + * + * @packageDocumentation + */ + export * from "./runtime.js"; export * from "./sidecar.js"; diff --git a/packages/mcp-workspace-browser/src/runtime.ts b/packages/mcp-workspace-browser/src/runtime.ts index d5c1752..d2d2d0c 100644 --- a/packages/mcp-workspace-browser/src/runtime.ts +++ b/packages/mcp-workspace-browser/src/runtime.ts @@ -1,41 +1,141 @@ +/** + * A file entry returned when listing a browser workspace directory. + */ export interface WorkspaceFileEntry { + /** + * Discriminator identifying this entry as a file. + */ readonly type: "file"; + + /** + * Base name of the file. + */ readonly name: string; + + /** + * Workspace-relative path to the file. + */ readonly path: string; + + /** + * Size of the file content in bytes. + */ readonly sizeBytes: number; } +/** + * A directory entry returned when listing a browser workspace directory. + */ export interface WorkspaceDirectoryEntry { + /** + * Discriminator identifying this entry as a directory. + */ readonly type: "directory"; + + /** + * Base name of the directory. + */ readonly name: string; + + /** + * Workspace-relative path to the directory. + */ readonly path: string; } +/** + * A file or directory entry in a browser workspace listing. + */ export type WorkspaceEntry = WorkspaceFileEntry | WorkspaceDirectoryEntry; +/** + * Result of a command executed by a browser workspace runtime. + */ export interface CommandResult { + /** + * Process exit code reported by the runtime. + */ readonly exitCode: number; + + /** + * Captured standard output. + */ readonly stdout: string; + + /** + * Captured standard error. + */ readonly stderr: string; + + /** + * Whether the command exceeded the requested timeout. + */ readonly timedOut: boolean; + + /** + * Whether stdout or stderr was shortened to satisfy the requested output limit. + */ readonly truncated: boolean; } +/** + * Runtime adapter used by browser workspace MCP tools to access files and commands. + */ export interface IWorkspaceRuntime { + /** + * Reads a UTF-8 text file from the workspace. + */ readFile(path: string): Promise; + + /** + * Writes UTF-8 text content to a workspace file. + */ writeFile(path: string, content: string): Promise; + + /** + * Lists immediate children of a workspace directory. + */ listDirectory(path: string): Promise; + + /** + * Searches workspace text files for an exact query string. + */ searchText(query: string, path: string): Promise; + + /** + * Runs a shell command from a workspace directory. + */ runCommand(command: string, cwd: string, timeoutMs: number, maxOutputBytes: number): Promise; + + /** + * Releases runtime resources when the sidecar is closed. + */ dispose?(): Promise | void; } +/** + * A single text search match in a browser workspace file. + */ export interface SearchMatch { + /** + * Workspace-relative path containing the match. + */ readonly path: string; + + /** + * One-based line number of the match. + */ readonly line: number; + + /** + * Full line of text containing the match. + */ readonly text: string; } +/** + * Appends exact line-based search matches for a file to an existing collection. + */ export function appendSearchMatches(matches: SearchMatch[], path: string, content: string, query: string): void { const lines = content.split(/\r?\n/); for (const [index, line] of lines.entries()) { @@ -45,20 +145,32 @@ export function appendSearchMatches(matches: SearchMatch[], path: string, conten } } +/** + * Normalizes a workspace-relative path to use forward slashes and `.` for the root. + */ export function normalizeWorkspacePath(path: string): string { const normalized = path.replaceAll("\\", "/").replace(/^\/+/, ""); return normalized.length === 0 || normalized === "." ? "." : normalized; } +/** + * Normalizes a workspace path to an absolute browser-workspace path. + */ export function normalizeAbsoluteWorkspacePath(path: string): string { const normalized = normalizeWorkspacePath(path); return normalized === "." ? "/" : `/${normalized}`; } +/** + * Joins two normalized workspace-relative path segments. + */ export function joinWorkspacePath(parent: string, child: string): string { return parent === "." ? child : `${parent}/${child}`; } +/** + * Joins two normalized absolute browser-workspace path segments. + */ export function joinAbsoluteWorkspacePath(parent: string, child: string): string { return parent === "/" ? `/${child}` : `${parent}/${child}`; } diff --git a/packages/mcp-workspace-browser/src/sidecar.ts b/packages/mcp-workspace-browser/src/sidecar.ts index a687cb2..1fba1c3 100644 --- a/packages/mcp-workspace-browser/src/sidecar.ts +++ b/packages/mcp-workspace-browser/src/sidecar.ts @@ -16,11 +16,24 @@ interface ContextHint { readonly routingTags?: string[]; } +/** + * Browser workspace MCP sidecar connected through an in-memory client transport. + */ export interface WebWorkspaceSidecar { + /** + * Transport that a browser-side MCP client can connect to. + */ readonly clientTransport: InMemoryTransport; + + /** + * Closes the MCP server and its client transport. + */ close(): Promise; } +/** + * Creates an MCP sidecar server backed by a browser workspace runtime. + */ export async function createWebWorkspaceSidecar(runtime: IWorkspaceRuntime): Promise { const server = new McpServer( { @@ -46,6 +59,9 @@ export async function createWebWorkspaceSidecar(runtime: IWorkspaceRuntime): Pro }; } +/** + * Registers browser workspace file, search, and command tools on an MCP server. + */ export function registerWebWorkspaceTools(server: McpServer, runtime: IWorkspaceRuntime): void { server.registerTool( "workspace.read_file", diff --git a/packages/mcp-workspace-webcontainer/config/api-extractor.json b/packages/mcp-workspace-webcontainer/config/api-extractor.json new file mode 100644 index 0000000..abf87f1 --- /dev/null +++ b/packages/mcp-workspace-webcontainer/config/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "@fledgling/heft-rig/profiles/default/config/api-extractor-base.json" +} diff --git a/packages/mcp-workspace-webcontainer/src/index.ts b/packages/mcp-workspace-webcontainer/src/index.ts index c9435f8..36a927e 100644 --- a/packages/mcp-workspace-webcontainer/src/index.ts +++ b/packages/mcp-workspace-webcontainer/src/index.ts @@ -1,2 +1,8 @@ +/** + * WebContainer-backed MCP workspace runtime for browser-hosted workspaces. + * + * @packageDocumentation + */ + export * from "./runtime.js"; export * from "@fledgling/mcp-workspace-browser"; diff --git a/packages/mcp-workspace-webcontainer/src/runtime.ts b/packages/mcp-workspace-webcontainer/src/runtime.ts index d2acc9d..1b05cb0 100644 --- a/packages/mcp-workspace-webcontainer/src/runtime.ts +++ b/packages/mcp-workspace-webcontainer/src/runtime.ts @@ -9,21 +9,36 @@ import { type WorkspaceEntry } from "@fledgling/mcp-workspace-browser"; +/** + * Workspace runtime adapter backed by a WebContainer instance. + */ export class WebContainerWorkspaceRuntime implements IWorkspaceRuntime { readonly #container: WebContainer; + /** + * Creates a runtime that reads, writes, lists, searches, and runs commands in the provided container. + */ public constructor(container: WebContainer) { this.#container = container; } + /** + * Reads a UTF-8 text file from the workspace. + */ public async readFile(path: string): Promise { return this.#container.fs.readFile(normalizeWorkspacePath(path), "utf8"); } + /** + * Writes UTF-8 text content to a workspace file. + */ public async writeFile(path: string, content: string): Promise { await this.#container.fs.writeFile(normalizeWorkspacePath(path), content); } + /** + * Lists immediate children of a workspace directory. + */ public async listDirectory(path: string): Promise { const root = normalizeWorkspacePath(path); const entries = await this.#container.fs.readdir(root, { withFileTypes: true }); @@ -46,12 +61,18 @@ export class WebContainerWorkspaceRuntime implements IWorkspaceRuntime { }); } + /** + * Searches workspace text files under a directory for an exact query string. + */ public async searchText(query: string, path: string): Promise { const matches: SearchMatch[] = []; await this.#searchDirectory(normalizeWorkspacePath(path), query, matches); return matches; } + /** + * Runs a shell command from a workspace directory. + */ public async runCommand( command: string, cwd: string, diff --git a/packages/mcp-workspace/config/api-extractor.json b/packages/mcp-workspace/config/api-extractor.json new file mode 100644 index 0000000..abf87f1 --- /dev/null +++ b/packages/mcp-workspace/config/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "@fledgling/heft-rig/profiles/default/config/api-extractor-base.json" +} diff --git a/packages/mcp-workspace/src/index.ts b/packages/mcp-workspace/src/index.ts index 360fa1b..1a60c69 100644 --- a/packages/mcp-workspace/src/index.ts +++ b/packages/mcp-workspace/src/index.ts @@ -1,5 +1,11 @@ #!/usr/bin/env node +/** + * Context-aware first-party workspace MCP server for Fledgling. + * + * @packageDocumentation + */ + import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; diff --git a/packages/session-file-system/config/api-extractor.json b/packages/session-file-system/config/api-extractor.json new file mode 100644 index 0000000..abf87f1 --- /dev/null +++ b/packages/session-file-system/config/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "@fledgling/heft-rig/profiles/default/config/api-extractor-base.json" +} diff --git a/packages/session-file-system/src/index.ts b/packages/session-file-system/src/index.ts index 5f16de6..4bdbb30 100644 --- a/packages/session-file-system/src/index.ts +++ b/packages/session-file-system/src/index.ts @@ -1,3 +1,9 @@ +/** + * Filesystem-backed session persistence for Fledgling. + * + * @packageDocumentation + */ + import { randomUUID } from "node:crypto"; import { appendFile, mkdir, readFile } from "node:fs/promises"; import path from "node:path"; @@ -5,10 +11,27 @@ import path from "node:path"; import type { ISessionManager, SessionEventBase } from "@fledgling/agent-core"; import type { SessionEvent } from "@fledgling/common"; +/** + * Stores and loads Fledgling session events as newline-delimited JSON files. + * + * Each session normally maps to a separate `.jsonl` file under the configured + * root directory. A single explicit session file can also be supplied for + * integrations that want all operations to target one known file path. + * + * @public + */ export class FileSystemSessionManager implements ISessionManager { readonly #root: string; readonly #sessionFile: string | undefined; + /** + * Creates a filesystem-backed session manager. + * + * @param root - Directory used for per-session JSONL files when `sessionFile` + * is not provided. + * @param sessionFile - Optional path to a specific JSONL file. Defaults to + * the `FLEDGLING_SESSION_FILE` environment variable when it is set. + */ public constructor( root: string = defaultSessionStoreRoot(), sessionFile: string | undefined = process.env.FLEDGLING_SESSION_FILE @@ -17,10 +40,21 @@ export class FileSystemSessionManager implements ISessionManager { this.#sessionFile = sessionFile ? path.resolve(sessionFile) : undefined; } + /** + * Creates a new unique session identifier. + * + * @returns A UUID suitable for use as a Fledgling session ID. + */ public createSessionId(): string { return randomUUID(); } + /** + * Creates the common event fields for a session event. + * + * @param sessionId - Session identifier that the event belongs to. + * @returns A base event object with a new event ID and timestamp. + */ public createEventBase(sessionId: string): SessionEventBase { return { eventId: randomUUID(), @@ -29,12 +63,28 @@ export class FileSystemSessionManager implements ISessionManager { }; } + /** + * Appends a session event to persistent storage. + * + * The event is serialized as one JSON line. Missing parent directories are + * created before writing. + * + * @param event - Session event to append. + */ public async appendEvent(event: SessionEvent): Promise { const sessionPath = this.#sessionPath(event.sessionId); await mkdir(path.dirname(sessionPath), { recursive: true }); await appendFile(sessionPath, `${JSON.stringify(event)}\n`, "utf8"); } + /** + * Loads all stored events for a session. + * + * @param sessionId - Session identifier to load. + * @returns Events read from the session JSONL file in storage order. + * @throws Error when the session file cannot be read or contains events for a + * different session ID. + */ public async loadEvents(sessionId: string): Promise { let raw: string; try { @@ -64,6 +114,15 @@ export class FileSystemSessionManager implements ISessionManager { } } +/** + * Resolves the default directory for filesystem session storage. + * + * @returns The absolute path from `FLEDGLING_SESSION_DIR`, or + * `.fledgling/sessions` under the current working directory when the environment + * variable is unset. + * + * @public + */ export function defaultSessionStoreRoot(): string { return path.resolve(process.env.FLEDGLING_SESSION_DIR ?? path.join(process.cwd(), ".fledgling", "sessions")); } diff --git a/packages/session-local-storage/config/api-extractor.json b/packages/session-local-storage/config/api-extractor.json new file mode 100644 index 0000000..abf87f1 --- /dev/null +++ b/packages/session-local-storage/config/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "@fledgling/heft-rig/profiles/default/config/api-extractor-base.json" +} diff --git a/packages/session-local-storage/src/index.ts b/packages/session-local-storage/src/index.ts index 1766203..2a237e9 100644 --- a/packages/session-local-storage/src/index.ts +++ b/packages/session-local-storage/src/index.ts @@ -1,24 +1,70 @@ +/** + * Browser localStorage-backed session persistence for Fledgling. + * + * @packageDocumentation + */ + import type { ISessionManager, SessionEventBase } from "@fledgling/agent-core"; import type { SessionEvent } from "@fledgling/common"; +/** + * Options for configuring a {@link LocalStorageSessionManager}. + * + * @public + */ export interface LocalStorageSessionManagerOptions { + /** + * Storage implementation used to persist session events. + * + * Defaults to `globalThis.localStorage`. + */ readonly storage?: Storage; + + /** + * Prefix prepended to generated storage keys. + * + * Defaults to `fledgling:sessions:`. + */ readonly keyPrefix?: string; } +/** + * Stores and loads Fledgling session events from Web Storage. + * + * Each session is serialized as a JSON array under a key derived from the + * configured key prefix and sanitized session ID. + * + * @public + */ export class LocalStorageSessionManager implements ISessionManager { readonly #storage: Storage; readonly #keyPrefix: string; + /** + * Creates a localStorage-backed session manager. + * + * @param options - Optional storage implementation and key prefix overrides. + */ public constructor(options: LocalStorageSessionManagerOptions = {}) { this.#storage = options.storage ?? getDefaultStorage(); this.#keyPrefix = options.keyPrefix ?? "fledgling:sessions:"; } + /** + * Creates a new unique session identifier. + * + * @returns A UUID suitable for use as a Fledgling session ID. + */ public createSessionId(): string { return createId(); } + /** + * Creates the common event fields for a session event. + * + * @param sessionId - Session identifier that the event belongs to. + * @returns A base event object with a new event ID and timestamp. + */ public createEventBase(sessionId: string): SessionEventBase { return { eventId: createId(), @@ -27,12 +73,25 @@ export class LocalStorageSessionManager implements ISessionManager { }; } + /** + * Appends a session event to persistent storage. + * + * @param event - Session event to append. + */ public async appendEvent(event: SessionEvent): Promise { const events = this.#loadExistingEvents(event.sessionId); events.push(event); this.#storage.setItem(this.#sessionKey(event.sessionId), JSON.stringify(events)); } + /** + * Loads all stored events for a session. + * + * @param sessionId - Session identifier to load. + * @returns Events read from Web Storage in insertion order. + * @throws Error when the session is unknown or contains events for a + * different session ID. + */ public async loadEvents(sessionId: string): Promise { const raw = this.#storage.getItem(this.#sessionKey(sessionId)); if (!raw) { diff --git a/packages/session-log/config/api-extractor.json b/packages/session-log/config/api-extractor.json new file mode 100644 index 0000000..abf87f1 --- /dev/null +++ b/packages/session-log/config/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "@fledgling/heft-rig/profiles/default/config/api-extractor-base.json" +} diff --git a/packages/session-log/src/index.ts b/packages/session-log/src/index.ts index f2496b0..646e4cd 100644 --- a/packages/session-log/src/index.ts +++ b/packages/session-log/src/index.ts @@ -1 +1,6 @@ +/** + * Provides JSONL-backed session event storage for Fledgling sessions. + * + * @packageDocumentation + */ export * from "./session-store.js"; diff --git a/packages/session-log/src/session-store.ts b/packages/session-log/src/session-store.ts index ee3efb5..536e92d 100644 --- a/packages/session-log/src/session-store.ts +++ b/packages/session-log/src/session-store.ts @@ -4,10 +4,19 @@ import path from "node:path"; import type { SessionEvent } from "@fledgling/common"; +/** + * Stores and loads session events as newline-delimited JSON files. + */ export class SessionStore { readonly #root: string; readonly #sessionFile: string | undefined; + /** + * Creates a session store rooted at the provided directory. + * + * @param root - Directory used for per-session log files when no explicit session file is provided. + * @param sessionFile - Optional log file path that stores every session in one portable JSONL file. + */ public constructor( root: string = defaultSessionStoreRoot(), sessionFile: string | undefined = process.env.FLEDGLING_SESSION_FILE @@ -16,10 +25,18 @@ export class SessionStore { this.#sessionFile = sessionFile ? path.resolve(sessionFile) : undefined; } + /** + * Creates a new unique session identifier. + */ public createId(): string { return randomUUID(); } + /** + * Creates the common event fields for a session event. + * + * @param sessionId - Session identifier to include in the event base. + */ public createEventBase(sessionId: string): Pick { return { eventId: randomUUID(), @@ -28,12 +45,23 @@ export class SessionStore { }; } + /** + * Appends an event to its session log. + * + * @param event - Session event to serialize as a JSONL entry. + */ public async append(event: SessionEvent): Promise { const sessionPath = this.#sessionPath(event.sessionId); await mkdir(path.dirname(sessionPath), { recursive: true }); await appendFile(sessionPath, `${JSON.stringify(event)}\n`, "utf8"); } + /** + * Loads all events for a session. + * + * @param sessionId - Session identifier to read. + * @throws Error if the session log cannot be read or contains events for another session. + */ public async load(sessionId: string): Promise { let raw: string; try { @@ -63,6 +91,11 @@ export class SessionStore { } } +/** + * Returns the default directory for session log files. + * + * The `FLEDGLING_SESSION_DIR` environment variable overrides the workspace-local default. + */ export function defaultSessionStoreRoot(): string { return path.resolve(process.env.FLEDGLING_SESSION_DIR ?? path.join(process.cwd(), ".fledgling", "sessions")); } diff --git a/packages/session-memory/config/api-extractor.json b/packages/session-memory/config/api-extractor.json new file mode 100644 index 0000000..abf87f1 --- /dev/null +++ b/packages/session-memory/config/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "@fledgling/heft-rig/profiles/default/config/api-extractor-base.json" +} diff --git a/packages/session-memory/src/index.ts b/packages/session-memory/src/index.ts index 21bf0c4..08dba3d 100644 --- a/packages/session-memory/src/index.ts +++ b/packages/session-memory/src/index.ts @@ -1,3 +1,9 @@ +/** + * In-memory session persistence for Fledgling. + * + * @packageDocumentation + */ + import type { ISessionManager, SessionEventBase } from "@fledgling/agent-core"; import type { SessionEvent } from "@fledgling/common"; @@ -6,13 +12,32 @@ interface WebCryptoLike { getRandomValues?: (array: T) => T; } +/** + * Stores and loads Fledgling session events in process memory. + * + * Events are grouped by session ID and are discarded when the manager instance + * is discarded. + * + * @public + */ export class MemorySessionManager implements ISessionManager { readonly #eventsBySessionId: Map = new Map(); + /** + * Creates a new unique session identifier. + * + * @returns A UUID suitable for use as a Fledgling session ID. + */ public createSessionId(): string { return createId(); } + /** + * Creates the common event fields for a session event. + * + * @param sessionId - Session identifier that the event belongs to. + * @returns A base event object with a new event ID and timestamp. + */ public createEventBase(sessionId: string): SessionEventBase { return { eventId: createId(), @@ -21,12 +46,24 @@ export class MemorySessionManager implements ISessionManager { }; } + /** + * Appends a session event to memory. + * + * @param event - Session event to append. + */ public async appendEvent(event: SessionEvent): Promise { const events = this.#eventsBySessionId.get(event.sessionId) ?? []; events.push(event); this.#eventsBySessionId.set(event.sessionId, events); } + /** + * Loads all stored events for a session. + * + * @param sessionId - Session identifier to load. + * @returns Events read from memory in insertion order. + * @throws Error when the session is unknown. + */ public async loadEvents(sessionId: string): Promise { const events = this.#eventsBySessionId.get(sessionId); if (!events) { diff --git a/packages/tools-mcp-node/config/api-extractor.json b/packages/tools-mcp-node/config/api-extractor.json new file mode 100644 index 0000000..abf87f1 --- /dev/null +++ b/packages/tools-mcp-node/config/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "@fledgling/heft-rig/profiles/default/config/api-extractor-base.json" +} diff --git a/packages/tools-mcp-node/src/config.ts b/packages/tools-mcp-node/src/config.ts index fce24c2..81ee433 100644 --- a/packages/tools-mcp-node/src/config.ts +++ b/packages/tools-mcp-node/src/config.ts @@ -2,7 +2,13 @@ import { existsSync } from "node:fs"; import { readFile } from "node:fs/promises"; import path from "node:path"; +/** + * Configuration file shape for Node MCP tool providers. + */ export interface FledglingConfig { + /** + * MCP servers keyed by their configured server name. + */ readonly mcpServers?: Record; } @@ -14,6 +20,9 @@ export interface ResolvedMcpServer { readonly config: McpServerConfig; } +/** + * Supported MCP server connection configuration. + */ export type McpServerConfig = | { readonly type: "firstPartyWorkspace"; diff --git a/packages/tools-mcp-node/src/index.ts b/packages/tools-mcp-node/src/index.ts index 566dfe8..eb4c4f7 100644 --- a/packages/tools-mcp-node/src/index.ts +++ b/packages/tools-mcp-node/src/index.ts @@ -1,3 +1,9 @@ +/** + * Provides Node.js MCP tool discovery and session tool wiring for Fledgling. + * + * @packageDocumentation + */ + import { fileURLToPath } from "node:url"; import * as acp from "@agentclientprotocol/sdk"; @@ -11,13 +17,29 @@ import type { ToolSet } from "ai"; import { loadConfig, type FledglingConfig, type McpServerConfig, type ResolvedMcpServer } from "./config.js"; +export type { FledglingConfig, McpServerConfig } from "./config.js"; + +/** + * Creates MCP-backed tools for Fledgling sessions running in Node.js. + */ export class NodeMcpToolProvider implements IToolProvider { readonly #configPromise: Promise; + /** + * Initializes the provider with a configuration promise. + * + * @param configPromise - Resolves to MCP server configuration loaded from the host environment. + */ public constructor(configPromise: Promise = loadConfig()) { this.#configPromise = configPromise; } + /** + * Resolves configured and client-provided MCP servers into tools for a session. + * + * @param request - Session context and ACP-provided MCP server declarations. + * @returns MCP clients and tools that should be closed when the session ends. + */ public async createSessionTools(request: { readonly cwd: string | undefined; readonly mcpServers: acp.McpServer[]; diff --git a/packages/web-agent/config/api-extractor.json b/packages/web-agent/config/api-extractor.json new file mode 100644 index 0000000..abf87f1 --- /dev/null +++ b/packages/web-agent/config/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "@fledgling/heft-rig/profiles/default/config/api-extractor-base.json" +} diff --git a/packages/web-agent/src/dependencies.ts b/packages/web-agent/src/dependencies.ts index 722d9af..e6073fb 100644 --- a/packages/web-agent/src/dependencies.ts +++ b/packages/web-agent/src/dependencies.ts @@ -12,12 +12,38 @@ import { LocalStorageSessionManager } from "@fledgling/session-local-storage"; import { WebMcpToolProvider } from "./tool-provider.js"; +/** + * Options used to compose a browser-hosted Fledgling agent. + * + * @public + */ export interface WebAgentOptions { + /** + * Browser workspace runtime that backs the agent's MCP workspace tools. + */ readonly workspaceRuntime: IWorkspaceRuntime; + + /** + * Model turn runner used to generate agent responses. + */ readonly modelTurnRunner: IModelTurnRunner; + + /** + * Web Storage implementation used to persist session events. + * + * Defaults to `globalThis.localStorage`. + */ readonly storage?: Storage; } +/** + * Creates the default browser dependencies for a Fledgling agent. + * + * @param options - Browser runtime, model, and optional storage configuration. + * @returns Dependencies wired for local storage sessions and browser MCP tools. + * + * @public + */ export function createWebAgentDependencies(options: WebAgentOptions): FledglingAgentDependencies { return { sessionManager: new LocalStorageSessionManager({ storage: options.storage }), @@ -29,6 +55,15 @@ export function createWebAgentDependencies(options: WebAgentOptions): FledglingA }; } +/** + * Creates a Fledgling agent with browser-oriented default dependencies. + * + * @param connection - ACP connection used by the agent. + * @param options - Browser runtime, model, and optional storage configuration. + * @returns A Fledgling agent ready to run against the supplied connection. + * + * @public + */ export function createWebAgent(connection: acp.AgentSideConnection, options: WebAgentOptions): FledglingAgent { return new FledglingAgent(connection, createWebAgentDependencies(options)); } diff --git a/packages/web-agent/src/index.ts b/packages/web-agent/src/index.ts index 6966fe8..a09501f 100644 --- a/packages/web-agent/src/index.ts +++ b/packages/web-agent/src/index.ts @@ -1,3 +1,9 @@ +/** + * Browser-ready Fledgling agent composition helpers. + * + * @packageDocumentation + */ + export * from "@fledgling/agent-core"; export * from "@fledgling/mcp-workspace-browser"; export * from "./dependencies.js"; diff --git a/packages/web-agent/src/tool-provider.ts b/packages/web-agent/src/tool-provider.ts index 4508115..8240681 100644 --- a/packages/web-agent/src/tool-provider.ts +++ b/packages/web-agent/src/tool-provider.ts @@ -8,7 +8,15 @@ import type * as acp from "@agentclientprotocol/sdk"; import type { WebWorkspaceSidecar } from "@fledgling/mcp-workspace-browser"; import type { ToolSet } from "ai"; +/** + * Options for configuring a {@link WebMcpToolProvider}. + * + * @public + */ export interface WebMcpToolProviderOptions { + /** + * Creates the browser workspace sidecar used to host MCP tools for a session. + */ readonly createSidecar: () => Promise; } @@ -27,13 +35,29 @@ class WebMcpClientHandle { } } +/** + * Provides MCP workspace tools backed by a browser sidecar. + * + * @public + */ export class WebMcpToolProvider implements IToolProvider { readonly #createSidecar: () => Promise; + /** + * Creates a browser MCP tool provider. + * + * @param options - Sidecar factory used when session tools are requested. + */ public constructor(options: WebMcpToolProviderOptions) { this.#createSidecar = options.createSidecar; } + /** + * Creates MCP clients and AI SDK tools for an agent session. + * + * @param _request - Session context supplied by the agent core. + * @returns MCP clients and tools that should be closed when the session ends. + */ public async createSessionTools(_request: { readonly cwd: string | undefined; readonly mcpServers: acp.McpServer[]; diff --git a/packages/workspace-nodepod/config/api-extractor.json b/packages/workspace-nodepod/config/api-extractor.json new file mode 100644 index 0000000..abf87f1 --- /dev/null +++ b/packages/workspace-nodepod/config/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "@fledgling/heft-rig/profiles/default/config/api-extractor-base.json" +} diff --git a/packages/workspace-nodepod/src/index.ts b/packages/workspace-nodepod/src/index.ts index 01ddd67..2799e26 100644 --- a/packages/workspace-nodepod/src/index.ts +++ b/packages/workspace-nodepod/src/index.ts @@ -1 +1,6 @@ +/** + * Nodepod-backed workspace runtime for browser-hosted Fledgling workspaces. + * + * @packageDocumentation + */ export * from "./runtime.js"; diff --git a/packages/workspace-nodepod/src/runtime.ts b/packages/workspace-nodepod/src/runtime.ts index 3f93f17..446739b 100644 --- a/packages/workspace-nodepod/src/runtime.ts +++ b/packages/workspace-nodepod/src/runtime.ts @@ -13,45 +13,112 @@ import { const SKIPPED_SEARCH_DIRECTORIES = new Set(["node_modules", ".git", ".cache", "dist", "lib", "coverage", "temp"]); -interface NodepodLike { +/** + * Minimal Nodepod surface required by {@link NodepodWorkspaceRuntime}. + * + * This allows callers and tests to provide a compatible Nodepod instance + * without depending on the full concrete `Nodepod` class shape. + * + * @public + */ +export interface NodepodLike { + /** + * Filesystem API used to read, write, list, stat, and create workspace files. + */ readonly fs: Nodepod["fs"]; + /** + * Spawns a command in the Nodepod environment. + */ spawn: Nodepod["spawn"]; + /** + * Runs a shell command in the Nodepod environment when supported by the + * installed Nodepod version. + */ run?: ( command: string, options?: { + /** + * Working directory for the command. + */ readonly cwd?: string; + /** + * Signal used to cancel the command. + */ readonly signal?: AbortSignal; + /** + * Receives stdout chunks as the command runs. + */ readonly onStdout?: (chunk: string) => void; + /** + * Receives stderr chunks as the command runs. + */ readonly onStderr?: (chunk: string) => void; } ) => Promise<{ readonly stdout: string; readonly stderr: string; readonly exitCode: number }>; + /** + * Releases resources held by the Nodepod instance. + */ teardown(): void; } +/** + * Options used when booting a Nodepod-backed workspace runtime. + * + * @public + */ export interface NodepodWorkspaceRuntimeOptions { + /** + * Initial files to populate in the workspace. + */ readonly files?: Record; + /** + * Initial working directory for the Nodepod instance. + */ readonly workdir?: string; + /** + * Whether Nodepod should use its service worker integration. + */ readonly serviceWorker?: boolean; + /** + * Called when the Nodepod server is ready to accept browser connections. + */ readonly onServerReady?: (port: number, url: string) => void; } +/** + * Browser workspace runtime backed by a Nodepod instance. + * + * @public + */ export class NodepodWorkspaceRuntime implements IWorkspaceRuntime { readonly #nodepod: NodepodLike; + /** + * Creates a runtime around an existing Nodepod-compatible instance. + */ public constructor(nodepod: NodepodLike) { this.#nodepod = nodepod; } + /** + * Reads a UTF-8 file from the workspace. + */ public async readFile(path: string): Promise { return this.#nodepod.fs.readFile(normalizeAbsoluteWorkspacePath(path), "utf8"); } + /** + * Writes a file to the workspace, creating parent directories as needed. + */ public async writeFile(path: string, content: string): Promise { const target = normalizeAbsoluteWorkspacePath(path); await this.#ensureParentDirectory(target); await this.#nodepod.fs.writeFile(target, content); } + /** + * Lists files and directories immediately under a workspace path. + */ public async listDirectory(path: string): Promise { const root = normalizeAbsoluteWorkspacePath(path); const entries = await this.#nodepod.fs.readdir(root); @@ -81,12 +148,18 @@ export class NodepodWorkspaceRuntime implements IWorkspaceRuntime { return result; } + /** + * Searches workspace text files for a query string. + */ public async searchText(query: string, path: string): Promise { const matches: SearchMatch[] = []; await this.#searchDirectory(normalizeAbsoluteWorkspacePath(path), query, matches); return matches; } + /** + * Runs a command in the workspace. + */ public async runCommand( command: string, cwd: string, @@ -149,6 +222,9 @@ export class NodepodWorkspaceRuntime implements IWorkspaceRuntime { } } + /** + * Tears down the underlying Nodepod instance. + */ public dispose(): void { this.#nodepod.teardown(); } @@ -177,6 +253,11 @@ export class NodepodWorkspaceRuntime implements IWorkspaceRuntime { } } +/** + * Boots a Nodepod instance and wraps it in a workspace runtime. + * + * @public + */ export async function createNodepodWorkspaceRuntime( options: NodepodWorkspaceRuntimeOptions = {} ): Promise { diff --git a/rigs/heft-rig/package.json b/rigs/heft-rig/package.json index 8fcac66..6824d6d 100644 --- a/rigs/heft-rig/package.json +++ b/rigs/heft-rig/package.json @@ -11,8 +11,10 @@ "dependencies": { "@rushstack/eslint-config": "4.6.4", "@rushstack/heft": "1.2.7", + "@rushstack/heft-api-extractor-plugin": "1.3.7", "@rushstack/heft-lint-plugin": "1.2.7", "@rushstack/heft-typescript-plugin": "1.3.2", + "@microsoft/api-extractor": "~7.57.7", "@vitest/coverage-v8": "^3.1.1", "eslint": "~9.39.4", "vitest": "^3.1.1", diff --git a/rigs/heft-rig/profiles/default/config/api-extractor-base.json b/rigs/heft-rig/profiles/default/config/api-extractor-base.json new file mode 100644 index 0000000..550811f --- /dev/null +++ b/rigs/heft-rig/profiles/default/config/api-extractor-base.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + "mainEntryPointFilePath": "/lib/index.d.ts", + + "apiReport": { + "enabled": true, + "reportFolder": "/../../common/reviews", + "reportVariants": ["public"] + }, + + "docModel": { + "enabled": true, + "apiJsonFilePath": "/../../common/temp/api/.api.json" + }, + + "dtsRollup": { + "enabled": false + }, + + "messages": { + "extractorMessageReporting": { + "ae-missing-release-tag": { + "logLevel": "none" + } + }, + "tsdocMessageReporting": { + "tsdoc-undefined-tag": { + "logLevel": "none" + } + } + } +} diff --git a/rigs/heft-rig/profiles/default/config/heft.json b/rigs/heft-rig/profiles/default/config/heft.json index 3881ebd..ee4fcab 100644 --- a/rigs/heft-rig/profiles/default/config/heft.json +++ b/rigs/heft-rig/profiles/default/config/heft.json @@ -18,6 +18,12 @@ "taskPlugin": { "pluginPackage": "@rushstack/heft-lint-plugin" } + }, + "api-extractor": { + "taskDependencies": ["typescript"], + "taskPlugin": { + "pluginPackage": "@rushstack/heft-api-extractor-plugin" + } } } }