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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions examples/audiobook-curator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,16 @@ instead of maintaining separate MCP and CLI presenters:
| Component | MCP composition | Rendered authored CLI composition |
| --- | --- | --- |
| `CuratorDocument` and its `CuratorReceipt` union | Wrap the structured receipt and headline for 15 receipt-bearing tools | Wrap `inventory`, `select`, `audible-search`, `convert`, `audit`, and `library-audit` |
| `DataList`, `Field`, and `Callout` | Provide report primitives throughout the component library and directly in the catalog resource, curate prompt, cache route, and library audit | Provide the same primitives through the shared components and directly in `library-audit` |
| `AudiobookCard` | Renders files in `audit_library` and under shelf and ranking views | Reached through `LibraryShelf` and `CandidateRanking` |
| `LibraryShelf` | Composes `inventory_sources`, `inspect_sources`, and `select_sources` | Composes `inventory` and `select` |
| `CandidateRanking` | Composes `search_audible`, `select_audible_edition`, and `identify_audible_sample` | Composes `audible-search` |
| `EvidenceTrail` | Composes acoustic identification, acoustic verification, and Whisper verification | No authored rendered counterpart; those compatibility commands remain plain `.ts` routes |
| `MutationReceipt` | Composes prepare, conversion, metadata, and chapter mutation tools | Composes `convert` |
| `ChapterOutline` | Composes integrity audit, conversion, and chapter application tools | Composes `audit` and `convert` |
| `IntegrityReport` | Composes integrity audit, conversion, metadata, and chapter application tools | Composes `audit` and `convert` |
| `DataList`, `Field`, `Callout`, and `FileList` | Provide atomic report fields, prose callouts, and file-list blocks throughout the component library and directly in the catalog resource, curate prompt, cache route, and library audit | Provide the same primitives through the shared components and directly in `library-audit` |
| `FileCard` and `EditionCard`, fed by `view-models` | Render file and edition models in `audit_library`, shelf, and ranking views | Reached through the receipt-specific shelves and ranking components |
| `InspectionShelf`, `InventoryShelf`, `AuditShelf`, and `SelectionShelf` | Compose receipt-specific inspection, inventory, audit, and selection reports; the inspection, inventory, and selection shelves are used directly by their MCP routes | `InventoryShelf` and `SelectionShelf` compose `inventory` and `select` |
| `SearchRanking`, `IdentifyRanking`, and `SelectionRanking` | Render the statically typed ranking for `search_audible`, `identify_audible_sample`, and `select_audible_edition` | `SearchRanking` composes `audible-search` |
| `AcousticTrail`, `IdentifyTrail`, and `WhisperTrail` | Render the statically typed evidence for acoustic verification, acoustic identification, and Whisper verification | No authored rendered counterpart; those compatibility commands remain plain `.ts` routes |
| `MetadataMutation`, `ChapterMutation`, `ConversionMutation`, and `PrepareMutation` | Render each statically typed metadata, chapter, conversion, or preparation mutation | `ConversionMutation` composes `convert` |
| `ChapterOutline` with normalized `chapters` props | Composes integrity audit, conversion, and chapter application tools through receipt-specific mappers | Composes `audit` and `convert` |
| `IntegrityAuditReport`, `MetadataIntegrityReport`, `ChapterIntegrityReport`, and `ConversionIntegrityReport` | Render each statically typed integrity report | `IntegrityAuditReport` and `ConversionIntegrityReport` compose `audit` and `convert` |
| `CurationShelf` | Composes shelf review, Audible edition selection, and metadata/chapter application | Composes `shelf` |
| `LibraryAnalysis` | Resolves the asynchronous duplicate and multipart analysis in `audit_library` | Resolves the same analysis in `library-audit` |
| `LibraryAnalysis` and `CandidateGroupCallout` | Resolve asynchronous duplicate and multipart analysis in `audit_library` with shared atomic candidate prose and file lists | Resolve the same analysis and candidate-group presentation in `library-audit` |

The catalog resource at `src/mcp/curator/resources/catalog.tsx` and the prompt at
`src/mcp/curator/prompts/curate.tsx` are compositions too: both return their
Expand Down
30 changes: 16 additions & 14 deletions examples/audiobook-curator/src/audible.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { mkdir, mkdtemp, rename, rm, writeFile } from 'node:fs/promises';
import { dirname, extname, join, resolve } from 'node:path';

import type { JsonObject } from '@agent-bundle/runtime';

import {
CuratorError,
audibleHosts,
Expand All @@ -21,14 +23,14 @@ export interface CuratorHttpOptions {

export type CuratorHttpClient = (url: string, options?: CuratorHttpOptions) => Promise<unknown>;

export interface AudibleQuery {
export type AudibleQuery = {
readonly author?: string;
readonly durationSeconds?: number;
readonly narrator?: string;
readonly title: string;
}
};

export interface AudibleCandidateEvidence {
export type AudibleCandidateEvidence = {
readonly authorMatch: boolean;
readonly durationDifferencePercent?: number;
readonly language?: string;
Expand All @@ -38,12 +40,12 @@ export interface AudibleCandidateEvidence {
readonly strictIdentityMatch: boolean;
readonly titleMatch: boolean;
readonly unabridged: boolean;
}
};

export interface AudibleCandidate extends Record<string, unknown> {
export type AudibleCandidate = JsonObject & {
readonly evidence: AudibleCandidateEvidence;
readonly region: AudibleRegion;
}
};

export interface AudibleSearchInput extends AudibleQuery {
readonly attempts?: number;
Expand All @@ -52,7 +54,7 @@ export interface AudibleSearchInput extends AudibleQuery {
readonly report?: string;
}

export interface AudibleSearchReceipt {
export type AudibleSearchReceipt = {
readonly candidates: readonly AudibleCandidate[];
readonly errors: readonly { readonly error: string; readonly region: AudibleRegion }[];
readonly exitCode: 0 | 1;
Expand All @@ -62,9 +64,9 @@ export interface AudibleSearchReceipt {
readonly operation: 'audible-search';
readonly query: AudibleQuery;
readonly reviewNote: string;
}
};

export interface AudibleSelectionReceipt {
export type AudibleSelectionReceipt = {
readonly candidateNumber: number;
readonly candidateReport?: string;
readonly generatedAt: string;
Expand All @@ -73,7 +75,7 @@ export interface AudibleSelectionReceipt {
readonly operation: 'audible-select';
readonly reviewNote?: string;
readonly selected: AudibleCandidate;
}
};

export interface AudibleCacheInput {
readonly asin: string;
Expand All @@ -83,7 +85,7 @@ export interface AudibleCacheInput {
readonly region?: AudibleRegion;
}

export interface AudibleCacheReceipt {
export type AudibleCacheReceipt = {
readonly artwork?: string;
readonly asin: string;
readonly chapterError?: string;
Expand All @@ -99,15 +101,15 @@ export interface AudibleCacheReceipt {
readonly chapters: string;
readonly product: string;
};
}
};

export interface AudibleDependencies {
readonly http?: CuratorHttpClient;
readonly signal?: AbortSignal;
}

const objects = (value: unknown): Record<string, unknown>[] => Array.isArray(value)
? value.filter((row): row is Record<string, unknown> => row !== null && typeof row === 'object' && !Array.isArray(row))
const objects = (value: unknown): JsonObject[] => Array.isArray(value)
? value.filter((row): row is JsonObject => row !== null && typeof row === 'object' && !Array.isArray(row))
: [];

export const audibleCandidateEvidence = (
Expand Down
7 changes: 4 additions & 3 deletions examples/audiobook-curator/src/cli/audible-search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import type { CliRouteConfig, CliRouteProps } from 'agent-bundle';
import { z } from 'zod';

import type { AudibleSearchReceipt } from '../audible.js';
import { CandidateRanking } from '../components/candidate-ranking.js';
import { SearchRanking } from '../components/candidate-ranking.js';
import { CuratorDocument } from '../components/curator-document.js';
import { audibleSearchHeadline } from '../components/headlines.js';
import { audibleOperations, audibleRegionList, defaultAudibleOperations } from '../operations/audible.js';

const operation = audibleOperations(defaultAudibleOperations).audibleSearch;
Expand Down Expand Up @@ -43,10 +44,10 @@ export default async function audibleSearch({ input, signal }: CliRouteProps<typ
}, { signal }) as AudibleSearchReceipt;
return (
<CuratorDocument
headline={`Ranked ${receipt.candidates.length} Audible candidates across reviewed regions; human selection is required.`}
headline={audibleSearchHeadline(receipt)}
receipt={receipt}
>
<CandidateRanking receipt={receipt} />
<SearchRanking receipt={receipt} />
</CuratorDocument>
);
}
11 changes: 6 additions & 5 deletions examples/audiobook-curator/src/cli/audit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import React from 'react';
import type { CliRouteConfig, CliRouteProps } from 'agent-bundle';
import { z } from 'zod';

import { ChapterOutline } from '../components/chapter-outline.js';
import { ChapterOutline, chaptersFromAuditReceipt } from '../components/chapter-outline.js';
import { CuratorDocument } from '../components/curator-document.js';
import { IntegrityReport } from '../components/integrity-report.js';
import { integrityAuditHeadline } from '../components/headlines.js';
import { IntegrityAuditReport } from '../components/integrity-report.js';
import type { IntegrityAuditReceipt } from '../integrity-audit.js';
import { defaultOutputOperations, outputOperations } from '../operations/output.js';

Expand All @@ -28,11 +29,11 @@ export default async function audit({ input, signal }: CliRouteProps<typeof inpu
const receipt = await operation.handler(input, { signal }) as IntegrityAuditReceipt;
return (
<CuratorDocument
headline={`Audited ${receipt.bytes} bytes with SHA-256 ${receipt.sha256}; status is ${receipt.status}.`}
headline={integrityAuditHeadline(receipt)}
receipt={receipt}
>
<IntegrityReport receipt={receipt} />
<ChapterOutline receipt={receipt} />
<IntegrityAuditReport receipt={receipt} />
<ChapterOutline chapters={chaptersFromAuditReceipt(receipt)} />
</CuratorDocument>
);
}
18 changes: 8 additions & 10 deletions examples/audiobook-curator/src/cli/convert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import React from 'react';
import type { CliRouteConfig, CliRouteProps } from 'agent-bundle';
import { z } from 'zod';

import { ChapterOutline } from '../components/chapter-outline.js';
import { ChapterOutline, chaptersFromConvertReceipt } from '../components/chapter-outline.js';
import { CuratorDocument } from '../components/curator-document.js';
import { IntegrityReport } from '../components/integrity-report.js';
import { MutationReceipt } from '../components/mutation-receipt.js';
import { convertHeadline } from '../components/headlines.js';
import { ConversionIntegrityReport } from '../components/integrity-report.js';
import { ConversionMutation } from '../components/mutation-receipt.js';
import type { ConvertReceipt } from '../conversion.js';
import { defaultOutputOperations, outputOperations } from '../operations/output.js';

Expand Down Expand Up @@ -39,14 +40,11 @@ export const resultSchema = operation.resultSchema;

export default async function convert({ input, signal }: CliRouteProps<typeof inputSchema>) {
const receipt = await operation.handler(input, { signal }) as ConvertReceipt;
const headline = receipt.status === 'planned'
? `Planned ${receipt.audioMode} output at ${receipt.output}; sources remain unchanged.`
: `Converted and verified ${receipt.output}; sources remain unchanged.`;
return (
<CuratorDocument headline={headline} receipt={receipt}>
<MutationReceipt receipt={receipt} />
<ChapterOutline receipt={receipt} />
<IntegrityReport receipt={receipt} />
<CuratorDocument headline={convertHeadline(receipt)} receipt={receipt}>
<ConversionMutation receipt={receipt} />
<ChapterOutline chapters={chaptersFromConvertReceipt(receipt)} />
<ConversionIntegrityReport receipt={receipt} />
</CuratorDocument>
);
}
7 changes: 4 additions & 3 deletions examples/audiobook-curator/src/cli/inventory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import type { CliRouteConfig, CliRouteProps } from 'agent-bundle';
import { z } from 'zod';

import { CuratorDocument } from '../components/curator-document.js';
import { LibraryShelf } from '../components/library-shelf.js';
import { inventoryHeadline } from '../components/headlines.js';
import { InventoryShelf } from '../components/library-shelf.js';
import type { InventoryReceipt } from '../library.js';
import { defaultDiscoveryOperations, discoveryOperations } from '../operations/discovery.js';

Expand All @@ -27,10 +28,10 @@ export default async function inventory({ input, signal }: CliRouteProps<typeof
const receipt = await operation.handler(input, { signal }) as InventoryReceipt;
return (
<CuratorDocument
headline={`Inventoried ${receipt.summary.files} media files with ${receipt.summary.errors} retained errors.`}
headline={inventoryHeadline(receipt)}
receipt={receipt}
>
<LibraryShelf receipt={receipt} />
<InventoryShelf receipt={receipt} />
</CuratorDocument>
);
}
8 changes: 4 additions & 4 deletions examples/audiobook-curator/src/cli/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import type { CliRouteConfig, CliRouteProps } from 'agent-bundle';
import { z } from 'zod';

import { CuratorDocument } from '../components/curator-document.js';
import { LibraryShelf } from '../components/library-shelf.js';
import { selectionHeadline } from '../components/headlines.js';
import { SelectionShelf } from '../components/library-shelf.js';
import type { SelectionReceipt } from '../library.js';
import { defaultDiscoveryOperations, discoveryOperations } from '../operations/discovery.js';

Expand All @@ -22,13 +23,12 @@ export const resultSchema = operation.resultSchema;

export default async function select({ input, signal }: CliRouteProps<typeof inputSchema>) {
const receipt = await operation.handler(input, { signal }) as SelectionReceipt;
const reviewCount = receipt.selections.filter((selection) => selection.reviewRequired).length;
return (
<CuratorDocument
headline={`Selected ${receipt.selections.length} source groups; ${reviewCount} require review.`}
headline={selectionHeadline(receipt)}
receipt={receipt}
>
<LibraryShelf receipt={receipt} />
<SelectionShelf receipt={receipt} />
</CuratorDocument>
);
}
6 changes: 3 additions & 3 deletions examples/audiobook-curator/src/cli/shelf.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Agent, agent, type JsonValue } from '@agent-bundle/runtime';
import { Agent, agent } from '@agent-bundle/runtime';
import type { CliRouteConfig } from 'agent-bundle';
import React from 'react';
import { z } from 'zod';
Expand All @@ -25,15 +25,15 @@ export default async function Shelf() {
const state = (await agent()).state;
if (state === undefined) {
return (
<Agent.Result value={emptyShelf as unknown as JsonValue}>
<Agent.Result value={emptyShelf}>
<Agent.Text>Persisted curation shelf unavailable.</Agent.Text>
<ShelfUnavailable />
</Agent.Result>
);
}
const shelf = CurationShelfStateSchema.parse((await state.read()).state);
return (
<Agent.Result value={shelf as unknown as JsonValue}>
<Agent.Result value={shelf}>
<Agent.Text>Persisted curation shelf ready.</Agent.Text>
<CurationShelf state={shelf} />
</Agent.Result>
Expand Down
Loading
Loading