From d517886a574979c8071a584094188e9472e1736f Mon Sep 17 00:00:00 2001 From: Marcel Wege Date: Thu, 20 Aug 2026 16:06:54 +0200 Subject: [PATCH 1/2] feat(plugin-api): golden .d.ts API snapshot test (epic #470 C1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @omadia/plugin-api is the type contract the kernel and every plugin compile against, and a breaking change to it is currently invisible at the moment it is made: every consumer lives in this repo, so they are all recompiled in the same commit and tsc stays green while a renamed method or a narrowed type quietly changes what a plugin must build against. Once plugins ship from their own repositories that silence becomes an install-time incident in another repo. Compile the package's declarations into a temp dir, normalize them (comments stripped, blank lines dropped, whitespace collapsed, files in sorted path order) and compare against a committed snapshot. Drift fails with a unified diff naming both the regeneration command and the SemVer bump it implies. - scripts/api-snapshot.mjs --check (default) / --update - api-snapshot/plugin-api.d.ts.snap 34 declaration files, 2331 lines - test/apiSnapshot.test.ts the node:test gate CI runs - README.md what to do when it goes red, with the SemVer table Declarations go to a temp dir rather than dist/, so the check cannot race the test files that import the compiled output and always measures the current src/ instead of a previous build. assertEmitCoverage derives its expectation from disk — every src/**/*.ts must produce a matching .d.ts — so a shifted rootDir cannot leave the check green while it covers less than the package. The middleware root test glob only reaches test/**, never workspace package tests, so the package test dir is added as a second positional glob to that same script. No new required CI job; the existing "middleware (lint + typecheck + test)" job runs the gate. tsconfig.json has rootDir: src, which left the new test tree outside every project. tsconfig.test.json closes that and is folded into the package typecheck. No publishing — the package stays private: true (D1 unchanged). Mutation proof: an added export and a removed re-export each fail the check and the node:test gate with exit 1; reverting restores green. Core-decoupling ratchet unchanged at 3296. --- middleware/package.json | 2 +- middleware/packages/plugin-api/README.md | 56 + .../api-snapshot/plugin-api.d.ts.snap | 2331 +++++++++++++++++ middleware/packages/plugin-api/package.json | 5 +- .../plugin-api/scripts/api-snapshot.mjs | 405 +++ .../plugin-api/test/apiSnapshot.test.ts | 46 + .../packages/plugin-api/tsconfig.test.json | 20 + specs/470-dev-platform-plugin/README.md | 22 + 8 files changed, 2885 insertions(+), 2 deletions(-) create mode 100644 middleware/packages/plugin-api/README.md create mode 100644 middleware/packages/plugin-api/api-snapshot/plugin-api.d.ts.snap create mode 100644 middleware/packages/plugin-api/scripts/api-snapshot.mjs create mode 100644 middleware/packages/plugin-api/test/apiSnapshot.test.ts create mode 100644 middleware/packages/plugin-api/tsconfig.test.json diff --git a/middleware/package.json b/middleware/package.json index dbaf67aeb..5044c828e 100644 --- a/middleware/package.json +++ b/middleware/package.json @@ -43,7 +43,7 @@ "eval:adversarial": "node --import tsx test/adversarial/adversarialSuite.eval.ts", "setup:tigris-lifecycle": "tsx scripts/setup-tigris-lifecycle.ts", "pretest": "node scripts/check-node-version.mjs", - "test": "node --import tsx --test --test-timeout=120000 --test-concurrency=4 --test-reporter=spec --test-reporter-destination=stdout --test-reporter=./scripts/testFileDurations.reporter.mjs --test-reporter-destination=test-file-durations.json 'test/**/*.test.ts'", + "test": "node --import tsx --test --test-timeout=120000 --test-concurrency=4 --test-reporter=spec --test-reporter-destination=stdout --test-reporter=./scripts/testFileDurations.reporter.mjs --test-reporter-destination=test-file-durations.json 'test/**/*.test.ts' 'packages/plugin-api/test/**/*.test.ts'", "test:filetimes": "node scripts/check-test-file-durations.mjs", "test:pg": "node --import tsx --test --test-timeout=120000 --test-concurrency=1 --test-reporter=spec 'test/**/*.pg.test.ts'", "test:updater": "node --test 'sidecars/updater/test/*.test.mjs'" diff --git a/middleware/packages/plugin-api/README.md b/middleware/packages/plugin-api/README.md new file mode 100644 index 000000000..844966f43 --- /dev/null +++ b/middleware/packages/plugin-api/README.md @@ -0,0 +1,56 @@ +# `@omadia/plugin-api` + +The shared type contract between the kernel and every plugin. The kernel imports it to build +`PluginContext`; each plugin imports it to describe what it needs. Nothing in here has a runtime +of its own beyond a handful of pure helpers and fixtures. + +The package is `private: true` and is not published. Consumers inside this repo resolve it +through the npm workspace; out-of-repo plugins consume it by `file:` link, a vendored `.d.ts`, or +a git tag. + +## The API surface is machine-checked + +`api-snapshot/plugin-api.d.ts.snap` is a golden snapshot of every declaration this package emits: +comments stripped, blank lines dropped, whitespace collapsed, files concatenated in sorted path +order. `test/apiSnapshot.test.ts` regenerates it from the current `src/` and fails on any +difference. + +It exists because a breaking change here is invisible at the moment it is made. Every consumer +lives in this repo today and gets recompiled in the same commit, so `tsc` stays green while a +renamed method or a narrowed parameter quietly changes what a plugin must compile against. Once +plugins ship from their own repositories that silence becomes someone else's install-time +incident. The snapshot turns it into a diff in the PR that causes it. + +```bash +npm run api:check -w packages/plugin-api # what CI runs +npm run api:update -w packages/plugin-api # accept the new surface +``` + +The declarations are compiled into a temporary directory, never into `dist/`, so the check always +measures the current source and never races the compiled output other suites import. + +## What to do when the check fails + +A red snapshot check is not a request to run `api:update` and move on. It is the one place the +change is visible, so read the diff first and decide what it means. + +1. **Unintended?** Fix the source. That is the whole point of the gate. +2. **Intended?** Run `npm run api:update -w packages/plugin-api`, commit the regenerated snapshot + in the same commit as the source change, and bump `version` in `package.json`: + +| Change in the diff | Bump | +| --- | --- | +| Symbol removed or renamed; parameter added; type narrowed; optional field made required | **MAJOR** — every consumer must be checked | +| Symbol added; required field made optional; type widened | **MINOR** — existing consumers keep compiling | +| Nothing (the diff is empty) | none | + +SemVer is load-bearing here rather than decorative: after the split it is the only signal an +out-of-repo plugin gets about whether its pinned contract still holds. + +## Layout + +- `src/` — the contract. `index.ts` re-exports the modules that make up the public surface. +- `scripts/api-snapshot.mjs` — snapshot generator and checker. +- `api-snapshot/` — the committed golden snapshot. Generated; do not hand-edit. +- `test/` — the gate. Run standalone with `npm test -w packages/plugin-api`; CI runs it as part + of the middleware suite. diff --git a/middleware/packages/plugin-api/api-snapshot/plugin-api.d.ts.snap b/middleware/packages/plugin-api/api-snapshot/plugin-api.d.ts.snap new file mode 100644 index 000000000..8330bc111 --- /dev/null +++ b/middleware/packages/plugin-api/api-snapshot/plugin-api.d.ts.snap @@ -0,0 +1,2331 @@ +// Golden API snapshot for @omadia/plugin-api — generated, do not hand-edit. +// Regenerate deliberately: npm run api:update -w packages/plugin-api + +// ===== agentGraph.d.ts ===== +export interface CanvasPosition { +readonly x: number; +readonly y: number; +} +export type ModelRoutingMode = 'single' | 'triage'; +export type EscalationTrigger = 'tool_error' | 'long_context' | 'low_confidence'; +export interface ModelRoutingConfig { +readonly mode: ModelRoutingMode; +readonly main: string; +readonly triage?: string; +readonly simple?: string; +readonly escalateOn?: readonly EscalationTrigger[]; +} +export interface AgentNode { +readonly id: string; +readonly slug: string; +readonly name: string; +readonly description: string | null; +readonly privacyProfile: 'strict' | 'default'; +readonly status: 'enabled' | 'disabled'; +readonly modelRouting: ModelRoutingConfig | null; +readonly position: CanvasPosition | null; +} +export interface ChannelNode { +readonly channelType: string; +readonly channelKey: string; +readonly position: CanvasPosition | null; +} +export interface SubAgentNode { +readonly id: string; +readonly parentAgentId: string; +readonly name: string; +readonly skillId: string | null; +readonly model: string | null; +readonly maxTokens: number | null; +readonly maxIterations: number | null; +readonly systemPromptOverride: string | null; +readonly status: 'enabled' | 'disabled'; +readonly position: CanvasPosition | null; +} +export type SkillSource = 'db' | 'file'; +export interface SkillNode { +readonly id: string; +readonly slug: string; +readonly name: string; +readonly description: string | null; +readonly body: string; +readonly source: SkillSource; +} +export type ToolKind = 'native' | 'mcp'; +export interface ToolGrantNode { +readonly id: string; +readonly agentId: string | null; +readonly subAgentId: string | null; +readonly toolKind: ToolKind; +readonly toolRef: string; +readonly mcpServerId: string | null; +} +export type McpTransport = 'stdio' | 'http' | 'sse'; +export interface McpDiscoveredTool { +readonly name: string; +readonly description?: string; +readonly inputSchema?: Record; +readonly outputSchema?: Record; +} +export interface McpServerNode { +readonly id: string; +readonly name: string; +readonly transport: McpTransport; +readonly endpoint: string | null; +readonly status: 'enabled' | 'disabled'; +readonly lastDiscoveredAt: string | null; +readonly discoveredTools: readonly McpDiscoveredTool[]; +} +export interface ScheduleNode { +readonly id: string; +readonly agentId: string; +readonly cron: string; +readonly timezone: string; +readonly payload: Record; +readonly status: 'enabled' | 'disabled'; +readonly lastRunAt: string | null; +} +export type EdgeKind = 'channel_bind' | 'subagent' | 'skill' | 'tool_grant' | 'schedule'; +export interface CanvasEdge { +readonly id: string; +readonly kind: EdgeKind; +readonly source: string; +readonly target: string; +} +export interface AgentGraph { +readonly agent: AgentNode; +readonly channels: readonly ChannelNode[]; +readonly subAgents: readonly SubAgentNode[]; +readonly skills: readonly SkillNode[]; +readonly tools: readonly ToolGrantNode[]; +readonly mcpServers: readonly McpServerNode[]; +readonly schedules: readonly ScheduleNode[]; +readonly edges: readonly CanvasEdge[]; +} +export interface CreateEdgeRequest { +readonly kind: EdgeKind; +readonly source: string; +readonly target: string; +readonly config?: Record; +} + +// ===== agentPriorities.d.ts ===== +export declare const AGENT_PRIORITIES_SERVICE_NAME = "agentPriorities"; +export declare const AGENT_PRIORITIES_CAPABILITY = "agentPriorities@1"; +export interface AgentPriorityRecord { +readonly agentId: string; +readonly entryExternalId: string; +readonly action: 'block' | 'boost'; +readonly weight: number; +readonly reason: string | null; +readonly updatedAt: string; +} +export type AgentPriorityUpsert = Omit; +export interface AgentPrioritiesStore { +listForAgent(agentId: string): Promise; +upsert(record: AgentPriorityUpsert): Promise; +remove(agentId: string, entryExternalId: string): Promise; +} +export declare class NoopAgentPrioritiesStore implements AgentPrioritiesStore { +listForAgent(_agentId: string): Promise; +upsert(_record: AgentPriorityUpsert): Promise; +remove(_agentId: string, _entryExternalId: string): Promise; +} + +// ===== bulkInconsistency.d.ts ===== +export interface BulkInconsistencyPreview { +unchecked: number; +alreadyChecked: number; +withoutEmbedding: number; +detectorAvailable: boolean; +} +export interface BulkInconsistencyRunOptions { +limit?: number; +} +export interface BulkInconsistencyResult { +scanned: number; +checked: number; +inconsistenciesCreated: number; +skippedNoEmbedding: number; +failed: number; +durationMs: number; +} +export interface BulkInconsistencyService { +preview(): Promise; +run(options?: BulkInconsistencyRunOptions): Promise; +} +export declare const BULK_INCONSISTENCY_SERVICE_NAME = "bulkInconsistencyDetect"; +export declare const BULK_INCONSISTENCY_CAPABILITY = "bulkInconsistencyDetect@1"; + +// ===== bulkPromotion.d.ts ===== +export interface BulkPromotePreview { +nullSignificanceCount: number; +eligibleForPromoteCount: number; +alreadyPromotedCount: number; +scorerAvailable: boolean; +threshold: number; +} +export interface BulkPromoteRunOptions { +scoreLimit?: number; +promoteLimit?: number; +threshold?: number; +} +export interface BulkScorePhaseResult { +scanned: number; +scored: number; +failed: number; +} +export interface BulkPromotePhaseResult { +scanned: number; +promoted: number; +alreadyPromoted: number; +belowThreshold: number; +failed: number; +} +export interface BulkPromoteRunResult { +scorePhase: BulkScorePhaseResult; +promotePhase: BulkPromotePhaseResult; +durationMs: number; +} +export interface BulkPromotionService { +preview(threshold: number): Promise; +run(options?: BulkPromoteRunOptions): Promise; +} +export declare const BULK_PROMOTION_SERVICE_NAME = "bulkPromotion"; +export declare const BULK_PROMOTION_CAPABILITY = "bulkPromotion@1"; + +// ===== conductorApproval.d.ts ===== +export interface ApprovalReminder { +awaitId: string; +runId: string; +question: string; +workflowName: string; +stepLabel: string; +stepIndex?: number; +totalSteps?: number; +quorum: 'any' | 'all'; +} +export declare const CONDUCTOR_AWAIT_RESOLVER_SERVICE_NAME = "conductorAwaitResolver"; +export type ConductorAwaitOutcome = 'resumed' | 'recorded' | 'already_resolved' | 'not_a_holder'; +export interface ConductorAwaitResolver { +resolve(awaitId: string, responderId: string, approved: boolean): Promise; +} + +// ===== conversation.d.ts ===== +export interface ConversationTurn { +userMessage: string; +assistantAnswer: string; +at: number; +} + +// ===== embeddingClient.d.ts ===== +export interface EmbeddingClient { +embed(text: string): Promise; +} +export interface EmbeddingProviderMetadata { +readonly modelId: string; +readonly dimensions: number; +} +export interface EmbeddingProvider extends EmbeddingClient, EmbeddingProviderMetadata { +} +export declare class EmbeddingError extends Error { +readonly status?: number | undefined; +readonly body?: string | undefined; +constructor(message: string, status?: number | undefined, body?: string | undefined); +} +export declare function withConcurrencyLimit(client: T, max: number): T; +export declare function readEmbeddingProviderMetadata(client: EmbeddingClient | undefined): EmbeddingProviderMetadata | undefined; + +// ===== entityRef.d.ts ===== +export interface EntityRef { +system: string; +model: string; +id: string | number; +displayName?: string; +op: 'read' | 'write'; +} + +// ===== entityRefBus.d.ts ===== +import type { EntityRef } from './entityRef.js'; +export interface EntityRefBusOptions { +getCurrentTurnId?: () => string | undefined; +} +export declare class EntityRefBus { +private readonly emitter; +private readonly getCurrentTurnId; +constructor(opts?: EntityRefBusOptions); +publish(ref: EntityRef): void; +beginCollection(turnId: string): EntityRefCollection; +} +export interface EntityRefCollection { +drain(): EntityRef[]; +} + +// ===== excerptMerge.d.ts ===== +export type ExcerptMergeStatus = 'open' | 'resolved' | 'dismissed'; +export type ExcerptMergeResolution = +'keep_a' +| 'keep_b' +| 'not_duplicate'; +export interface ExcerptMergeCandidateNode { +id: string; +type: 'ExcerptMergeCandidate'; +props: { +cosine_sim: number; +status: ExcerptMergeStatus; +resolution: ExcerptMergeResolution | null; +created_at: string; +resolved_at: string | null; +resolved_by: string | null; +}; +duplicateExcerptOf: [string, string]; +} +export interface ListExcerptMergeCandidatesOptions { +viewerOmadiaUserId: string; +status?: ExcerptMergeStatus; +limit?: number; +} +export interface CreateExcerptMergeCandidateInput { +excerptAExternalId: string; +excerptBExternalId: string; +cosineSim: number; +} +export interface BulkExcerptMergeDetectPreview { +unchecked: number; +alreadyChecked: number; +withoutEmbedding: number; +detectorAvailable: boolean; +} +export interface BulkExcerptMergeDetectRunOptions { +limit?: number; +} +export interface BulkExcerptMergeDetectResult { +scanned: number; +checked: number; +excerptMergeCandidatesCreated: number; +skippedNoEmbedding: number; +failed: number; +durationMs: number; +} +export interface BulkExcerptMergeDetectService { +preview(): Promise; +run(options?: BulkExcerptMergeDetectRunOptions): Promise; +} +export declare const BULK_EXCERPT_MERGE_DETECT_SERVICE_NAME = "bulkExcerptMergeDetect"; +export declare const BULK_EXCERPT_MERGE_DETECT_CAPABILITY = "bulkExcerptMergeDetect@1"; + +// ===== inconsistency.d.ts ===== +export type InconsistencyStatus = 'open' | 'resolved' | 'dismissed'; +export type InconsistencyResolution = +'a_wins' +| 'b_wins' +| 'both' +| 'dismiss'; +export type InconsistencySeverity = 'low' | 'medium' | 'high'; +export interface InconsistencyNode { +id: string; +type: 'Inconsistency'; +props: { +summary: string; +severity: InconsistencySeverity; +status: InconsistencyStatus; +resolution: InconsistencyResolution | null; +created_at: string; +resolved_at: string | null; +resolved_by: string | null; +}; +conflictsWith: [string, string]; +} +export interface ListInconsistenciesOptions { +viewerOmadiaUserId: string; +status?: InconsistencyStatus; +limit?: number; +} +export interface CreateInconsistencyInput { +mkAExternalId: string; +mkBExternalId: string; +summary: string; +severity: InconsistencySeverity; +} +export interface InconsistencyDetectorService { +detectFor(memorableKnowledgeNodeId: string): Promise<{ +candidatesScanned: number; +inconsistenciesCreated: number; +}>; +} +export declare const INCONSISTENCY_DETECTOR_SERVICE_NAME = "inconsistencyDetector"; +export declare const INCONSISTENCY_DETECTOR_CAPABILITY = "inconsistencyDetector@1"; + +// ===== index.d.ts ===== +export * from './pluginContext.js'; +export * from './pkce.js'; +export * from './conversation.js'; +export * from './limitSignal.js'; +export * from './selfExtend.js'; +export * from './localSubAgentTool.js'; +export * from './piiAnnotation.js'; +export * from './targetRef.js'; +export * from './writeCapabilities.js'; +export * from './agentGraph.js'; +export * from './entityRef.js'; +export * from './entityRefBus.js'; +export * from './knowledgeGraph.js'; +export * from './embeddingClient.js'; +export * from './responseGuard.js'; +export * from './agentPriorities.js'; +export * from './privacyReceipt.js'; +export * from './privacyReceiptFixtures.js'; +export * from './turnReceiptStore.js'; +export * from './privacyMode.js'; +export * from './sessionBriefing.js'; +export * from './processMemory.js'; +export * from './nudge.js'; +export * from './routinesIntegration.js'; +export * from './conductorApproval.js'; +export * from './routineTarget.js'; +export * from './palaiaExcerpt.js'; +export * from './bulkPromotion.js'; +export * from './inconsistency.js'; +export * from './bulkInconsistency.js'; +export * from './mergeCandidate.js'; +export * from './topic.js'; +export * from './excerptMerge.js'; + +// ===== knowledgeGraph.d.ts ===== +import type { EntityRef } from './entityRef.js'; +import type { CreateInconsistencyInput, InconsistencyNode, InconsistencyResolution, InconsistencyStatus, ListInconsistenciesOptions } from './inconsistency.js'; +import type { CreateMergeCandidateInput, ListMergeCandidatesOptions, MergeCandidateNode, MergeCandidateResolution } from './mergeCandidate.js'; +import type { TopicNamingSource, TopicNode } from './topic.js'; +import type { CreateExcerptMergeCandidateInput, ExcerptMergeCandidateNode, ExcerptMergeResolution, ListExcerptMergeCandidatesOptions } from './excerptMerge.js'; +export interface KnowledgeGraph { +ingestTurn(turn: TurnIngest): Promise; +ingestEntities(entities: EntityIngest[]): Promise; +ingestFacts(facts: FactIngest[]): Promise; +ingestRun(trace: RunTrace): Promise; +ingestPlan(input: PlanIngest): Promise; +upsertPlanStep(input: PlanStepIngest): Promise; +getPlan(planExternalId: string): Promise; +getPlanSteps(planExternalId: string): Promise; +getPlanStepsForPlans(planExternalIds: string[]): Promise>; +setPlanStepStatus(stepExternalId: string, status: PlanStepStatus, opts?: { +resultSummary?: string; +}): Promise; +listPlansForScope(scope: string): Promise; +deletePlan(planExternalId: string): Promise; +listRecentPlans(opts: { +userId?: string; +limit?: number; +openOnly?: boolean; +agentScopePrefix?: string; +}): Promise; +getRunForTurn(turnExternalId: string): Promise; +getSession(scope: string): Promise; +listSessions(filter?: SessionFilter): Promise; +getNeighbors(nodeId: string): Promise; +stats(): Promise; +searchTurns(opts: SearchTurnsOptions): Promise; +findEntityCapturedTurns(opts: EntityCapturedTurnsOptions): Promise; +searchTurnsByEmbedding(opts: SearchTurnsByEmbeddingOptions): Promise; +findEntities(opts: FindEntitiesOptions): Promise; +resolveOrCreateChannelIdentity(ingest: ChannelIdentityIngest): Promise; +createMemorableKnowledge(input: MemorableKnowledgeIngest): Promise; +getMemorableKnowledge(memorableKnowledgeNodeId: string, viewerOmadiaUserId?: string): Promise; +listMemorableKnowledgeFor(omadiaUserId: string, opts?: ListMemorableKnowledgeOptions): Promise; +addOwner(memorableKnowledgeNodeId: string, omadiaUserIdToAdd: string, actor: AclMutationOptions): Promise; +removeOwner(memorableKnowledgeNodeId: string, omadiaUserIdToRemove: string, actor: AclMutationOptions): Promise; +deleteMemory(memorableKnowledgeNodeId: string, actor: AclMutationOptions): Promise; +countMemorableKnowledge(filter: MemorableKnowledgePurgeFilter): Promise<{ +count: number; +}>; +purgeMemorableKnowledge(filter: MemorableKnowledgePurgeFilter): Promise<{ +deletedNodes: number; +}>; +listMemoryAclAudit(memorableKnowledgeNodeId: string, opts?: { +limit?: number; +}): Promise; +updateMemorableKnowledge(memorableKnowledgeNodeId: string, patch: MemorableKnowledgeUpdate, actor: AclMutationOptions): Promise; +listExcerptsForMemory(memorableKnowledgeNodeId: string): Promise; +updateExcerpt(memorableKnowledgeNodeId: string, position: number, patch: PalaiaExcerptUpdate, actor: AclMutationOptions): Promise; +deleteExcerpt(memorableKnowledgeNodeId: string, position: number, actor: AclMutationOptions): Promise; +searchMemorableKnowledgeByEmbedding(opts: MemorableKnowledgeSearchOptions): Promise; +searchExcerptsByEmbedding(opts: ExcerptSearchOptions): Promise; +listInconsistencies(opts: ListInconsistenciesOptions): Promise; +getInconsistency(inconsistencyExternalId: string, viewerOmadiaUserId: string): Promise; +createInconsistency(input: CreateInconsistencyInput): Promise; +resolveInconsistency(inconsistencyExternalId: string, resolution: InconsistencyResolution, actor: AclMutationOptions): Promise; +listMemorableKnowledgeIdsForBulkInconsistencyCheck(opts: { +limit: number; +}): Promise; +countMemorableKnowledgeInconsistencyCheckBuckets(): Promise<{ +unchecked: number; +alreadyChecked: number; +withoutEmbedding: number; +}>; +markMemorableKnowledgeInconsistencyChecked(memorableKnowledgeNodeId: string): Promise; +listMergeCandidates(opts: ListMergeCandidatesOptions): Promise; +getMergeCandidate(mergeCandidateExternalId: string, viewerOmadiaUserId: string): Promise; +createMergeCandidate(input: CreateMergeCandidateInput): Promise; +resolveMergeCandidate(mergeCandidateExternalId: string, resolution: MergeCandidateResolution, actor: AclMutationOptions): Promise; +listMemorableKnowledgeIdsForBulkMergeCheck(opts: { +limit: number; +}): Promise; +countMemorableKnowledgeMergeCheckBuckets(): Promise<{ +unchecked: number; +alreadyChecked: number; +withoutEmbedding: number; +}>; +markMemorableKnowledgeMergeChecked(memorableKnowledgeNodeId: string): Promise; +listExcerptMergeCandidates(opts: ListExcerptMergeCandidatesOptions): Promise; +getExcerptMergeCandidate(externalId: string, viewerOmadiaUserId: string): Promise; +createExcerptMergeCandidate(input: CreateExcerptMergeCandidateInput): Promise; +resolveExcerptMergeCandidate(externalId: string, resolution: ExcerptMergeResolution, actor: AclMutationOptions): Promise; +listPalaiaExcerptIdsForBulkMergeCheck(opts: { +limit: number; +}): Promise; +countPalaiaExcerptMergeCheckBuckets(): Promise<{ +unchecked: number; +alreadyChecked: number; +withoutEmbedding: number; +}>; +markPalaiaExcerptMergeChecked(excerptExternalId: string): Promise; +listTopics(): Promise; +getTopic(topicExternalId: string): Promise; +listTopicMembers(topicExternalId: string): Promise; +listMemorableKnowledgeWithEmbeddings(): Promise>; +deleteAllTopics(): Promise; +createTopic(input: { +name: string; +description: string; +namingSource: TopicNamingSource; +memberMkIds: readonly string[]; +}): Promise; +listTopicMembershipEdges(): Promise>; +listAllIssues(opts?: { +status?: InconsistencyStatus; +}): Promise<{ +inconsistencies: InconsistencyNode[]; +mergeCandidates: MergeCandidateNode[]; +excerptMergeCandidates: ExcerptMergeCandidateNode[]; +edges: Array<{ +from: string; +to: string; +type: 'CONFLICTS_WITH' | 'DUPLICATE_OF' | 'DUPLICATE_EXCERPT_OF'; +}>; +}>; +listMemoriesForScope(scope: string | undefined, opts?: ListMemoriesForScopeOptions): Promise; +getMemorableKnowledgeSubgraph(rootExternalIds: string[], opts?: { +maxHops?: number; +maxNodes?: number; +}): Promise<{ +nodes: KgWalkNode[]; +edges: KgWalkEdge[]; +}>; +ingestDataset(input: DatasetIngest): Promise; +listDatasets(opts: { +ownerOmadiaUserId: string; +limit?: number; +}): Promise; +getDataset(datasetId: string, viewerOmadiaUserId: string): Promise; +queryDatasetRows(datasetId: string, viewerOmadiaUserId: string, opts?: DatasetQueryOptions): Promise; +deleteDataset(datasetId: string, actor: AclMutationOptions): Promise; +} +export interface KgWalkNode { +id: string; +label: string; +kind: string; +score?: number; +inserted?: boolean; +} +export interface KgWalkEdge { +from: string; +to: string; +type: string; +hop: number; +inserted?: boolean; +} +export interface KgWalkPayload { +rootIds: string[]; +nodes: KgWalkNode[]; +edges: KgWalkEdge[]; +} +export interface MemoryWithAncestors { +node: GraphNode; +level1: GraphNode[]; +level2: GraphNode[]; +} +export interface MemoryProvenanceEdge { +from: string; +to: string; +type: GraphEdgeType; +} +export interface MemoriesProvenanceView { +memories: MemoryWithAncestors[]; +edges: MemoryProvenanceEdge[]; +} +export interface ListMemoriesForScopeOptions { +limit?: number; +includeExcerpts?: boolean; +} +export interface MemorableKnowledgeSearchOptions { +queryEmbedding: number[]; +viewerOmadiaUserId: string; +limit?: number; +minSimilarity?: number; +teamVisibility?: boolean; +sharedOnly?: boolean; +viewerAgentSlug?: string; +manuallyAuthoredOnly?: boolean; +} +export interface MemorableKnowledgeHit { +mk: GraphNode; +cosineSim: number; +} +export interface ExcerptSearchOptions { +queryEmbedding: number[]; +viewerOmadiaUserId: string; +limit?: number; +minSimilarity?: number; +teamVisibility?: boolean; +sharedOnly?: boolean; +viewerAgentSlug?: string; +} +export interface PalaiaExcerptHit { +excerpt: PalaiaExcerptNode; +parentMkId: string; +cosineSim: number; +} +export interface RecalledPlan { +planId: string; +scope: string; +strategy?: string; +createdAt?: string; +openStepGoals: string[]; +completedStepGoals: string[]; +resumeFromInProgress: boolean; +resumeFromSideEffecting: boolean; +doneCount: number; +totalCount: number; +} +export interface RecalledProcess { +id: string; +title: string; +scope: string; +stepCount: number; +score: number; +} +export interface RecalledInsight { +mkId: string; +kind: string; +summary: string; +score: number; +durable?: boolean; +} +export interface RecalledContext { +plans: RecalledPlan[]; +processes: RecalledProcess[]; +insights: RecalledInsight[]; +} +export interface MemorableKnowledgeUpdate { +kind?: MemorableKind; +summary?: string; +rationale?: string | null; +significance?: number; +} +export interface SessionFilter { +userId?: string; +} +export type GraphNodeType = 'Session' | 'Turn' | 'OdooEntity' | 'ConfluencePage' +| 'PluginEntity' +| 'User' +| 'ChannelIdentity' | 'Run' | 'AgentInvocation' | 'ToolCall' | 'Fact' +| 'MemorableKnowledge' +| 'PalaiaExcerpt' +| 'Inconsistency' +| 'MergeCandidate' +| 'Topic' +| 'ExcerptMergeCandidate' +| 'Plan' +| 'PlanStep'; +export type GraphEdgeType = 'IN_SESSION' | 'NEXT_TURN' | 'CAPTURED' | 'BELONGS_TO' | 'EXECUTED' | 'INVOKED_AGENT' | 'INVOKED_TOOL' | 'PRODUCED' | 'DERIVED_FROM' | 'MENTIONS' +| 'IS_IDENTITY_OF' +| 'INVOLVED' +| 'REQUIRES' +| 'EXCERPT_OF' +| 'CONFLICTS_WITH' +| 'DUPLICATE_OF' +| 'HAS_TOPIC' +| 'DUPLICATE_EXCERPT_OF' +| 'STEP_OF' +| 'DEPENDS_ON' +| 'PLAN_OF'; +export type FactSeverity = 'info' | 'warning' | 'critical'; +export type EntryType = 'memory' | 'process' | 'task'; +export type Visibility = 'private' | 'team' | 'public' | `shared:${string}`; +export type Tier = 'HOT' | 'WARM' | 'COLD'; +export type TaskStatus = 'open' | 'done'; +export interface GraphNode { +id: string; +type: GraphNodeType; +props: Readonly>; +entryType?: EntryType; +visibility?: Visibility; +tier?: Tier; +accessedAt?: string | null; +accessCount?: number; +decayScore?: number; +contentHash?: string | null; +manuallyAuthored?: boolean; +taskStatus?: TaskStatus | null; +significance?: number | null; +} +export interface GraphEdge { +type: GraphEdgeType; +from: string; +to: string; +props?: Readonly>; +} +export interface EntityIngest { +system: string; +model: string; +id: string | number; +displayName?: string; +extras?: Record; +} +export interface EntityIngestResult { +entityIds: string[]; +inserted: number; +updated: number; +} +export type ChannelKind = 'teams' | 'telegram' | 'slack' | 'email' | 'web'; +export interface ChannelIdentityIngest { +channelKind: ChannelKind; +channelUserId: string; +displayName?: string; +email?: string; +emailVerified?: boolean; +aadObjectId?: string; +authSubject?: { +provider: string; +providerUserId: string; +}; +internalChannelData?: Record; +} +export interface ResolveOrCreateChannelIdentityResult { +channelIdentityNodeId: string; +userNodeId: string; +omadiaUserId: string; +isNewIdentity: boolean; +isNewCluster: boolean; +clusterAuthSubject?: { +provider: string; +providerUserId: string; +}; +} +export declare function authSubjectProps(ingest: Pick): Record; +export type MemorableKind = 'decision' | 'insight' | 'preference' | 'reference'; +export interface MemorableKnowledgeIngest { +kind: MemorableKind; +summary: string; +rationale?: string; +significance?: number; +createdBy: string; +involvedOmadiaUserIds?: string[]; +requiredEntityIds?: string[]; +derivedFromTurnIds?: string[]; +aclOwners?: string[]; +visibility?: Visibility; +originAgent?: string; +actorOmadiaUserId?: string; +palaiaExcerpts?: PalaiaExcerptInput; +manuallyAuthored?: boolean; +} +export type ExcerptSource = 'llm' | 'hint' | 'fallback'; +export interface PalaiaExcerptInput { +texts: readonly string[]; +source: ExcerptSource; +} +export interface PalaiaExcerptNode { +id: string; +type: 'PalaiaExcerpt'; +props: { +text: string; +position: number; +source: ExcerptSource; +created_at: string; +}; +} +export interface PalaiaExcerptUpdate { +text?: string; +source?: ExcerptSource; +} +export interface MemorableKnowledgeIngestResult { +memorableKnowledgeNodeId: string; +skippedInvolved: number; +skippedRequired: number; +skippedDerivedFrom: number; +} +export interface ListMemorableKnowledgeOptions { +limit?: number; +kind?: MemorableKind; +} +export type AclAction = 'create' | 'expand' | 'shrink' | 'delete' | 'edit' | 'edit_excerpt' +| 'delete_excerpt'; +export interface AclAuditEntry { +id: string; +memoryExternalId: string; +actorOmadiaUserId: string; +actorChannelIdentityId?: string; +action: AclAction; +beforeOwners: string[]; +afterOwners: string[] | null; +reason?: string; +createdAt: string; +} +export interface AclMutationOptions { +actorOmadiaUserId: string; +actorChannelIdentityId?: string; +reason?: string; +} +export interface MemorableKnowledgePurgeFilter { +tenantId: string; +originAgent?: string; +aclOwner?: string; +} +export interface FactIngest { +factId: string; +sourceTurnId: string; +subject: string; +predicate: string; +object: string; +confidence?: number; +severity?: FactSeverity; +mentionedEntityIds?: string[]; +} +export interface FactIngestResult { +factIds: string[]; +inserted: number; +updated: number; +} +export interface TurnIngest { +scope: string; +time: string; +userMessage: string; +assistantAnswer: string; +toolCalls?: number; +iterations?: number; +entityRefs: EntityRef[]; +userId?: string; +entryType?: EntryType; +visibility?: Visibility; +significance?: number | null; +} +export interface TurnIngestResult { +sessionId: string; +turnId: string; +entityNodeIds: string[]; +} +export interface CaptureDisclosure { +persisted: boolean; +reasons: readonly string[]; +entryType: 'memory' | 'process' | 'task' | null; +visibility: string | null; +significance: number | null; +embedded: boolean; +privacyBlocksStripped: number; +hintTagsProcessed: number; +graphRefs?: { +sessionId: string; +turnId: string; +entityNodeIds: readonly string[]; +}; +} +export type RunStatus = 'success' | 'error'; +export interface RunToolCall { +callId: string; +toolName: string; +durationMs: number; +isError: boolean; +agentContext: string; +producedEntityIds?: string[]; +postcondition?: { +issues: readonly string[]; +}; +} +export interface RunAgentInvocation { +index: number; +agentName: string; +agentId?: string; +durationMs: number; +subIterations: number; +status: RunStatus; +toolCalls: RunToolCall[]; +} +export interface RunTrace { +turnId: string; +scope: string; +userId?: string; +startedAt: string; +finishedAt: string; +durationMs: number; +status: RunStatus; +iterations: number; +orchestratorToolCalls: RunToolCall[]; +agentInvocations: RunAgentInvocation[]; +error?: string; +model?: string; +provider?: string; +} +export interface RunIngestResult { +runId: string; +agentInvocationIds: string[]; +toolCallIds: string[]; +userNodeId?: string; +} +export type PlanStepStatus = 'pending' | 'in_progress' | 'done' | 'failed' | 'skipped'; +export interface PlanIngest { +planId: string; +scope: string; +turnExternalId?: string; +userId?: string; +strategy?: string; +createdBy?: 'gate' | 'manual' | 'process'; +createdAt: string; +requestSummary?: string; +} +export interface PlanIngestResult { +planExternalId: string; +} +export interface PlanDeleteResult { +deleted: boolean; +deletedSteps: number; +} +export interface PlanStepIngest { +stepId: string; +planId: string; +scope: string; +goal: string; +order: number; +status?: PlanStepStatus; +exitCondition?: string; +toolHint?: string; +dependsOnStepIds?: string[]; +sideEffecting?: boolean; +resultSummary?: string; +} +export interface PlanStepIngestResult { +stepExternalId: string; +} +export interface RunToolCallView { +node: GraphNode; +producedEntities: GraphNode[]; +} +export interface RunAgentInvocationView { +node: GraphNode; +toolCalls: RunToolCallView[]; +} +export interface RunTraceView { +turn: GraphNode; +run: GraphNode; +user?: GraphNode; +orchestratorToolCalls: RunToolCallView[]; +agentInvocations: RunAgentInvocationView[]; +} +export interface SessionSummary { +id: string; +scope: string; +turnCount: number; +firstAt: string; +lastAt: string; +} +export interface SessionView { +session: GraphNode; +turns: Array<{ +turn: GraphNode; +entities: GraphNode[]; +}>; +user?: GraphNode; +} +export interface GraphStats { +nodes: number; +edges: number; +byNodeType: Record; +byEdgeType: Record; +} +export interface SearchTurnsOptions { +query: string; +userId?: string; +excludeScope?: string; +excludeTurnIds?: readonly string[]; +agentScopePrefix?: string; +limit?: number; +} +export interface TurnSearchHit { +turnId: string; +scope: string; +time: string; +userMessage: string; +assistantAnswer: string; +rank: number; +entryType?: EntryType; +manuallyAuthored?: boolean; +} +export interface SearchTurnsByEmbeddingOptions { +queryEmbedding: readonly number[]; +userId?: string; +excludeScope?: string; +excludeTurnIds?: readonly string[]; +agentScopePrefix?: string; +limit?: number; +minSimilarity?: number; +ftsQuery?: string; +recallMinScore?: number; +recallRecencyBoost?: number; +typeWeights?: Partial>; +entryTypes?: readonly EntryType[]; +includeCold?: boolean; +} +export interface EntityCapturedTurnsOptions { +terms: readonly string[]; +userId?: string; +excludeScope?: string; +agentScopePrefix?: string; +perEntityLimit?: number; +entityLimit?: number; +} +export interface FindEntitiesOptions { +model: string; +nameContains?: string; +limit?: number; +} +export interface EntityCapturedTurnsHit { +entity: GraphNode; +turns: Array<{ +turnId: string; +scope: string; +time: string; +userMessage: string; +assistantAnswer: string; +}>; +} +export declare const AGENT_SCOPE_SEP = "::"; +export declare function qualifyScope(agentSlug: string, conversationScope: string): string; +export declare function agentScopePrefix(agentSlug: string): string; +export declare function sessionNodeId(scope: string): string; +export declare function turnNodeId(scope: string, time: string): string; +export declare function planNodeId(planId: string): string; +export declare function planStepNodeId(stepId: string): string; +export declare function entityNodeId(ref: EntityRef): string; +export declare function userNodeId(omadiaUserId: string): string; +export declare function channelIdentityNodeId(channelKind: ChannelKind, channelUserId: string): string; +export declare function memorableKnowledgeNodeId(memorableId: string): string; +export declare function palaiaExcerptNodeId(excerptId: string): string; +export declare function inconsistencyNodeId(inconsistencyId: string): string; +export declare function mergeCandidateNodeId(mergeId: string): string; +export declare function topicNodeId(topicId: string): string; +export declare function excerptMergeCandidateNodeId(id: string): string; +export declare function runNodeId(turnExternalId: string): string; +export declare function agentInvocationNodeId(turnExternalId: string, agentName: string, index: number): string; +export declare function toolCallNodeId(turnExternalId: string, callId: string): string; +export declare function factNodeId(sourceTurnId: string, subject: string, predicate: string, object: string): string; +export type DatasetColumnType = 'string' | 'number' | 'boolean' | 'date'; +export interface DatasetColumnSchema { +name: string; +type: DatasetColumnType; +sample?: string; +} +export interface DatasetIngest { +ownerOmadiaUserId: string; +name: string; +sourceFileName: string; +sourceStorageKey?: string; +columns: DatasetColumnSchema[]; +rows: ReadonlyArray>; +} +export interface DatasetIngestResult { +datasetId: string; +rowCount: number; +graphNodeId: string; +} +export interface DatasetSummary { +id: string; +name: string; +sourceFileName: string; +ownerOmadiaUserId: string; +rowCount: number; +columns: DatasetColumnSchema[]; +createdAt: string; +} +export type DatasetFilterOp = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains'; +export interface DatasetFilter { +column: string; +op: DatasetFilterOp; +value: string | number | boolean; +} +export type DatasetAggregateFn = 'count' | 'sum' | 'avg' | 'min' | 'max'; +export interface DatasetAggregate { +fn: DatasetAggregateFn; +column?: string; +} +export interface DatasetQueryOptions { +filters?: DatasetFilter[]; +groupBy?: string; +aggregate?: DatasetAggregate; +limit?: number; +offset?: number; +} +export interface DatasetQueryResult { +rows?: Array>; +groups?: Array<{ +key: unknown; +value: number | null; +}>; +aggregateValue?: number | null; +totalMatched: number; +} +export declare class DatasetQueryValidationError extends Error { +readonly code: string; +constructor(code: string, message: string); +} +export interface NormalizedDatasetQuery { +filters: DatasetFilter[]; +groupBy?: string; +aggregate?: DatasetAggregate; +limit: number; +offset: number; +} +export declare function validateDatasetQueryOptions(columns: readonly DatasetColumnSchema[], opts: DatasetQueryOptions | undefined): NormalizedDatasetQuery; + +// ===== limitSignal.d.ts ===== +export type LimitSignalKind = +'row_cap' +| 'page_truncated' +| 'unsupported_operation' +| 'rate_limited' +| 'missing_capability'; +export interface LimitSignal { +readonly kind: LimitSignalKind; +readonly detail: string; +readonly cap?: number; +readonly observed?: number; +readonly hint?: string; +} +export declare function makeLimitSignal(kind: LimitSignalKind, detail: string, extra?: { +cap?: number; +observed?: number; +hint?: string; +}): LimitSignal; +export declare function formatLimitSignalNote(signal: LimitSignal | undefined): string; +export declare function appendLimitSignalNote(output: string, signal: LimitSignal | undefined): string; + +// ===== localSubAgentTool.d.ts ===== +import type { LimitSignal } from './limitSignal.js'; +import type { ToolPIIField } from './piiAnnotation.js'; +export interface LocalSubAgentToolSpec { +name: string; +description: string; +input_schema: { +type: 'object'; +properties: Record; +required: string[]; +}; +} +export interface LocalSubAgentToolResult { +readonly output: string; +readonly postcondition?: { +readonly issues: readonly string[]; +}; +readonly structured?: StructuredToolOutput; +readonly limitSignal?: LimitSignal; +} +export interface StructuredToolOutput { +readonly kind: string; +readonly data: unknown; +readonly prose?: string; +} +export interface LocalSubAgentTool { +spec: LocalSubAgentToolSpec; +handle(input: unknown): Promise; +piiFields?: readonly ToolPIIField[]; +} + +// ===== mergeCandidate.d.ts ===== +export type MergeCandidateStatus = 'open' | 'resolved' | 'dismissed'; +export type MergeCandidateResolution = +'keep_a' +| 'keep_b' +| 'not_duplicate'; +export interface MergeCandidateNode { +id: string; +type: 'MergeCandidate'; +props: { +cosine_sim: number; +status: MergeCandidateStatus; +resolution: MergeCandidateResolution | null; +created_at: string; +resolved_at: string | null; +resolved_by: string | null; +}; +duplicateOf: [string, string]; +} +export interface ListMergeCandidatesOptions { +viewerOmadiaUserId: string; +status?: MergeCandidateStatus; +limit?: number; +} +export interface CreateMergeCandidateInput { +mkAExternalId: string; +mkBExternalId: string; +cosineSim: number; +} +export interface MergeCandidateDetectorService { +detectFor(memorableKnowledgeNodeId: string): Promise<{ +candidatesScanned: number; +mergeCandidatesCreated: number; +}>; +detectForExcerpt(excerptExternalId: string): Promise<{ +candidatesScanned: number; +excerptMergeCandidatesCreated: number; +}>; +} +export declare const MERGE_CANDIDATE_DETECTOR_SERVICE_NAME = "mergeCandidateDetector"; +export declare const MERGE_CANDIDATE_DETECTOR_CAPABILITY = "mergeCandidateDetector@1"; +export interface BulkMergeDetectPreview { +unchecked: number; +alreadyChecked: number; +withoutEmbedding: number; +detectorAvailable: boolean; +} +export interface BulkMergeDetectRunOptions { +limit?: number; +} +export interface BulkMergeDetectResult { +scanned: number; +checked: number; +mergeCandidatesCreated: number; +skippedNoEmbedding: number; +failed: number; +durationMs: number; +} +export interface BulkMergeDetectService { +preview(): Promise; +run(options?: BulkMergeDetectRunOptions): Promise; +} +export declare const BULK_MERGE_DETECT_SERVICE_NAME = "bulkMergeDetect"; +export declare const BULK_MERGE_DETECT_CAPABILITY = "bulkMergeDetect@1"; + +// ===== nudge.d.ts ===== +import type { ProcessMemoryService } from './processMemory.js'; +export declare const NUDGE_REGISTRY_SERVICE_NAME = "nudgeRegistry"; +export declare const NUDGE_REGISTRY_CAPABILITY = "nudgeRegistry@1"; +export declare const NUDGE_STATE_SERVICE_NAME = "nudgeStateStore"; +export declare const NUDGE_STATE_CAPABILITY = "nudgeStateStore@1"; +export declare const NUDGE_PROVIDERS_SERVICE_NAME = "nudgeProviders"; +export declare const NUDGE_PROVIDERS_CAPABILITY = "nudgeProviders@1"; +export interface ParsedNudge { +readonly id: string; +readonly text: string; +readonly cta?: NudgeCta; +} +export interface NudgeParseResult { +readonly cleaned: string; +readonly nudge: ParsedNudge | null; +} +export declare function parseNudge(content: string): NudgeParseResult; +export declare const NUDGE_PROVIDER_TIMEOUT_MS = 500; +export declare const NUDGE_MAX_PER_TURN = 3; +export declare const NUDGE_MAX_PER_TOOL_CALL = 1; +export declare const NUDGE_SUPPRESS_DEFAULT_DAYS = 7; +export declare const NUDGE_RETIRE_AFTER_STREAK = 3; +export declare const NUDGE_REGRESSION_AFTER_MISSES = 3; +export interface NudgeCta { +readonly label: string; +readonly toolCall: { +readonly name: string; +readonly arguments: Record; +}; +} +export type NudgeSuccessSignal = { +readonly kind: 'tool_call_after'; +readonly toolName: string; +readonly withinTurns: number; +}; +export interface Nudge { +readonly id: string; +readonly text: string; +readonly cta?: NudgeCta; +readonly successSignal?: NudgeSuccessSignal; +readonly workflowHash?: string; +} +export interface ReadonlyToolTraceEntry { +readonly toolName: string; +readonly args: unknown; +readonly result: string; +readonly status: 'ok' | 'error'; +readonly domain?: string; +readonly durationMs?: number; +} +export interface ReadonlyTurnContext { +readonly turnId: string; +readonly agentId: string; +readonly userMessage: string; +readonly toolTrace: readonly ReadonlyToolTraceEntry[]; +readonly sessionScope: string; +} +export interface NudgeStateRecord { +readonly agentId: string; +readonly nudgeId: string; +readonly successStreak: number; +readonly regressionCount: number; +readonly suppressedUntil: Date | null; +readonly retiredAt: Date | null; +readonly lastEmittedAt: Date | null; +readonly lastFollowedAt: Date | null; +} +export interface NudgeEmissionRecord { +readonly agentId: string; +readonly nudgeId: string; +readonly turnId: string; +readonly toolName: string; +readonly hintText: string; +readonly workflowHash?: string; +readonly cta?: NudgeCta; +} +export interface NudgeStateReader { +read(agentId: string, nudgeId: string): Promise; +} +export interface NudgeStateStore extends NudgeStateReader { +recordEmission(record: NudgeEmissionRecord): Promise; +recordFollow(agentId: string, nudgeId: string, turnId: string): Promise; +recordRegression(agentId: string, nudgeId: string): Promise; +suppress(agentId: string, nudgeId: string, until: Date): Promise; +} +export interface NudgeEvaluationInput { +readonly turnId: string; +readonly toolName: string; +readonly toolArgs: unknown; +readonly toolResult: string; +readonly turnContext: ReadonlyTurnContext; +readonly nudgeStateStore: NudgeStateReader; +readonly processMemory?: ProcessMemoryService; +} +export interface NudgeProvider { +readonly id: string; +readonly priority: number; +evaluate(input: NudgeEvaluationInput): Promise; +} +export interface NudgeRegistry { +register(provider: NudgeProvider): void; +list(): readonly NudgeProvider[]; +} +export declare class InMemoryNudgeRegistry implements NudgeRegistry { +private readonly providers; +register(provider: NudgeProvider): void; +list(): readonly NudgeProvider[]; +} +export declare class NoopNudgeStateStore implements NudgeStateStore { +read(_agentId: string, _nudgeId: string): Promise; +recordEmission(_record: NudgeEmissionRecord): Promise; +recordFollow(_agentId: string, _nudgeId: string, _turnId: string): Promise; +recordRegression(_agentId: string, _nudgeId: string): Promise; +suppress(_agentId: string, _nudgeId: string, _until: Date): Promise; +} + +// ===== palaiaExcerpt.d.ts ===== +import type { EntryType, MemorableKind } from './knowledgeGraph.js'; +export interface PalaiaExcerpt { +suggestedKind: MemorableKind; +suggestedSummary: string; +suggestedRationale?: string; +excerpts: readonly string[]; +source: 'llm' | 'hint' | 'fallback'; +} +export interface PalaiaExcerptExtractInput { +cleanedUserMessage: string; +cleanedAssistantAnswer: string; +significance?: number | null; +entryTypeHint?: EntryType; +} +export interface PalaiaExcerptExtractor { +extract(input: PalaiaExcerptExtractInput): Promise; +} +export declare const PALAIA_EXCERPT_SERVICE_NAME = "palaiaExcerpt"; +export declare const PALAIA_EXCERPT_CAPABILITY = "palaiaExcerpt@1"; + +// ===== piiAnnotation.d.ts ===== +export type PIIFieldType = 'PERSON' | 'EMAIL' | 'PHONE' | 'IBAN' | 'CARD' | 'ADDRESS' | 'ORG' | 'APIKEY'; +export interface ToolPIIField { +readonly path: string; +readonly idPath: string; +readonly type?: PIIFieldType; +} +export interface OdooMany2OneOptions { +readonly type?: PIIFieldType; +readonly recordsAt?: string; +} +export declare function odooMany2OnePiiField(field: string, options?: OdooMany2OneOptions): ToolPIIField; +export declare function odooSearchReadPiiFields(fields: Readonly>, options?: Pick): ToolPIIField[]; + +// ===== pkce.d.ts ===== +export declare function generateCodeVerifier(): string; +export declare function computeCodeChallenge(verifier: string): string; + +// ===== pluginContext.d.ts ===== +import type { Socket } from 'node:net'; +import type { WriteCapability } from './writeCapabilities.js'; +import type { EntityCapturedTurnsHit, EntityCapturedTurnsOptions, EntityIngest, EntityIngestResult, FactIngest, FactIngestResult, GraphNode, GraphStats, SearchTurnsOptions, TurnSearchHit } from './knowledgeGraph.js'; +export interface PluginContext { +readonly agentId: string; +readonly domain: string; +readonly secrets: SecretsAccessor; +readonly config: ConfigAccessor; +readonly services: ServicesAccessor; +readonly smokeMode: boolean; +readonly scratch?: ScratchDirAccessor; +readonly http?: HttpAccessor; +readonly net?: NetAccessor; +readonly memory?: MemoryAccessor; +readonly tools: ToolsAccessor; +readonly routes: RoutesAccessor; +readonly notifications: NotificationsAccessor; +readonly uiRoutes: UiRoutesAccessor; +readonly jobs: JobsAccessor; +readonly events?: EventsAccessor; +readonly subAgent?: SubAgentAccessor; +readonly knowledgeGraph?: KnowledgeGraphAccessor; +readonly llm?: LlmAccessor; +readonly mcp?: McpAccessor; +readonly flows?: FlowsAccessor; +readonly oauthTokens?: OAuthTokensAccessor; +readonly operatorAuth?: OperatorAuthAccessor; +readonly status: StatusAccessor; +log(...args: unknown[]): void; +} +export interface JobSpec { +readonly name: string; +readonly schedule: JobSchedule; +readonly timeoutMs?: number; +readonly overlap?: 'skip' | 'queue'; +} +export type JobSchedule = { +readonly cron: string; +} | { +readonly intervalMs: number; +}; +export declare const JOB_DEFAULT_TIMEOUT_MS = 30000; +export type JobHandler = (signal: AbortSignal) => Promise; +export interface JobsAccessor { +register(spec: JobSpec, handler: JobHandler): () => void; +} +export declare class JobValidationError extends Error { +constructor(message: string); +} +export declare class JobAlreadyRegisteredError extends Error { +constructor(agentId: string, name: string); +} +export interface EmitResult { +eventId: string; +matchedWorkflows: number; +startedRuns: Array<{ +workflowSlug: string; +runId: string; +}>; +} +export interface EventsAccessor { +emit(id: string, payload: Record): Promise; +} +export declare class EventNotDeclaredError extends Error { +constructor(agentId: string, eventId: string); +} +export declare class ConductorUnavailableError extends Error { +constructor(); +} +export interface CapabilityRef { +readonly name: string; +readonly major: number; +} +export declare class CapabilityParseError extends Error { +constructor(raw: string, detail: string); +} +export declare function parseCapabilityRef(raw: string): CapabilityRef; +export declare function capabilitiesMatch(provider: CapabilityRef, consumer: CapabilityRef): boolean; +export interface ServicesAccessor { +get(name: string): T | undefined; +has(name: string): boolean; +provide(name: string, impl: T): () => void; +replace(name: string, impl: T): () => void; +} +export interface NativeToolSpec { +readonly name: string; +readonly description: string; +readonly input_schema: { +readonly type: 'object'; +readonly properties: Record; +readonly required?: readonly string[]; +}; +readonly domain?: string; +} +export declare const PLUGIN_DOMAIN_REGEX: RegExp; +export declare function validatePluginDomain(value: unknown): { +ok: true; +domain: string; +} | { +ok: false; +message: string; +}; +export type NativeToolHandler = (input: unknown) => Promise; +export type NativeToolAttachmentSink = () => NativeToolAttachment[] | undefined; +export interface NativeToolAttachment { +readonly kind: string; +readonly payload: unknown; +} +export interface ToolsAccessor { +register(spec: NativeToolSpec, handler: NativeToolHandler, options?: ToolRegistrationOptions): () => void; +registerHandler(name: string, handler: NativeToolHandler, options?: ToolRegistrationOptions): () => void; +invoke?(name: string, input: unknown): Promise; +} +export interface ToolRegistrationOptions { +readonly promptDoc?: string; +readonly attachmentSink?: NativeToolAttachmentSink; +readonly writeCapabilities?: readonly WriteCapability[]; +} +export interface RoutesAccessor { +register(prefix: string, router: unknown): () => void; +} +export interface FlowsAccessor { +publicUrl(relPath: string, opts?: { +prefix?: string; +}): string; +signState(claims: Record, opts?: { +ttl?: string; +}): Promise; +verifyState(token: string): Promise>; +} +export type PluginActionState = 'ok' | 'needs_action' | 'error'; +export interface PluginActionStatus { +readonly state: PluginActionState; +readonly title?: string; +readonly detail?: string; +} +export interface StatusAccessor { +report(status: PluginActionStatus): void; +clear(): void; +} +export interface OAuthTokensAccessor { +get(fieldKey: string): Promise; +} +export type OAuthTokenErrorCode = 'not_connected' | 'refresh_failed'; +export interface OperatorAuthAccessor { +hasValidSession(cookieHeader: string | undefined): Promise; +} +export declare class OAuthTokenError extends Error { +readonly code: OAuthTokenErrorCode; +constructor(code: OAuthTokenErrorCode, message: string); +} +export interface UiRoutesAccessor { +register(descriptor: UiRouteDescriptorInput): () => void; +registerNav(entry: UiNavEntryInput): () => void; +} +export interface UiRouteDescriptorInput { +readonly routeId: string; +readonly path: string; +readonly title: string; +readonly description?: string; +readonly order?: number; +} +export interface UiRouteDescriptor extends UiRouteDescriptorInput { +readonly pluginId: string; +} +export interface UiNavEntryInput { +readonly navId: string; +readonly href: string; +readonly cluster?: string; +readonly order?: number; +readonly label: Readonly>; +} +export interface UiNavEntry extends UiNavEntryInput { +readonly pluginId: string; +} +export interface ResolvedUiNavEntry { +readonly pluginId: string; +readonly navId: string; +readonly href: string; +readonly cluster?: string; +readonly order: number; +readonly label: string; +} +export interface NotificationsAccessor { +send(payload: NotificationPayload): Promise; +registerChannel(channelId: string, handler: ChannelNotificationHandler): () => void; +} +export interface NotificationPayload { +readonly title: string; +readonly body: string; +readonly deepLink?: string; +readonly recipients?: 'broadcast' | readonly string[]; +} +export interface NotificationDispatchResult { +readonly delivered: readonly string[]; +readonly failed: readonly { +readonly channelId: string; +readonly error: string; +}[]; +readonly anyHandlerPresent: boolean; +} +export type ChannelNotificationHandler = (payload: ResolvedNotificationPayload) => Promise; +export interface ResolvedNotificationPayload { +readonly pluginId: string; +readonly title: string; +readonly body: string; +readonly deepLink?: string; +readonly recipients: 'broadcast' | readonly string[]; +} +export interface ScratchDirAccessor { +path(): Promise; +} +export interface HttpAccessor { +fetch(url: string, init?: RequestInit): Promise; +} +export declare class HttpForbiddenError extends Error { +constructor(agentId: string, host: string); +} +export declare class HttpRateLimitError extends Error { +constructor(agentId: string); +} +export interface NetConnectOptions { +readonly host: string; +readonly port: number; +readonly tls?: boolean; +readonly servername?: string; +} +export interface NetAccessor { +connect(options: NetConnectOptions): Promise; +} +export declare class NetForbiddenError extends Error { +constructor(agentId: string, target: string); +} +export declare class NetRateLimitError extends Error { +constructor(agentId: string); +} +export interface MemoryAccessor { +readFile(relPath: string): Promise; +writeFile(relPath: string, content: string): Promise; +createFile(relPath: string, content: string): Promise; +delete(relPath: string): Promise; +list(relPath: string): Promise; +exists(relPath: string): Promise; +} +export interface MemoryEntryInfo { +readonly relPath: string; +readonly isDirectory: boolean; +readonly sizeBytes: number; +} +export declare class MemoryPathError extends Error { +constructor(message: string); +} +export interface MemoryStore { +list(virtualPath: string): Promise; +fileExists(virtualPath: string): Promise; +directoryExists(virtualPath: string): Promise; +readFile(virtualPath: string): Promise; +createFile(virtualPath: string, content: string): Promise; +writeFile(virtualPath: string, content: string): Promise; +delete(virtualPath: string): Promise; +rename(fromVirtualPath: string, toVirtualPath: string): Promise; +} +export interface MemoryEntry { +virtualPath: string; +isDirectory: boolean; +sizeBytes: number; +} +export interface SecretsAccessor { +get(key: string): Promise; +require(key: string): Promise; +keys(): Promise; +set?(key: string, value: string): Promise; +delete?(key: string): Promise; +} +export interface SecretsReadWriteAccessor extends SecretsAccessor { +set(key: string, value: string): Promise; +delete(key: string): Promise; +} +export interface ConfigAccessor { +get(key: string): T | undefined; +require(key: string): T; +set?(key: string, value: unknown): Promise; +} +export interface SubAgentAccessor { +ask(targetAgentId: string, question: string): Promise; +has(targetAgentId: string): boolean; +list(): readonly string[]; +} +export declare class UnknownSubAgentError extends Error { +constructor(callerAgentId: string, targetAgentId: string); +} +export declare class SubAgentPermissionDeniedError extends Error { +constructor(callerAgentId: string, targetAgentId: string); +} +export declare class SubAgentRecursionError extends Error { +constructor(agentId: string); +} +export declare class SubAgentBudgetExceededError extends Error { +constructor(callerAgentId: string, budget: number); +} +export interface KnowledgeGraphAccessor { +ingestEntities(entities: EntityIngest[]): Promise; +ingestFacts(facts: FactIngest[]): Promise; +searchTurns(opts: SearchTurnsOptions): Promise; +findEntityCapturedTurns(opts: EntityCapturedTurnsOptions): Promise; +getNeighbors(nodeId: string): Promise; +stats(): Promise; +readonly entitySystems: readonly string[]; +} +export declare class KgEntityNamespaceError extends Error { +constructor(callerAgentId: string, system: string); +} +export declare class KgServiceUnavailableError extends Error { +constructor(callerAgentId: string); +} +export interface LlmAccessor { +complete(req: LlmCompleteRequest): Promise; +readonly modelsAllowed: readonly string[]; +} +export interface LlmCompleteRequest { +readonly model: string; +readonly system?: string; +readonly messages: ReadonlyArray<{ +readonly role: 'user' | 'assistant'; +readonly content: string; +}>; +readonly maxTokens?: number; +readonly temperature?: number; +} +export interface McpAccessorToolDescriptor { +readonly name: string; +readonly description?: string; +readonly inputSchema?: Record; +} +export interface McpAccessor { +listServers(): Promise; +listTools(serverId: string): Promise; +callTool(serverId: string, toolName: string, args: Record): Promise; +} +export interface LlmCompleteResult { +readonly text: string; +readonly model: string; +readonly inputTokens: number; +readonly outputTokens: number; +readonly finishReason: 'stop' | 'tool_calls' | 'max_tokens'; +readonly stopReason: 'end_turn' | 'max_tokens' | 'stop_sequence' | 'tool_use'; +} +export declare class LlmServiceUnavailableError extends Error { +constructor(callerAgentId: string); +} +export declare class LlmModelNotAllowedError extends Error { +constructor(callerAgentId: string, model: string); +} +export declare class LlmBudgetExceededError extends Error { +constructor(callerAgentId: string, budget: number); +} +export interface LlmProvider { +complete(req: LlmCompleteRequest): Promise; +} +export declare class MissingSecretError extends Error { +constructor(agentId: string, key: string); +} +export declare class MissingConfigError extends Error { +constructor(agentId: string, key: string); +} +export interface MigrationContext extends Omit { +readonly fromVersion: string; +readonly toVersion: string; +readonly previousConfig: Record; +readonly secrets: SecretsReadWriteAccessor; +} +export interface MigrationResult { +newConfig: Record; +} +export type MigrationHook = (ctx: MigrationContext) => Promise; +export declare const MIGRATION_TIMEOUT_MS_DEFAULT = 10000; +export declare class MigrationTimeoutError extends Error { +constructor(agentId: string, fromVersion: string, toVersion: string, timeoutMs: number); +} +export declare class MigrationHookError extends Error { +readonly migrationCause: unknown; +constructor(agentId: string, fromVersion: string, toVersion: string, cause: unknown); +} + +// ===== privacyMode.d.ts ===== +export declare const PRIVACY_MODE_CONFIG_KEY = "_privacy_mode"; +export declare const PRIVACY_BYPASS_SCOPES_CONFIG_KEY = "_privacy_bypass_scopes"; +export declare const PRIVACY_FORCE_GUARDED_ENV_VAR = "OMADIA_PRIVACY_FORCE_GUARDED"; +export declare const PRIVACY_MODE_VALUES: readonly ["guarded", "bypass", "per_tool"]; +export type PrivacyMode = (typeof PRIVACY_MODE_VALUES)[number]; +export declare const PRIVACY_MODE_DEFAULT: PrivacyMode; +export declare function resolveEffectivePrivacyMode(input: { +readonly storedMode: unknown; +readonly storedScopes: unknown; +readonly toolName: string; +readonly env: NodeJS.ProcessEnv; +}): 'guarded' | 'bypass'; +export declare function parseScopes(stored: unknown): readonly string[]; + +// ===== privacyReceipt.d.ts ===== +export declare const PRIVACY_REDACT_SERVICE_NAME = "privacyRedact"; +export declare const PRIVACY_REDACT_CAPABILITY = "privacy.redact@1"; +export interface BypassedToolEntry { +readonly toolName: string; +readonly pluginId: string; +readonly reason: 'operator_setting'; +readonly bytes: number; +} +export interface StructuredPayloadEntry { +readonly toolName: string; +readonly serverName: string; +readonly bytes: number; +readonly hasOutputSchema: boolean; +} +export interface PrivacyReceipt { +readonly datasetsInterned: number; +readonly fieldsMasked: number; +readonly fieldsCleartext: number; +readonly verbsExecuted: readonly string[]; +readonly pseudonymProjectionUsed: boolean; +readonly identityValuesOnWire?: number; +readonly bypassedTools?: readonly BypassedToolEntry[]; +readonly maskedPromptSpans?: readonly PromptMaskedSpanInfo[]; +readonly structuredPayloads?: readonly StructuredPayloadEntry[]; +} +export interface PrivacyToolResultV4Request { +readonly sessionId: string; +readonly turnId: string; +readonly toolName: string; +readonly rawResult: string; +} +export interface PrivacyToolResultV4Result { +readonly digestText: string; +readonly datasetId: string; +} +export interface PrivacySubAgentResultV4Request { +readonly turnId: string; +readonly narration: string; +readonly datasetIds: readonly string[]; +} +export interface PrivacyV4ToolRequest { +readonly sessionId: string; +readonly turnId: string; +readonly toolName: string; +readonly input: unknown; +} +export interface PrivacyV4ToolSpec { +readonly name: string; +readonly description: string; +readonly input_schema: Record; +} +export interface PrivacyRenderedAnswer { +readonly text: string; +readonly maskedValues: readonly string[]; +} +export interface PrivacyBypassedToolRequest { +readonly turnId: string; +readonly toolName: string; +readonly pluginId: string; +readonly reason: 'operator_setting'; +readonly bytes: number; +} +export interface PrivacyStructuredPayloadRequest { +readonly turnId: string; +readonly toolName: string; +readonly serverName: string; +readonly bytes: number; +readonly hasOutputSchema: boolean; +} +export interface PrivacyResolvedDataset { +readonly rowCount: number; +readonly columns: ReadonlyArray<{ +readonly path: string; +readonly type: string; +readonly classification?: 'safe-cleartext' | 'sensitive-masked'; +}>; +readonly rows: ReadonlyArray>; +} +export interface PromptPiiSpan { +readonly start: number; +readonly end: number; +readonly type: string; +readonly confidence: number; +} +export interface PromptPiiDetector { +readonly id: string; +detect(text: string): Promise; +} +export interface PromptMaskedSpanInfo { +readonly type: string; +readonly detector: string; +} +export interface PrivacyPromptMaskRequest { +readonly sessionId: string; +readonly turnId: string; +readonly text: string; +} +export type PrivacyPromptMaskResult = { +readonly outcome: 'disabled'; +} | { +readonly outcome: 'masked'; +readonly maskedText: string; +readonly spans: readonly PromptMaskedSpanInfo[]; +readonly degraded: boolean; +} | { +readonly outcome: 'blocked'; +readonly reason: string; +}; +export interface PrivacyGuardService { +internToolResultV4(request: PrivacyToolResultV4Request): Promise; +recordBypassedTool(request: PrivacyBypassedToolRequest): Promise; +recordStructuredPayload?(request: PrivacyStructuredPayloadRequest): Promise; +runV4Tool(request: PrivacyV4ToolRequest): Promise<{ +readonly resultText: string; +}>; +subAgentResultV4(request: PrivacySubAgentResultV4Request): Promise<{ +readonly resultText: string; +}>; +takeRenderedAnswerV4(turnId: string): Promise; +resolveDatasetForRender?(turnId: string, datasetId: string): PrivacyResolvedDataset | undefined; +maskUserPrompt?(request: PrivacyPromptMaskRequest): Promise; +restorePromptPseudonyms?(turnId: string, text: string): Promise; +snapshotPromptRestorer?(turnId: string): ((text: string) => string) | undefined; +v4ToolSpecs(): ReadonlyArray; +finalizeTurn(turnId: string, turnInput?: string): Promise; +} + +// ===== privacyReceiptFixtures.d.ts ===== +import type { PrivacyReceipt } from './privacyReceipt.js'; +export declare const RECEIPT_FIXTURE_QUIET: PrivacyReceipt; +export declare const RECEIPT_FIXTURE_RANKED: PrivacyReceipt; +export declare const RECEIPT_FIXTURE_PSEUDONYM: PrivacyReceipt; +export declare const ALL_PRIVACY_RECEIPT_FIXTURES: readonly PrivacyReceipt[]; + +// ===== processMemory.d.ts ===== +export declare const PROCESS_MEMORY_SERVICE_NAME = "processMemory"; +export declare const PROCESS_MEMORY_CAPABILITY = "processMemory@1"; +export declare const PROCESS_TITLE_REGEX: RegExp; +export declare const PROCESS_DEDUP_DEFAULT_THRESHOLD = 0.9; +export interface ProcessRecord { +readonly id: string; +readonly scope: string; +readonly title: string; +readonly steps: readonly string[]; +readonly visibility: string; +readonly version: number; +readonly createdAt: string; +readonly updatedAt: string; +} +export interface WriteProcessInput { +readonly title: string; +readonly steps: readonly string[]; +readonly scope: string; +readonly visibility?: string; +} +export type WriteProcessResult = { +ok: true; +record: ProcessRecord; +} | { +ok: false; +reason: 'invalid-title'; +message: string; +} | { +ok: false; +reason: 'duplicate'; +conflictingId: string; +conflictingTitle: string; +similarity: number; +} | { +ok: false; +reason: 'embedding-unavailable'; +message: string; +}; +export interface EditProcessInput { +readonly id: string; +readonly title?: string; +readonly steps?: readonly string[]; +readonly visibility?: string; +} +export type EditProcessResult = { +ok: true; +record: ProcessRecord; +} | { +ok: false; +reason: 'not-found'; +} | { +ok: false; +reason: 'invalid-title'; +message: string; +} | { +ok: false; +reason: 'embedding-unavailable'; +message: string; +}; +export interface QueryProcessesInput { +readonly query: string; +readonly scope?: string; +readonly limit?: number; +} +export interface ProcessQueryHit { +readonly record: ProcessRecord; +readonly score: number; +} +export interface ProcessMemoryService { +write(input: WriteProcessInput): Promise; +edit(input: EditProcessInput): Promise; +query(input: QueryProcessesInput): Promise; +get(id: string): Promise; +history(id: string): Promise; +} +export declare class NoopProcessMemoryService implements ProcessMemoryService { +write(_input: WriteProcessInput): Promise; +edit(_input: EditProcessInput): Promise; +query(_input: QueryProcessesInput): Promise; +get(_id: string): Promise; +history(_id: string): Promise; +} +export declare function slugifyProcessTitle(title: string): string; +export declare function buildProcessId(scope: string, title: string): string; + +// ===== responseGuard.d.ts ===== +export declare const RESPONSE_GUARD_SERVICE_NAME = "responseGuard"; +export declare const RESPONSE_GUARD_CAPABILITY = "responseGuard@1"; +export type SycophancyLevel = 'off' | 'low' | 'medium' | 'high'; +export type BoundaryPresetId = string; +export interface ProfileQualityConfig { +readonly sycophancy?: SycophancyLevel; +readonly boundaries?: { +readonly presets?: readonly BoundaryPresetId[]; +readonly custom?: readonly string[]; +}; +} +export interface ResponseGuardRequest { +readonly systemPrompt: string; +readonly messages: ReadonlyArray<{ +readonly role: 'user' | 'assistant' | 'system'; +readonly content: string; +}>; +readonly profileQuality?: ProfileQualityConfig; +readonly agentId?: string; +} +export interface ResponseGuardResult { +readonly prependRules: string; +} +export interface ResponseGuardService { +getRules(input: ResponseGuardRequest): Promise; +} + +// ===== routinesIntegration.d.ts ===== +import type { ApprovalReminder } from './conductorApproval.js'; +export declare const ROUTINES_INTEGRATION_SERVICE_NAME = "routinesIntegration"; +export interface RoutineCardAttachment { +contentType: string; +content: unknown; +} +export interface RoutineListAttachmentInput { +filter: 'all' | 'active' | 'paused'; +totals: { +all: number; +active: number; +paused: number; +}; +routines: Array<{ +id: string; +name: string; +cron: string; +prompt: string; +status: 'active' | 'paused'; +lastRunAt: string | null; +lastRunStatus: 'ok' | 'error' | 'timeout' | null; +}>; +} +export interface RoutinesIntegration { +captureRoutineTurn(info: { +tenant: string; +userId: string; +channel: string; +conversationRef: unknown; +principalRef?: string; +canTargetOthers?: boolean; +}): void; +updateRoutineConversationRef(routineId: string, conversationRef: unknown): Promise; +publishProactiveSend(channel: string, send: (conversationRef: unknown, message: { +text: string; +cardBody?: readonly unknown[]; +approval?: ApprovalReminder; +}, routine?: { +id: string; +name: string; +cron: string; +}) => Promise): void; +handleRoutineAction(input: { +action: 'pause' | 'resume' | 'trigger_now' | 'delete'; +id: string; +}): Promise; +buildRoutineSmartCardAttachment(input: { +routine: { +id: string; +name: string; +cron: string; +}; +body: string; +bodyItems?: readonly unknown[]; +}): RoutineCardAttachment; +buildRoutineListSmartCardAttachment(input: RoutineListAttachmentInput): RoutineCardAttachment; +} + +// ===== routineTarget.d.ts ===== +export type RoutineRecipient = { +readonly by: 'email'; +readonly email: string; +} | { +readonly by: 'aadObjectId'; +readonly aadObjectId: string; +}; +export type RoutineOrchestratorProfile = 'bare' | 'inherit'; +export declare const COLD_START_TARGET_KIND: "coldStart"; +export interface ColdStartTarget { +readonly kind: typeof COLD_START_TARGET_KIND; +readonly channel: string; +readonly recipient: RoutineRecipient; +readonly orchestratorProfile: RoutineOrchestratorProfile; +readonly createdBy: { +readonly tenant: string; +readonly userId: string; +}; +} +export declare function isColdStartTarget(ref: unknown): ref is ColdStartTarget; +export declare function isRoutineRecipient(value: unknown): value is RoutineRecipient; +export declare function normaliseRecipientEmail(raw: string): string | null; +export declare function buildEmailColdStartTarget(input: { +channel: string; +email: string; +createdBy: { +tenant: string; +userId: string; +}; +orchestratorProfile?: RoutineOrchestratorProfile; +}): ColdStartTarget | null; + +// ===== selfExtend.d.ts ===== +import type { PluginContext } from './pluginContext.js'; +export interface ExtensionRequiredSurface { +readonly graphReads?: readonly string[]; +readonly graphWrites?: readonly string[]; +readonly graphEntitySystems?: readonly string[]; +readonly subAgentCalls?: readonly string[]; +readonly llmModels?: readonly string[]; +readonly networkOutbound?: readonly string[]; +readonly webScanner?: boolean; +} +export interface ExtensionTemplate { +readonly id: string; +readonly title: string; +readonly description: string; +readonly paramsSchema: Record; +readonly requires?: ExtensionRequiredSurface; +} +export interface ApprovedExtension { +readonly templateId: string; +readonly params: Record; +} +export interface SelfExtendContract { +readonly templates: readonly ExtensionTemplate[]; +apply(approved: ApprovedExtension, ctx: PluginContext): Promise<() => void> | (() => void); +} + +// ===== sessionBriefing.d.ts ===== +export declare const SESSION_BRIEFING_SERVICE_NAME = "sessionBriefing"; +export declare const SESSION_BRIEFING_CAPABILITY = "sessionBriefing@1"; +export interface LoadSessionBriefingInput { +scope: string; +userId?: string; +agentId: string; +budgetTokens?: number; +} +export type BriefingMode = 'resume' | 'briefing' | 'empty'; +export interface SessionBriefingResult { +text: string; +mode: BriefingMode; +stats: { +resumeTurns: number; +summaryFound: boolean; +summaryRegenerated: boolean; +openTasks: number; +tokensUsed: number; +}; +} +export interface SessionBriefingService { +loadSessionBriefing(input: LoadSessionBriefingInput): Promise; +} + +// ===== targetRef.d.ts ===== +export type TargetRef = { +kind: 'canvas'; +canvasSessionId: string; +} | { +kind: 'container'; +containerId: string; +} +| { +kind: 'element'; +elementId: string; +} | { +kind: 'rowField'; +containerId: string; +rowKey: string; +fieldKey: string; +} +| { +kind: 'item'; +containerId: string; +itemKey: string; +} +| { +kind: 'point'; +containerId: string; +pointKey: string; +} | { +kind: 'textRange'; +anchor: TextRangeAnchor; +} +| { +kind: 'region'; +region: BufferRegion; +} +| { +kind: 'buffer'; +primitiveId: string; +bufferContentHash: string; +} +| { +kind: 'timeRange'; +primitiveId: string; +bufferContentHash: string; +start: number; +end: number; +unit: 'seconds' | 'samples' | 'frames'; +trackId?: string; +clipId?: string; +}; +export interface TextRangeAnchor { +primitiveId: string; +contentHash: string; +start: number; +end: number; +fallbackSegment?: { +before: string; +selection: string; +after: string; +}; +} +export interface BufferRegion { +primitiveId: string; +bufferContentHash: string; +bbox: { +x: number; +y: number; +w: number; +h: number; +}; +shape?: { +kind: 'rect' | 'polygon' | 'mask'; +points?: Array<[number, number]>; +maskHash?: string; +}; +} + +// ===== topic.d.ts ===== +import type { GraphNode } from './knowledgeGraph.js'; +export type TopicNamingSource = 'haiku' | 'fallback'; +export interface TopicNode { +id: string; +type: 'Topic'; +props: { +name: string; +description: string; +member_count: number; +created_at: string; +updated_at: string; +naming_source: TopicNamingSource; +}; +} +export interface TopicDetail extends TopicNode { +members: GraphNode[]; +} +export interface TopicClusteringRunOptions { +similarityThreshold?: number; +minClusterSize?: number; +} +export interface TopicClusteringRunResult { +totalMemoriesScanned: number; +memoriesWithEmbedding: number; +topicsDeleted: number; +topicsCreated: number; +unclusteredMemories: number; +haikuCalls: number; +durationMs: number; +} +export interface TopicClusteringService { +list(): Promise; +getWithMembers(topicExternalId: string, viewerOmadiaUserId: string): Promise; +recluster(opts?: TopicClusteringRunOptions): Promise; +} +export declare const TOPIC_CLUSTERING_SERVICE_NAME = "topicClustering"; +export declare const TOPIC_CLUSTERING_CAPABILITY = "topicClustering@1"; + +// ===== turnReceiptStore.d.ts ===== +import type { PrivacyReceipt } from './privacyReceipt.js'; +export declare const TURN_RECEIPT_STORE_SERVICE_NAME = "turnReceiptStore"; +export interface TurnReceiptRecordInput { +readonly turnId: string; +readonly sessionScope?: string; +readonly channel?: string; +readonly model?: string; +readonly receipt: PrivacyReceipt; +} +export interface TurnReceiptStore { +record(entry: TurnReceiptRecordInput): Promise; +} + +// ===== writeCapabilities.d.ts ===== +export type WriteOperation = 'update' | 'create' | 'delete' | 'reorder'; +export interface WriteCapabilityField { +name: string; +type?: string; +editable: boolean; +values?: readonly unknown[]; +pattern?: string; +min?: number; +max?: number; +maxLength?: number; +} +export interface WriteCapability { +dataClass: string; +operation: WriteOperation; +targetSchema?: { +idField?: string; +fields?: readonly WriteCapabilityField[]; +containerHint?: string; +requiredFields?: readonly string[]; +orderField?: string; +}; +} +export declare function isWriteCapableTool(capabilities: readonly WriteCapability[] | undefined): boolean; +export interface DerivedMutability { +canAddItems: boolean; +canRemoveItems: boolean; +canReorder: boolean; +requiredFields: readonly string[]; +editableFields: Record; +} +export declare function deriveMutabilityCapabilities(capabilities: readonly WriteCapability[], dataClass: string): DerivedMutability; diff --git a/middleware/packages/plugin-api/package.json b/middleware/packages/plugin-api/package.json index 5a436109a..bd01dd3ce 100644 --- a/middleware/packages/plugin-api/package.json +++ b/middleware/packages/plugin-api/package.json @@ -16,7 +16,10 @@ ], "scripts": { "build": "tsc", - "typecheck": "tsc --noEmit", + "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json", + "api:check": "node scripts/api-snapshot.mjs --check", + "api:update": "node scripts/api-snapshot.mjs --update", + "test": "node --import tsx --test 'test/**/*.test.ts'", "clean": "rm -rf dist" }, "engines": { diff --git a/middleware/packages/plugin-api/scripts/api-snapshot.mjs b/middleware/packages/plugin-api/scripts/api-snapshot.mjs new file mode 100644 index 000000000..0c0989706 --- /dev/null +++ b/middleware/packages/plugin-api/scripts/api-snapshot.mjs @@ -0,0 +1,405 @@ +#!/usr/bin/env node +/** + * Golden `.d.ts` API snapshot for `@omadia/plugin-api` (epic #470, item C1). + * + * WHY THIS EXISTS + * --------------- + * This package is the type contract between the kernel and every plugin. Today + * nothing stops a breaking change to `PluginContext` — a renamed method, a + * widened parameter, a removed field — from landing silently: `tsc` is happy + * because every consumer still lives in this repo and gets recompiled in the + * same commit. Once plugins live in their own repos (D1) that same silence is + * an incident somewhere else, discovered at install time. + * + * So the surface becomes machine-checked. We compile the package's declarations + * and compare the normalized result against a committed snapshot. Any change to + * the emitted types shows up as a reviewable diff in the PR that causes it. + * + * WHAT IS SNAPSHOTTED + * ------------------- + * Every `.d.ts` the package emits, not just `index.d.ts`. `index.ts` re-exports + * most modules but the emitted declarations are what a consumer's `tsc` actually + * reads, so the whole emitted tree is the contract. Files are concatenated in + * sorted POSIX path order, so the snapshot is stable regardless of filesystem + * enumeration order. + * + * NORMALIZATION + * ------------- + * Comments are stripped (a reworded JSDoc paragraph is not an API change), + * blank lines are dropped, and runs of whitespace collapse to a single space. + * What survives is the shape: names, modifiers, parameter lists, type + * expressions. The comment stripper is a small scanner rather than a regex, + * because `.d.ts` string-literal types legitimately contain `//` and `/*`. + * + * WHY DECLARATIONS GO TO A TEMP DIR + * --------------------------------- + * Not into `dist/`. Under the root suite this check runs alongside other test + * files that import this package's compiled output; rewriting `dist/` in place + * would race them. Emitting declarations only, into a throwaway directory, also + * keeps the check honest: it always measures the CURRENT `src/`, never whatever + * a previous build happened to leave behind. + * + * node scripts/api-snapshot.mjs # check (default) — fails on drift + * node scripts/api-snapshot.mjs --check # same, explicit + * node scripts/api-snapshot.mjs --update # accept the current surface + */ + +import { execFileSync } from 'node:child_process'; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const SNAPSHOT_DIR = path.join(PACKAGE_ROOT, 'api-snapshot'); +const SNAPSHOT_FILE = path.join(SNAPSHOT_DIR, 'plugin-api.d.ts.snap'); +const SNAPSHOT_REL = path.relative(PACKAGE_ROOT, SNAPSHOT_FILE).split(path.sep).join('/'); + +/** Beyond this many line-pairs the LCS table is not worth the memory; report coarsely. */ +const MAX_DIFF_CELLS = 2_000_000; +/** Lines of unchanged context printed around each hunk. */ +const DIFF_CONTEXT = 3; + +// --------------------------------------------------------------------------- +// Emit +// --------------------------------------------------------------------------- + +/** Locate the workspace `tsc`. Package-local first, then the hoisted install. */ +function resolveTsc() { + const candidates = [ + path.join(PACKAGE_ROOT, 'node_modules', '.bin', 'tsc'), + path.join(PACKAGE_ROOT, '..', '..', 'node_modules', '.bin', 'tsc'), + ]; + const found = candidates.find((candidate) => existsSync(candidate)); + if (found) return found; + throw new Error( + `Could not find tsc. Looked in:\n${candidates.map((c) => ` ${c}`).join('\n')}\n` + + 'Run `npm install` in middleware/ first.', + ); +} + +/** Compile declarations only, into `outDir`. Throws with tsc's output on failure. */ +function emitDeclarations(outDir) { + try { + execFileSync( + resolveTsc(), + [ + '-p', + path.join(PACKAGE_ROOT, 'tsconfig.json'), + '--outDir', + outDir, + '--declaration', + '--emitDeclarationOnly', + '--declarationMap', + 'false', + '--sourceMap', + 'false', + // `composite`/`incremental` would drop a .tsbuildinfo next to the + // output and let a stale cache answer for the current source. + '--composite', + 'false', + '--incremental', + 'false', + '--pretty', + 'false', + ], + { cwd: PACKAGE_ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }, + ); + } catch (error) { + const output = `${error.stdout ?? ''}${error.stderr ?? ''}`.trim(); + throw new Error(`tsc failed to emit declarations:\n\n${output || error.message}`); + } +} + +/** Every file under `dir` matching `suffix`, as POSIX paths relative to `dir`, sorted. */ +function listFiles(dir, suffix) { + const out = []; + const walk = (current) => { + for (const entry of readdirSync(current, { withFileTypes: true }).sort((a, b) => + a.name.localeCompare(b.name), + )) { + const full = path.join(current, entry.name); + if (entry.isDirectory()) { + if (entry.name === 'node_modules' || entry.name === 'dist') continue; + walk(full); + } else if (entry.name.endsWith(suffix)) { + out.push(path.relative(dir, full).split(path.sep).join('/')); + } + } + }; + walk(dir); + return out.sort((a, b) => a.localeCompare(b)); +} + +/** + * Guard against a silently empty snapshot. + * + * A snapshot check that compares nothing to nothing passes forever. If `rootDir` + * shifts, or the `include` glob stops matching, tsc exits 0 having emitted less + * than the source tree — and every future breaking change sails through green. + * So derive the expectation from what is on disk: every `src/**\/*.ts` must have + * produced a matching `.d.ts`. + */ +function assertEmitCoverage(sourceFiles, declarationFiles) { + const emitted = new Set(declarationFiles); + const missing = sourceFiles + .map((file) => file.replace(/\.ts$/, '.d.ts')) + .filter((expected) => !emitted.has(expected)); + + if (sourceFiles.length === 0) { + throw new Error( + 'No source files found under src/. The snapshot would be empty, which would ' + + 'pass forever while checking nothing.', + ); + } + if (missing.length > 0) { + throw new Error( + `tsc emitted no declaration for ${missing.length} source file(s) — the snapshot ` + + `would cover less than the package:\n${missing.map((f) => ` ${f}`).join('\n')}`, + ); + } +} + +// --------------------------------------------------------------------------- +// Normalize +// --------------------------------------------------------------------------- + +/** + * Remove line and block comments, leaving string and template literals intact. + * + * A regex cannot do this correctly here: `.d.ts` files carry string-literal + * types and template-literal types whose contents include `//` (URLs, route + * prefixes) and `/*`. This scanner tracks quoting, so those survive. + */ +function stripComments(source) { + let out = ''; + let i = 0; + while (i < source.length) { + const char = source[i]; + const next = source[i + 1]; + + if (char === '/' && next === '/') { + while (i < source.length && source[i] !== '\n') i += 1; + continue; + } + if (char === '/' && next === '*') { + i += 2; + while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) i += 1; + i += 2; + // A space, not nothing: `a/* x */b` must not become `ab`. + out += ' '; + continue; + } + if (char === '"' || char === "'" || char === '`') { + out += char; + i += 1; + while (i < source.length) { + const inner = source[i]; + if (inner === '\\') { + out += inner + (source[i + 1] ?? ''); + i += 2; + continue; + } + out += inner; + i += 1; + if (inner === char) break; + } + continue; + } + + out += char; + i += 1; + } + return out; +} + +/** Comment-free, blank-free, whitespace-collapsed lines for one declaration file. */ +function normalizeDeclaration(source) { + return stripComments(source) + .split('\n') + .map((line) => line.replace(/\s+/g, ' ').trim()) + .filter((line) => line.length > 0); +} + +/** Build the full snapshot text for an emitted declaration tree. */ +function buildSnapshot(outDir, declarationFiles) { + const lines = [ + '// Golden API snapshot for @omadia/plugin-api — generated, do not hand-edit.', + '// Regenerate deliberately: npm run api:update -w packages/plugin-api', + ]; + for (const file of declarationFiles) { + lines.push(''); + lines.push(`// ===== ${file} =====`); + lines.push(...normalizeDeclaration(readFileSync(path.join(outDir, file), 'utf8'))); + } + return `${lines.join('\n')}\n`; +} + +// --------------------------------------------------------------------------- +// Diff +// --------------------------------------------------------------------------- + +/** Longest-common-subsequence edit script over two line arrays. */ +function diffLines(before, after) { + const rows = before.length; + const cols = after.length; + const table = new Int32Array((rows + 1) * (cols + 1)); + const at = (r, c) => r * (cols + 1) + c; + + for (let r = rows - 1; r >= 0; r -= 1) { + for (let c = cols - 1; c >= 0; c -= 1) { + table[at(r, c)] = + before[r] === after[c] + ? table[at(r + 1, c + 1)] + 1 + : Math.max(table[at(r + 1, c)], table[at(r, c + 1)]); + } + } + + const ops = []; + let r = 0; + let c = 0; + while (r < rows && c < cols) { + if (before[r] === after[c]) { + ops.push({ kind: ' ', text: before[r] }); + r += 1; + c += 1; + } else if (table[at(r + 1, c)] >= table[at(r, c + 1)]) { + ops.push({ kind: '-', text: before[r] }); + r += 1; + } else { + ops.push({ kind: '+', text: after[c] }); + c += 1; + } + } + while (r < rows) ops.push({ kind: '-', text: before[r++] }); + while (c < cols) ops.push({ kind: '+', text: after[c++] }); + return ops; +} + +/** Render a unified diff, or a coarse summary when the inputs are too large to align. */ +function unifiedDiff(before, after, fromLabel, toLabel) { + // Trim the identical head and tail first: a one-symbol change then aligns two + // tiny arrays instead of two thousand-line ones. + let head = 0; + while (head < before.length && head < after.length && before[head] === after[head]) head += 1; + let tail = 0; + while ( + tail < before.length - head && + tail < after.length - head && + before[before.length - 1 - tail] === after[after.length - 1 - tail] + ) { + tail += 1; + } + + const beforeCore = before.slice(head, before.length - tail); + const afterCore = after.slice(head, after.length - tail); + + if (beforeCore.length * afterCore.length > MAX_DIFF_CELLS) { + return [ + `--- ${fromLabel}`, + `+++ ${toLabel}`, + `@@ the surface changed in ${beforeCore.length} → ${afterCore.length} lines starting at line ${head + 1} @@`, + 'Too large to align line-by-line. First differing lines:', + `- ${beforeCore[0] ?? '(end of file)'}`, + `+ ${afterCore[0] ?? '(end of file)'}`, + ].join('\n'); + } + + const ops = diffLines(beforeCore, afterCore); + const out = [`--- ${fromLabel}`, `+++ ${toLabel}`]; + + // Emit each run of changes with a little context around it. + let index = 0; + while (index < ops.length) { + if (ops[index].kind === ' ') { + index += 1; + continue; + } + let end = index; + while (end < ops.length && ops[end].kind !== ' ') end += 1; + const from = Math.max(0, index - DIFF_CONTEXT); + const to = Math.min(ops.length, end + DIFF_CONTEXT); + out.push(`@@ around snapshot line ${head + from + 1} @@`); + for (const op of ops.slice(from, to)) out.push(`${op.kind}${op.text}`); + index = to; + } + return out.join('\n'); +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +function main() { + const args = new Set(process.argv.slice(2)); + const doUpdate = args.has('--update'); + + const outDir = mkdtempSync(path.join(os.tmpdir(), 'omadia-plugin-api-dts-')); + let snapshot; + try { + emitDeclarations(outDir); + const sourceFiles = listFiles(path.join(PACKAGE_ROOT, 'src'), '.ts'); + const declarationFiles = listFiles(outDir, '.d.ts'); + assertEmitCoverage(sourceFiles, declarationFiles); + snapshot = buildSnapshot(outDir, declarationFiles); + } finally { + rmSync(outDir, { recursive: true, force: true }); + } + + if (doUpdate) { + mkdirSync(SNAPSHOT_DIR, { recursive: true }); + const changed = !existsSync(SNAPSHOT_FILE) || readFileSync(SNAPSHOT_FILE, 'utf8') !== snapshot; + writeFileSync(SNAPSHOT_FILE, snapshot); + console.log( + changed + ? `Wrote ${SNAPSHOT_REL} (${snapshot.split('\n').length - 1} lines).\n` + + 'Review the diff, then bump the package version — a removed or narrowed ' + + 'symbol is a MAJOR, an added one a MINOR.' + : `${SNAPSHOT_REL} already up to date.`, + ); + return; + } + + if (!existsSync(SNAPSHOT_FILE)) { + console.error( + `\n✗ No API snapshot at ${SNAPSHOT_REL}.\n` + + 'Create it with: npm run api:update -w packages/plugin-api\n', + ); + process.exit(1); + } + + const committed = readFileSync(SNAPSHOT_FILE, 'utf8'); + if (committed === snapshot) { + console.log(`✓ API snapshot up to date (${SNAPSHOT_REL}).`); + return; + } + + console.error( + `\n✗ The public type surface of @omadia/plugin-api changed.\n\n` + + unifiedDiff( + committed.split('\n'), + snapshot.split('\n'), + `committed ${SNAPSHOT_REL}`, + 'current src/', + ) + + `\n\nThis package is the contract every plugin compiles against, so the change is\n` + + `only allowed to land deliberately. If it is intended:\n\n` + + ` 1. npm run api:update -w packages/plugin-api\n` + + ` 2. Bump the version in packages/plugin-api/package.json:\n` + + ` symbol removed / signature narrowed / field made required → MAJOR\n` + + ` symbol added / field made optional → MINOR\n` + + ` nothing in this diff → no bump\n` + + ` 3. Commit the regenerated snapshot alongside the source change.\n`, + ); + process.exit(1); +} + +main(); diff --git a/middleware/packages/plugin-api/test/apiSnapshot.test.ts b/middleware/packages/plugin-api/test/apiSnapshot.test.ts new file mode 100644 index 000000000..3dc8e518a --- /dev/null +++ b/middleware/packages/plugin-api/test/apiSnapshot.test.ts @@ -0,0 +1,46 @@ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +/** + * Epic #470 C1 — the `@omadia/plugin-api` type surface is machine-checked. + * + * This package is the contract the kernel and every plugin compile against. + * While all consumers live in this repo, a breaking change to `PluginContext` + * compiles clean because everything is rebuilt in the same commit — the break + * only surfaces once a plugin ships from its own repo, at install time. + * + * The snapshot closes that gap: any change to the emitted declarations turns + * into a diff in the PR that causes it. The real work is in + * `scripts/api-snapshot.mjs`; this file is the gate that makes CI run it. + */ + +const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const SNAPSHOT_SCRIPT = path.join(PACKAGE_ROOT, 'scripts', 'api-snapshot.mjs'); + +test('public .d.ts surface matches the committed golden snapshot', () => { + let stdout: string; + + try { + stdout = execFileSync(process.execPath, [SNAPSHOT_SCRIPT, '--check'], { + cwd: PACKAGE_ROOT, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + } catch (error) { + const { stdout: out = '', stderr = '' } = error as { stdout?: string; stderr?: string }; + assert.fail( + `The public type surface of @omadia/plugin-api drifted from ` + + `api-snapshot/plugin-api.d.ts.snap.\n\n${`${out}${stderr}`.trim()}`, + ); + } + + assert.match( + stdout, + /API snapshot up to date/, + 'The check reported success without confirming the snapshot — a snapshot gate ' + + 'that passes on an unexpected message is a gate that passes on nothing.', + ); +}); diff --git a/middleware/packages/plugin-api/tsconfig.test.json b/middleware/packages/plugin-api/tsconfig.test.json new file mode 100644 index 000000000..ea2786bb4 --- /dev/null +++ b/middleware/packages/plugin-api/tsconfig.test.json @@ -0,0 +1,20 @@ +{ + // The build project (`tsconfig.json`) has `rootDir: src`, so the test tree is + // outside it and `npm run typecheck` never sees it. Without this project a + // type error in a test file would only surface when tsx hits that line at + // runtime — the same blind spot `scripts/check-test-typecheck.mjs` exists to + // close for the middleware root trees. + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "noEmit": true, + // `composite`/`declaration` are build-project concerns and conflict with + // `noEmit`; this project only ever typechecks. + "composite": false, + "declaration": false, + "declarationMap": false, + "sourceMap": false + }, + "include": ["src/**/*.ts", "test/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/specs/470-dev-platform-plugin/README.md b/specs/470-dev-platform-plugin/README.md index d24f21be8..c55f0540e 100644 --- a/specs/470-dev-platform-plugin/README.md +++ b/specs/470-dev-platform-plugin/README.md @@ -63,6 +63,28 @@ it at all. an unbounded `pg_advisory_lock` inside a 10s `activate()` budget, and a retry that never read `pg_advisory_unlock`'s return value. +### Phase A — C1 shipped + +- **Golden `.d.ts` snapshot for `@omadia/plugin-api`.** The contract is now machine-checked: + `packages/plugin-api/api-snapshot/plugin-api.d.ts.snap` holds every emitted declaration + (comments stripped, whitespace normalized, files in sorted path order), and + `packages/plugin-api/test/apiSnapshot.test.ts` fails the middleware suite on any drift. The + package stays `private: true` — nothing is published (D1 stands). + + Regenerating is deliberate, and a regeneration is not the whole job: + + ```bash + npm run api:check -w packages/plugin-api # what CI runs + npm run api:update -w packages/plugin-api # accept the new surface + ``` + + **The snapshot and the version move together.** A removed or renamed symbol, an added + parameter, a narrowed type, or an optional field made required is a **MAJOR** bump; an added + symbol, a widened type, or a required field made optional is a **MINOR** one. After the split + that version number is the only signal an out-of-repo plugin gets about whether its pinned + contract still holds, so a snapshot updated without a bump is the same silent break C1 exists + to stop. Full table in `middleware/packages/plugin-api/README.md`. + ### Still held back - **DynamicAgentRuntime rollback** — two attempts rejected. The current one does not cover From 08a45d84bcba1c3fcba0949f9319e70f1836c95c Mon Sep 17 00:00:00 2001 From: Marcel Wege Date: Thu, 20 Aug 2026 16:21:30 +0200 Subject: [PATCH 2/2] fix(plugin-api): make the API snapshot order locale-independent The golden .d.ts snapshot concatenated files in `localeCompare` order, so its bytes depended on ambient ICU collation rather than on the source. Measured on this package: ICU (en-US, de-DE, sv-SE, C and POSIX all agree) orders routinesIntegration.d.ts before routineTarget.d.ts, while code-unit ordering is the reverse -- a Node built without Intl sorts the other way and the check goes permanently red for a reason no source diff explains. Sort by code unit at both call sites and regenerate. The snapshot change is a pure relocation of those two blocks: sorted, old and new are byte-identical. Also pin *.snap to LF. The check compares the committed file byte-for-byte against generated text that always uses \n, and the repo has no `* text=auto`, so a contributor with core.autocrlf=true (the Git-for-Windows installer default) would get a CRLF working copy and a red check with an invisible cause. It is the only .snap in the repo, so the rule renormalizes nothing. --- .gitattributes | 3 + .../api-snapshot/plugin-api.d.ts.snap | 66 +++++++++---------- .../plugin-api/scripts/api-snapshot.mjs | 10 +-- 3 files changed, 42 insertions(+), 37 deletions(-) diff --git a/.gitattributes b/.gitattributes index 0b3947e08..a75a71351 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,3 +6,6 @@ docker-entrypoint.sh text eol=lf Dockerfile text eol=lf *.bash text eol=lf Makefile text eol=lf + +# Keep committed snapshots on LF +*.snap text eol=lf diff --git a/middleware/packages/plugin-api/api-snapshot/plugin-api.d.ts.snap b/middleware/packages/plugin-api/api-snapshot/plugin-api.d.ts.snap index 8330bc111..3c363a59a 100644 --- a/middleware/packages/plugin-api/api-snapshot/plugin-api.d.ts.snap +++ b/middleware/packages/plugin-api/api-snapshot/plugin-api.d.ts.snap @@ -2024,6 +2024,39 @@ export interface ResponseGuardService { getRules(input: ResponseGuardRequest): Promise; } +// ===== routineTarget.d.ts ===== +export type RoutineRecipient = { +readonly by: 'email'; +readonly email: string; +} | { +readonly by: 'aadObjectId'; +readonly aadObjectId: string; +}; +export type RoutineOrchestratorProfile = 'bare' | 'inherit'; +export declare const COLD_START_TARGET_KIND: "coldStart"; +export interface ColdStartTarget { +readonly kind: typeof COLD_START_TARGET_KIND; +readonly channel: string; +readonly recipient: RoutineRecipient; +readonly orchestratorProfile: RoutineOrchestratorProfile; +readonly createdBy: { +readonly tenant: string; +readonly userId: string; +}; +} +export declare function isColdStartTarget(ref: unknown): ref is ColdStartTarget; +export declare function isRoutineRecipient(value: unknown): value is RoutineRecipient; +export declare function normaliseRecipientEmail(raw: string): string | null; +export declare function buildEmailColdStartTarget(input: { +channel: string; +email: string; +createdBy: { +tenant: string; +userId: string; +}; +orchestratorProfile?: RoutineOrchestratorProfile; +}): ColdStartTarget | null; + // ===== routinesIntegration.d.ts ===== import type { ApprovalReminder } from './conductorApproval.js'; export declare const ROUTINES_INTEGRATION_SERVICE_NAME = "routinesIntegration"; @@ -2083,39 +2116,6 @@ bodyItems?: readonly unknown[]; buildRoutineListSmartCardAttachment(input: RoutineListAttachmentInput): RoutineCardAttachment; } -// ===== routineTarget.d.ts ===== -export type RoutineRecipient = { -readonly by: 'email'; -readonly email: string; -} | { -readonly by: 'aadObjectId'; -readonly aadObjectId: string; -}; -export type RoutineOrchestratorProfile = 'bare' | 'inherit'; -export declare const COLD_START_TARGET_KIND: "coldStart"; -export interface ColdStartTarget { -readonly kind: typeof COLD_START_TARGET_KIND; -readonly channel: string; -readonly recipient: RoutineRecipient; -readonly orchestratorProfile: RoutineOrchestratorProfile; -readonly createdBy: { -readonly tenant: string; -readonly userId: string; -}; -} -export declare function isColdStartTarget(ref: unknown): ref is ColdStartTarget; -export declare function isRoutineRecipient(value: unknown): value is RoutineRecipient; -export declare function normaliseRecipientEmail(raw: string): string | null; -export declare function buildEmailColdStartTarget(input: { -channel: string; -email: string; -createdBy: { -tenant: string; -userId: string; -}; -orchestratorProfile?: RoutineOrchestratorProfile; -}): ColdStartTarget | null; - // ===== selfExtend.d.ts ===== import type { PluginContext } from './pluginContext.js'; export interface ExtensionRequiredSurface { diff --git a/middleware/packages/plugin-api/scripts/api-snapshot.mjs b/middleware/packages/plugin-api/scripts/api-snapshot.mjs index 0c0989706..48364a2e7 100644 --- a/middleware/packages/plugin-api/scripts/api-snapshot.mjs +++ b/middleware/packages/plugin-api/scripts/api-snapshot.mjs @@ -20,8 +20,8 @@ * Every `.d.ts` the package emits, not just `index.d.ts`. `index.ts` re-exports * most modules but the emitted declarations are what a consumer's `tsc` actually * reads, so the whole emitted tree is the contract. Files are concatenated in - * sorted POSIX path order, so the snapshot is stable regardless of filesystem - * enumeration order. + * code-unit-sorted POSIX path order, so the snapshot is stable regardless of + * filesystem enumeration order or ambient locale collation. * * NORMALIZATION * ------------- @@ -67,6 +67,8 @@ const SNAPSHOT_REL = path.relative(PACKAGE_ROOT, SNAPSHOT_FILE).split(path.sep). const MAX_DIFF_CELLS = 2_000_000; /** Lines of unchanged context printed around each hunk. */ const DIFF_CONTEXT = 3; +/** Code-unit ordering. Never `localeCompare`: a golden snapshot's order must not depend on ambient collation. */ +const byCodeUnit = (a, b) => (a < b ? -1 : a > b ? 1 : 0); // --------------------------------------------------------------------------- // Emit @@ -124,7 +126,7 @@ function listFiles(dir, suffix) { const out = []; const walk = (current) => { for (const entry of readdirSync(current, { withFileTypes: true }).sort((a, b) => - a.name.localeCompare(b.name), + byCodeUnit(a.name, b.name), )) { const full = path.join(current, entry.name); if (entry.isDirectory()) { @@ -136,7 +138,7 @@ function listFiles(dir, suffix) { } }; walk(dir); - return out.sort((a, b) => a.localeCompare(b)); + return out.sort(byCodeUnit); } /**