@asterism-app/sdk 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +20 -2
  3. package/docs/data-and-lifecycle.md +79 -0
  4. package/docs/getting-started.md +37 -0
  5. package/docs/manual.md +38 -0
  6. package/docs/minimal-plugin.md +35 -0
  7. package/docs/project-structure.md +33 -0
  8. package/docs/resources-and-tools.md +43 -0
  9. package/docs/session-entries.md +31 -0
  10. package/docs/views-and-settings.md +43 -0
  11. package/examples/author.ts +158 -0
  12. package/examples/consume-usage.ts +29 -0
  13. package/examples/content-author.ts +29 -0
  14. package/examples/content-contract.ts +10 -0
  15. package/examples/content-renderer.ts +26 -0
  16. package/examples/counter/docs/usage.md +9 -0
  17. package/examples/counter/package.json +27 -0
  18. package/examples/counter/src/contracts.ts +34 -0
  19. package/examples/counter/src/panel.module.css +2 -0
  20. package/examples/counter/src/renderer.tsx +80 -0
  21. package/examples/counter/src/runtime.ts +49 -0
  22. package/examples/counter/src/storage.ts +4 -0
  23. package/examples/counter/src/styles.d.ts +4 -0
  24. package/examples/counter/tsconfig.json +16 -0
  25. package/examples/counter-contract.ts +18 -0
  26. package/examples/data-codecs.ts +18 -0
  27. package/examples/events.ts +37 -0
  28. package/examples/i18n.ts +18 -0
  29. package/examples/model-completion.ts +15 -0
  30. package/examples/model-input.ts +16 -0
  31. package/examples/plugin-calls.ts +22 -0
  32. package/examples/provider-usage-headers.ts +20 -0
  33. package/examples/provider-usage.ts +34 -0
  34. package/examples/renderer.ts +47 -0
  35. package/examples/session-resources.ts +29 -0
  36. package/examples/settings-contract.ts +8 -0
  37. package/examples/settings-renderer.ts +30 -0
  38. package/examples/settings-runtime.ts +11 -0
  39. package/examples/storage.ts +26 -0
  40. package/examples/tool-renderer.ts +47 -0
  41. package/examples/web-provider.ts +27 -0
  42. package/package.json +39 -4
  43. package/src/calls.ts +28 -0
  44. package/src/card.ts +7 -0
  45. package/src/channels.ts +20 -0
  46. package/src/commands.ts +28 -0
  47. package/src/config.ts +25 -0
  48. package/src/content-runtime.ts +11 -0
  49. package/src/content.ts +6 -0
  50. package/src/contracts.ts +39 -0
  51. package/src/data-encoding.ts +14 -0
  52. package/src/error-details.ts +78 -0
  53. package/src/events.ts +91 -0
  54. package/src/i18n.ts +58 -0
  55. package/src/json.ts +4 -0
  56. package/src/mcp.ts +80 -0
  57. package/src/model-catalog.ts +18 -0
  58. package/src/model-cost-estimate.ts +49 -0
  59. package/src/model-cost.ts +44 -0
  60. package/src/model-input.ts +19 -0
  61. package/src/model-media.ts +11 -0
  62. package/src/model-provider.ts +44 -0
  63. package/src/model-records.ts +56 -0
  64. package/src/model-selection.ts +34 -0
  65. package/src/models.ts +134 -0
  66. package/src/module-contract.ts +44 -0
  67. package/src/objects.ts +89 -0
  68. package/src/plugin.ts +58 -0
  69. package/src/presentation.ts +21 -0
  70. package/src/provider-usage.ts +47 -0
  71. package/src/records.ts +60 -0
  72. package/src/renderer-calls.ts +15 -0
  73. package/src/renderer-card.ts +29 -0
  74. package/src/renderer-content.ts +9 -0
  75. package/src/renderer-creation.ts +83 -0
  76. package/src/renderer-i18n.ts +49 -0
  77. package/src/renderer-island.ts +66 -0
  78. package/src/renderer-records.ts +49 -0
  79. package/src/renderer-services.ts +89 -0
  80. package/src/renderer-tools.ts +103 -0
  81. package/src/renderer-view-state.ts +33 -0
  82. package/src/renderer.ts +174 -0
  83. package/src/runtime-agent-catalog.ts +27 -0
  84. package/src/runtime-application.ts +7 -0
  85. package/src/runtime-calls.ts +22 -0
  86. package/src/runtime-channels.ts +24 -0
  87. package/src/runtime-contracts.ts +207 -0
  88. package/src/runtime-mcp.ts +24 -0
  89. package/src/runtime-model-providers.ts +16 -0
  90. package/src/runtime-resources.ts +117 -0
  91. package/src/runtime-sessions.ts +36 -0
  92. package/src/runtime-shell.ts +18 -0
  93. package/src/runtime-terminal.ts +24 -0
  94. package/src/runtime-tools.ts +31 -0
  95. package/src/runtime-usage.ts +54 -0
  96. package/src/runtime-web.ts +36 -0
  97. package/src/runtime.ts +53 -0
  98. package/src/session-title.ts +8 -0
  99. package/src/shell.ts +76 -0
  100. package/src/terminal.ts +36 -0
  101. package/src/tool-result.ts +45 -0
  102. package/src/tool-validation.ts +14 -0
  103. package/src/usage-state.ts +20 -0
  104. package/src/web.ts +75 -0
  105. package/src/workspace.ts +43 -0
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ import { defineData } from "./presentation.ts";
3
+ import { modelCostSchema } from "./model-cost.ts";
4
+ import { thinkingSchema } from "./model-selection.ts";
5
+
6
+ export const catalogModelSchema = z.object({
7
+ providerId: z.string(), modelId: z.string(), name: z.string(), enabled: z.boolean(),
8
+ contextWindow: z.number().nullable(), cost: modelCostSchema.nullable(),
9
+ generation: z.object({ maxOutputTokens: z.number().nullable() }),
10
+ capabilities: z.object({ reasoning: z.boolean(), tools: z.boolean(), thinkingLevels: z.array(thinkingSchema) }),
11
+ });
12
+ /** Configured model metadata for plugin selectors and diagnostics; credentials remain backend capabilities. */
13
+ export const ModelCatalog = defineData("asterism.models/catalog", z.object({
14
+ modelProviders: z.array(z.object({ id: z.string(), name: z.string(), enabled: z.boolean() })),
15
+ models: z.array(catalogModelSchema),
16
+ }));
17
+ export type ModelCatalogSnapshot = z.infer<typeof ModelCatalog.schema>;
18
+ export type CatalogModel = z.infer<typeof catalogModelSchema>;
@@ -0,0 +1,49 @@
1
+ import type { ModelCost, ModelRates, ModalityRates, UnitPrice } from "./model-cost.ts";
2
+
3
+ /** Quantities already split by billing category; callers are responsible for preventing overlap between totals and subcategories. */
4
+ export interface CostQuantity {
5
+ modality: keyof ModelRates;
6
+ item: keyof ModalityRates;
7
+ quantity: number | null;
8
+ unit: UnitPrice["unit"];
9
+ }
10
+ export type CostIssue = "quantity" | "price" | "unit" | "tier" | "invalid";
11
+ export interface CostLine extends CostQuantity { price: UnitPrice | null; amount: number | null; issue: CostIssue | null }
12
+ export interface CostEstimate {
13
+ status: "complete" | "partial" | "unknown";
14
+ amount: number | null;
15
+ fromInputTokens: number | null;
16
+ lines: CostLine[];
17
+ }
18
+ export function selectModelRates(cost: ModelCost | null, inputTokens: number | null): { rates: ModelRates | null; fromInputTokens: number | null; unknownTier: boolean } {
19
+ if (!cost) return { rates: null, fromInputTokens: null, unknownTier: false };
20
+ if (cost.tiers.length && inputTokens === null) return { rates: null, fromInputTokens: null, unknownTier: true };
21
+ const tier = cost.tiers.findLast(tier => inputTokens !== null && inputTokens >= tier.fromInputTokens);
22
+ return { rates: tier?.rates ?? cost.rates, fromInputTokens: tier?.fromInputTokens ?? null, unknownTier: false };
23
+ }
24
+ /** Consumes only prices and named quantities, without modifying upstream usage or maintaining a historical ledger. */
25
+ export function estimateModelCost(cost: ModelCost | null, inputTokens: number | null, quantities: readonly CostQuantity[]): CostEstimate {
26
+ const selected = selectModelRates(cost, inputTokens);
27
+ const lines = quantities.map((quantity): CostLine => {
28
+ const price = selected.rates?.[quantity.modality][quantity.item] ?? null;
29
+ const missing = (issue: CostIssue): CostLine => ({ ...quantity, price, amount: null, issue });
30
+ if (quantity.quantity === null) return missing("quantity");
31
+ if (!Number.isFinite(quantity.quantity) || quantity.quantity < 0) return missing("invalid");
32
+ if (selected.unknownTier) return missing("tier");
33
+ if (price && price.unit !== quantity.unit) return missing("unit");
34
+ // A known zero quantity in the same unit needs no price; zero tokens cannot represent zero seconds or zero images.
35
+ if (quantity.quantity === 0) return { ...quantity, price, amount: 0, issue: null };
36
+ if (!price) return missing("price");
37
+ const amount = quantity.quantity / price.per * price.amount;
38
+ return Number.isFinite(amount) ? { ...quantity, price, amount, issue: null } : missing("invalid");
39
+ });
40
+ const complete = lines.length > 0 && lines.every(line => line.issue === null);
41
+ const hasAmount = complete || lines.some(line => line.quantity !== null && line.quantity > 0 && line.amount !== null);
42
+ const amount = hasAmount ? lines.reduce((sum, line) => sum + (line.amount ?? 0), 0) : null;
43
+ return { status: complete ? "complete" : hasAmount ? "partial" : "unknown", amount, fromInputTokens: selected.fromInputTokens, lines };
44
+ }
45
+ export function totalEstimatedCost(estimates: readonly CostEstimate[]): { amount: number | null; incomplete: number } {
46
+ const known = estimates.filter(estimate => estimate.amount !== null);
47
+ return { amount: known.length ? known.reduce((sum, estimate) => sum + estimate.amount!, 0) : null,
48
+ incomplete: estimates.filter(estimate => estimate.status !== "complete").length };
49
+ }
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+
3
+ export const priceUnits = ["token", "image", "second"] as const;
4
+ /** Amounts use the price table's common currency; amount is the price per per units of unit, and 0 means free. */
5
+ export const unitPriceSchema = z.strictObject({
6
+ amount: z.number().nonnegative(),
7
+ unit: z.enum(priceUnits),
8
+ per: z.number().positive(),
9
+ });
10
+ export type UnitPrice = z.infer<typeof unitPriceSchema>;
11
+
12
+ /** Billing items within a modality can use different units; missing items and null both mean unknown. */
13
+ const itemPrice = unitPriceSchema.nullable().default(null);
14
+ export const modalityRatesSchema = z.strictObject({
15
+ input: itemPrice, output: itemPrice, cacheRead: itemPrice, cacheWrite: itemPrice, reasoning: itemPrice,
16
+ });
17
+ export type ModalityRates = z.infer<typeof modalityRatesSchema>;
18
+ export const emptyModalityRates = (): ModalityRates => ({ input: null, output: null, cacheRead: null, cacheWrite: null, reasoning: null });
19
+ export const modelRatesSchema = z.strictObject({
20
+ text: modalityRatesSchema.default(emptyModalityRates),
21
+ image: modalityRatesSchema.default(emptyModalityRates),
22
+ audio: modalityRatesSchema.default(emptyModalityRates),
23
+ video: modalityRatesSchema.default(emptyModalityRates),
24
+ });
25
+ export type ModelRates = z.infer<typeof modelRatesSchema>;
26
+ export const emptyModelRates = (): ModelRates => ({
27
+ text: emptyModalityRates(), image: emptyModalityRates(), audio: emptyModalityRates(), video: emptyModalityRates(),
28
+ });
29
+
30
+ /** Select a tier using the entire request's total input tokens; reaching a threshold applies all prices in that tier, without progressive pricing across tiers. */
31
+ export const modelCostSchema = z.strictObject({
32
+ currency: z.literal("USD"),
33
+ rates: modelRatesSchema,
34
+ tiers: z.array(z.strictObject({
35
+ fromInputTokens: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
36
+ rates: modelRatesSchema,
37
+ })),
38
+ }).superRefine((cost, context) => {
39
+ cost.tiers.forEach((tier, index) => {
40
+ if (index > 0 && tier.fromInputTokens <= cost.tiers[index - 1]!.fromInputTokens)
41
+ context.addIssue({ code: "custom", path: ["tiers", index, "fromInputTokens"], message: "Price thresholds must be strictly increasing." });
42
+ });
43
+ });
44
+ export type ModelCost = z.infer<typeof modelCostSchema>;
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ import { contentRefSchema } from "./content.ts";
3
+
4
+ /** mediaType is the MIME type truthfully identified by the client and sent with the bytes; the client does not infer model capabilities from it, and acceptance is determined by server feedback. */
5
+ export const mediaTypeSchema = z.string().regex(/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/);
6
+
7
+ /** Model input contains only text and files; files store immutable content identity and consumption metadata, while bytes and read eligibility stay outside messages. */
8
+ export const modelInputPartSchema = z.discriminatedUnion("kind", [
9
+ z.strictObject({ kind: z.literal("text"), text: z.string() }),
10
+ z.strictObject({ kind: z.literal("file"), file: contentRefSchema,
11
+ mediaType: mediaTypeSchema, filename: z.string().min(1).max(255).optional() }),
12
+ ]);
13
+ export type ModelInputPart = z.infer<typeof modelInputPartSchema>;
14
+ export type ModelFilePart = Extract<ModelInputPart, { kind: "file" }>;
15
+ export const modelInputPartsSchema = z.array(modelInputPartSchema).min(1);
16
+ /** Plain-text consumers explicitly extract the body without implicitly reading files. */
17
+ export function modelInputText(parts: readonly (Extract<ModelInputPart, { kind: "text" }> | { kind: "file" })[]): string {
18
+ return parts.flatMap(part => part.kind === "text" ? [part.text] : []).join("\n");
19
+ }
@@ -0,0 +1,11 @@
1
+ /** When the browser supplies no type, infer common MIME types from the extension; mark as binary when still unrecognized. */
2
+ const extensionTypes: Readonly<Record<string, string>> = {
3
+ jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp", pdf: "application/pdf",
4
+ };
5
+ export function fileMediaType(file: { type: string; name: string }): string {
6
+ return file.type || extensionTypes[file.name.split(".").at(-1)?.toLowerCase() ?? ""] || "application/octet-stream";
7
+ }
8
+ /** A derived classification for UI display, not a model capability. */
9
+ export const previewsAsImage = (mediaType: string) => mediaType.startsWith("image/");
10
+ /** Client resource limits: per-file size, count, and total size leave room for protocol envelopes and base64. */
11
+ export const modelInputLimits = { file: 5 * 1024 * 1024, files: 20, total: 20 * 1024 * 1024 } as const;
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+ import { uiTextSchema, type UiText } from "./i18n.ts";
3
+ import { jsonSchema, type Json } from "./json.ts";
4
+ import { providerUsageIdSchema } from "./provider-usage.ts";
5
+
6
+ export const modelProviderFixedFields = ["protocol", "baseUrl", "auth", "compatibility", "usage"] as const;
7
+ export interface ModelProviderConnection {
8
+ protocol: "openai-responses" | "openai-chat" | "anthropic";
9
+ baseUrl: string;
10
+ auth: "api_key" | "none";
11
+ compatibility: { id: string; value?: Json }[];
12
+ usage?: string | null;
13
+ fixedFields?: (typeof modelProviderFixedFields)[number][];
14
+ }
15
+ export interface ModelProviderPreset {
16
+ id: string;
17
+ name: string;
18
+ billing: "usage" | "subscription" | "none";
19
+ region?: UiText;
20
+ plan?: UiText;
21
+ connection: ModelProviderConnection;
22
+ keyUrl?: string;
23
+ sources: readonly [string, ...string[]];
24
+ verifiedOn: string;
25
+ keywords?: readonly string[];
26
+ }
27
+ const webUrl = z.string().url().refine(value => ["http:", "https:"].includes(new URL(value).protocol));
28
+ export const modelProviderConnectionSchema = z.strictObject({
29
+ protocol: z.enum(["openai-responses", "openai-chat", "anthropic"]),
30
+ baseUrl: webUrl, auth: z.enum(["api_key", "none"]),
31
+ compatibility: z.array(z.strictObject({ id: z.string().min(1), value: jsonSchema.optional() })),
32
+ usage: providerUsageIdSchema.nullable().optional(),
33
+ fixedFields: z.array(z.enum(modelProviderFixedFields)).optional(),
34
+ });
35
+ export const modelProviderPresetSchema = z.strictObject({
36
+ id: z.string().trim().min(1).max(160).regex(/^[^/\s]+$/u), name: z.string().trim().min(1).max(160),
37
+ billing: z.enum(["usage", "subscription", "none"]), region: uiTextSchema.optional(), plan: uiTextSchema.optional(),
38
+ connection: modelProviderConnectionSchema, keyUrl: webUrl.optional(),
39
+ sources: z.tuple([webUrl]).rest(webUrl), verifiedOn: z.iso.date(), keywords: z.array(z.string()).optional(),
40
+ });
41
+ /** patch overrides only explicit fields; connection merges by field, arrays are replaced as a whole, and saved connections are not rewritten. */
42
+ export type ModelProviderPresetContribution =
43
+ | { type: "add"; preset: ModelProviderPreset }
44
+ | { type: "patch"; id: string; patch: Partial<Omit<ModelProviderPreset, "id" | "connection">> & { connection?: Partial<ModelProviderConnection> } };
@@ -0,0 +1,56 @@
1
+ /** Shared payloads for model execution; consuming plugins declare RecordContract identities, and the SDK does not create a separate history. */
2
+ import { z } from "zod";
3
+ import { jsonSchema, type Json } from "./json.ts";
4
+ import { toolOutcomeSchema } from "./tool-result.ts";
5
+ import { presentationSchema, type DataContract } from "./presentation.ts";
6
+ import type { ModelCompletion } from "./models.ts";
7
+ import { errorDetailsSchema } from "./error-details.ts";
8
+
9
+ const id = z.string().min(1).max(160);
10
+ export const modelUsageSchema = z.strictObject({
11
+ input: z.number().nonnegative().nullable(), output: z.number().nonnegative().nullable(),
12
+ total: z.number().nonnegative().nullable(), reasoning: z.number().nonnegative().nullable(),
13
+ cacheRead: z.number().nonnegative().nullable(), cacheWrite: z.number().nonnegative().nullable(),
14
+ });
15
+ /** Dates and times locate events; millisecond offsets come from the same monotonic clock, not from subtracting wall-clock timestamps. */
16
+ export const requestTimingSchema = z.strictObject({
17
+ startedAt: z.iso.datetime(),
18
+ endedAt: z.iso.datetime(),
19
+ durationMs: z.number().nonnegative(),
20
+ firstOutputMs: z.number().nonnegative().nullable(),
21
+ firstTextMs: z.number().nonnegative().nullable(),
22
+ firstReasoningMs: z.number().nonnegative().nullable(),
23
+ lastOutputMs: z.number().nonnegative().nullable(),
24
+ outputChunks: z.number().int().nonnegative(),
25
+ outcome: z.enum(["completed", "failed", "cancelled"]),
26
+ });
27
+ export type RequestTiming = z.infer<typeof requestTimingSchema>;
28
+
29
+ export const modelSourceSchema = z.strictObject({ model: z.string(), adapter: z.string(), scope: z.string() });
30
+ export const modelCompletionSchema = z.strictObject({ usage: modelUsageSchema.nullable(), finishReason: z.string(),
31
+ rawFinishReason: z.string().nullable(), providerResponseId: z.string().nullable(), timing: requestTimingSchema.optional(),
32
+ extensions: z.array(presentationSchema).optional() });
33
+ /** Decode only domain extensions known to the caller; unknown or undecodable extensions do not affect the public terminal state. */
34
+ export function completionData<T extends Json>(completion: ModelCompletion | null | undefined, contract: DataContract<T>): T | null {
35
+ const extension = completion?.extensions?.find(value => value.contract.id === contract.id);
36
+ if (!extension) return null;
37
+ const result = contract.schema.safeParse(structuredClone(extension.data));
38
+ return result.success ? result.data : null;
39
+ }
40
+ const block = { responseId: id, requestParentId: id.nullable(), ordinal: z.number().int().nonnegative(),
41
+ complete: z.boolean(), continuation: jsonSchema, source: modelSourceSchema, completion: modelCompletionSchema.nullable().optional(),
42
+ /** Block generation start and end times (millisecond timestamps); blocks finalized before generation ends have a null end time, and unrecorded times are omitted. See outcome for tool execution times. */
43
+ startedAt: z.number().nonnegative().optional(), endedAt: z.number().nonnegative().nullable().optional() };
44
+ export const modelBlockSchema = z.discriminatedUnion("kind", [
45
+ z.strictObject({ ...block, kind: z.literal("thinking"), text: z.string(), summary: z.string() }),
46
+ z.strictObject({ ...block, kind: z.literal("text"), text: z.string() }),
47
+ z.strictObject({ ...block, kind: z.literal("toolcall"), callId: id, name: id,
48
+ /** Original model text; retained only when parsed arguments are unavailable (the call is incomplete), otherwise arguments is authoritative. */
49
+ argumentsText: z.string().nullable(), arguments: jsonSchema, outcome: toolOutcomeSchema.nullable() })
50
+ .refine(data => data.complete === (data.outcome !== null), "Complete tool calls must have an outcome."),
51
+ ]);
52
+ /** Terminal state is embedded in finalized blocks; execution notifications carry only feedback, not response identity or measurements. */
53
+ export const modelRunRecordSchema = z.discriminatedUnion("type", [
54
+ z.strictObject({ type: z.literal("block"), block: modelBlockSchema }),
55
+ z.strictObject({ type: z.literal("diagnostic"), origin: z.enum(["model", "software"]), code: z.string(), message: z.string(), level: z.enum(["error", "warning"]), details: errorDetailsSchema.optional() }),
56
+ ]);
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+ import { thinkingLevels, type ThinkingLevel } from "./models.ts";
3
+
4
+ /** Model expression `<providerId>/<modelId>`: provider and model always appear as a pair, split at the first `/`. */
5
+ export interface ModelExpression { provider: string; model: string }
6
+ export function parseModelExpression(value: string): ModelExpression | null {
7
+ const index = value.indexOf("/");
8
+ if (index <= 0 || index === value.length - 1) return null;
9
+ return { provider: value.slice(0, index), model: value.slice(index + 1) };
10
+ }
11
+ export function formatModelExpression(expression: ModelExpression): string { return `${expression.provider}/${expression.model}`; }
12
+ export const modelExpressionSchema = z.string().max(321).refine(value => parseModelExpression(value) !== null, "Use a model expression such as provider/model.");
13
+ /** A reference string for a specific model; corresponds one-to-one with model records without using random record IDs. */
14
+ export function modelReference(model: { providerId: string; modelId: string }): string {
15
+ return formatModelExpression({ provider: model.providerId, model: model.modelId });
16
+ }
17
+ export const thinkingSchema = z.enum(thinkingLevels);
18
+
19
+ /** Display and requests share override calculation; empty values inherit defaults, without checking availability or rewriting selections. */
20
+ export function resolveModelSelection(
21
+ defaults: { model: string | null; thinking: ThinkingLevel | null } | undefined,
22
+ overrides: { model?: string | null; thinking?: ThinkingLevel | null },
23
+ ) {
24
+ return {
25
+ model: overrides.model ?? defaults?.model ?? null,
26
+ thinking: overrides.thinking ?? defaults?.thinking ?? null,
27
+ };
28
+ }
29
+
30
+ /** Generates menu options only; does not resolve existing selections or validate requests. */
31
+ export function availableThinkingLevels(model: { capabilities: { reasoning: boolean; thinkingLevels: readonly typeof thinkingLevels[number][] } } | null) {
32
+ if (!model) return thinkingLevels;
33
+ return model.capabilities.reasoning ? thinkingLevels.filter(level => model.capabilities.thinkingLevels.includes(level)) : [];
34
+ }
package/src/models.ts ADDED
@@ -0,0 +1,134 @@
1
+ /** Shared model protocol: independent of resource implementations, agent configuration, and host adapters. */
2
+ import type { Json } from "./json.ts";
3
+ import type { ModelInputPart } from "./model-input.ts";
4
+ import type { ContentReader } from "./content-runtime.ts";
5
+ import type { ToolOutcome, ToolInvocationInfo, ToolResult } from "./tool-result.ts";
6
+ import type { ResourceRef } from "./workspace.ts";
7
+ import type { RequestTiming } from "./model-records.ts";
8
+ import type { Presentation } from "./presentation.ts";
9
+ import type { ErrorDetails } from "./error-details.ts";
10
+
11
+ /** Standard levels shared across the application; protocol encoding does not change public values. */
12
+ export const thinkingLevels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
13
+ export type ThinkingLevel = typeof thinkingLevels[number];
14
+ export interface ModelTarget { provider: string; model: string; thinking?: ThinkingLevel }
15
+ export interface PromptBlock { id: string; text: string; source?: { id: string; plugin: string | null } }
16
+ export interface PromptFile { readonly file: string; readonly text: string }
17
+ export interface SystemPrompt { blocks: readonly PromptBlock[] }
18
+ /** Reasoning text is the plaintext delivered by the protocol, and summary is an independent summary; both can coexist or be empty, and neither fills the other.
19
+ * continuation carries only native data such as signatures/ciphertext; target projection determines whether it can be reused.
20
+ */
21
+ export type ModelPart =
22
+ | { kind: "thinking"; text: string; summary: string; continuation: Json }
23
+ | { kind: "text"; text: string; continuation: Json }
24
+ | { kind: "toolcall"; callId: string; name: string; arguments: Json; continuation: Json };
25
+ export type ModelItem =
26
+ | { kind: "input" | "context"; parts: readonly ModelInputPart[] }
27
+ // scope/adapter constrain native data; they do not gate public content in cross-model requests.
28
+ | { kind: "output"; parts: ModelPart[]; scope: string; adapter: string }
29
+ | { kind: "result"; callId: string; name: string; outcome: ToolOutcome };
30
+ export interface Usage {
31
+ input: number | null; output: number | null; total: number | null;
32
+ reasoning: number | null; cacheRead: number | null; cacheWrite: number | null;
33
+ }
34
+ export type StreamPart =
35
+ | { id: string; kind: "text" | "thinking" }
36
+ | { id: string; kind: "toolcall"; callId: string; name: string };
37
+ export type ModelEvent =
38
+ | { type: "part.start"; part: StreamPart }
39
+ | { type: "part.delta"; partId: string; delta: string; channel?: "summary" }
40
+ | { type: "part.end"; partId: string; arguments?: Json; continuation?: Json }
41
+ | { type: "finish"; reason: string; usage: Usage | null; responseId?: string; rawFinishReason?: string; source?: ModelSource; timing?: RequestTiming; response?: ModelResponse | null };
42
+ export type ModelSource = { model: string; adapter: string; scope: string };
43
+ export type ModelCompletion = {
44
+ usage: Usage | null; finishReason: string; rawFinishReason: string | null; providerResponseId: string | null;
45
+ timing?: RequestTiming;
46
+ /** Typed data declared and consumed by the domain; the public layer preserves unknown contracts. */
47
+ extensions?: Presentation[];
48
+ };
49
+ /** Finalized response blocks; these are not session nodes, and the consumer provides timeline identity and persistence. */
50
+ export type ModelBlock = {
51
+ responseId: string; requestParentId: string | null; ordinal: number; complete: boolean; continuation: Json; source: ModelSource;
52
+ /** Terminal state is delivered once with the actual response block having the highest ordinal; null for all other blocks. */
53
+ completion?: ModelCompletion | null;
54
+ /** Block generation start and end times (millisecond timestamps); blocks finalized before generation ends have a null end time, and unrecorded times are omitted. See outcome for tool execution times. */
55
+ startedAt?: number; endedAt?: number | null;
56
+ } & (
57
+ | { kind: "thinking"; text: string; summary: string }
58
+ | { kind: "text"; text: string }
59
+ | { kind: "toolcall"; callId: string; name: string; argumentsText: string | null; arguments: Json; outcome: ToolOutcome | null }
60
+ );
61
+ export type ModelLiveBlock = { id: string; responseId: string; ordinal: number } & (
62
+ | { kind: "text"; text: string; endedAt: number | null }
63
+ | { kind: "thinking"; text: string; startedAt: number; endedAt: number | null }
64
+ /** During the input phase, arguments is a display preview parsed tolerantly and cannot be used for execution; other phases contain complete arguments. */
65
+ | { kind: "toolcall"; name: string; argumentsText: string; arguments: Json; state: "input" | "ready" | "executing";
66
+ invocation: ToolInvocationInfo | null; result: Extract<ToolResult, { kind: "running" }> | null }
67
+ );
68
+ export type ModelRunEvent =
69
+ | { type: "block"; block: ModelBlock }
70
+ | { type: "progress"; blocks: readonly ModelLiveBlock[] }
71
+ | { type: "diagnostic"; origin: "model" | "software"; code: string; message: string; level: "error" | "warning"; details?: ErrorDetails };
72
+ /** Deliver execution events synchronously; consumers save finalized records, exceptions stop execution, and no further events are delivered after returning. */
73
+ export interface ModelRunObserver { event(event: ModelRunEvent): undefined }
74
+ /** Used only for HTTP request identity; does not enter model messages, history, or resource authorization. */
75
+ export interface ModelRequestIdentity {
76
+ userAgent?: string;
77
+ /** Stable within a session; may be omitted for a one-off call without a session. */
78
+ sessionId?: string;
79
+ }
80
+ /** Transient metadata from the actual HTTP response; contains no request credentials or body and is not automatically written to history. */
81
+ export interface ModelResponse {
82
+ connection: { id: string };
83
+ endpoint: string;
84
+ model: string;
85
+ status: number;
86
+ receivedAt: number;
87
+ headers: Readonly<Record<string, string>>;
88
+ }
89
+ export interface ModelRunRequest {
90
+ object: ResourceRef;
91
+ /** Session identity comes from object; callers may declare their own client identity. */
92
+ userAgent?: string;
93
+ requestParentId: string | null;
94
+ target: ModelTarget;
95
+ system: SystemPrompt | ((files: readonly PromptFile[]) => SystemPrompt);
96
+ messages: ModelItem[];
97
+ /** Filters plugin top-level tools and resource tools selected through actual links by name; does not affect fixed host entry points. */
98
+ tools?: { allow?: readonly string[]; exclude?: readonly string[] };
99
+ cache?: { expectHit: boolean };
100
+ /** Synchronously package domain data after final request preparation; delivered with the actual response's completion. */
101
+ completionExtensions?: (context: ModelRequestContext) => readonly Presentation[];
102
+ signal: AbortSignal;
103
+ observer: ModelRunObserver;
104
+ }
105
+ export type ModelRunResult = {
106
+ responseId: string | null; source: ModelSource | null; completion: ModelCompletion | null; tools: readonly ToolOutcome[];
107
+ /** Metadata from the last HTTP response received during this call; null if none was received, and excluded from response blocks and history. */
108
+ response: ModelResponse | null;
109
+ /** Measured timing for this call; still available for transient feedback when no terminal state was obtained. */
110
+ timing: RequestTiming | null;
111
+ } & ({ reason: "completed" } | { reason: "cancelled" } | { reason: "failed"; code: string; details?: ErrorDetails });
112
+ /** The final request's input context; an independent copy for domain extension producers to read. */
113
+ export interface ModelRequestContext {
114
+ system: SystemPrompt;
115
+ tools: readonly { name: string; description: string; parameters: Json }[];
116
+ }
117
+ export interface StreamRequest extends ModelRequestIdentity {
118
+ target: ModelTarget;
119
+ system: SystemPrompt;
120
+ messages: ModelItem[];
121
+ /** The output limit for this completion; uses the smaller of this value and the model's configured limit. */
122
+ maxOutputTokens?: number;
123
+ tools?: { name: string; description: string; parameters: Json }[];
124
+ signal: AbortSignal;
125
+ }
126
+ export interface ModelInputOptions { content?: ContentReader }
127
+ export interface ModelServices {
128
+ /** Check explicit request input and target without sending a model request or saving state. */
129
+ validate(request: Pick<StreamRequest, "target" | "system" | "messages" | "signal" | "userAgent" | "sessionId">, options?: ModelInputOptions): Promise<void>;
130
+ stream(request: StreamRequest, options?: ModelInputOptions): AsyncIterable<ModelEvent>;
131
+ /** One request and its tool batch; neither saves history nor drives the next request. */
132
+ run(request: ModelRunRequest): Promise<ModelRunResult>;
133
+ list(): readonly { model: string; provider: string; name: string; contextWindow: number | null; capabilities: { reasoning: boolean; tools: boolean; thinkingLevels: readonly ThinkingLevel[] } }[];
134
+ }
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+ import type { Json } from "./json.ts";
3
+ import { contractReferenceSchema, type ContractReference, type DataContract } from "./presentation.ts";
4
+
5
+ export interface ResourceContract<S extends Json = Json> {
6
+ kind: string;
7
+ /** Decodes saved state; creation callbacks return S for encoding before persistence. State is arbitrary JSON, stored separately from host identity and title. */
8
+ stateSchema: z.ZodType<S>;
9
+ }
10
+ export interface ToolContract<A extends Json = Json> extends ContractReference {
11
+ name: string;
12
+ description: string;
13
+ guidelines: readonly string[];
14
+ argsSchema: z.ZodType<A>;
15
+ }
16
+ /** Tool definitions fix selection descriptions, usage guidance, and args; implementations add only target/result and execution information. */
17
+ export interface ImplementationContract<S extends Json, T extends Json, A extends Json, O extends Json> extends DataContract<O> {
18
+ resource: ResourceContract<S>;
19
+ tool: ToolContract<A>;
20
+ targetSchema: z.ZodType<T>;
21
+ effect: "read" | "write";
22
+ execution: "parallel" | "sequential";
23
+ }
24
+ export function defineResource<S extends Json>(kind: string, stateSchema: z.ZodType<S>): ResourceContract<S> {
25
+ return { kind, stateSchema };
26
+ }
27
+ /** Tool args/target/result are projected as JSON Schema for the model; representability is validated at declaration time to avoid failures only when invoked. */
28
+ function requireDescription(schema: z.ZodType, subject: string): void {
29
+ if (!schema.description?.trim()) throw new Error(`Missing schema description: ${subject}`);
30
+ try { z.toJSONSchema(schema); } catch (error) { throw new Error(`Schema is not representable as JSON Schema: ${subject}`, { cause: error }); }
31
+ }
32
+ export function defineTool<A extends Json>(definition: ToolContract<A>): ToolContract<A> {
33
+ if (!definition.name.trim() || !definition.description.trim()) throw new Error("Tool name and description must not be empty.");
34
+ if (definition.guidelines.some(guideline => !guideline.trim())) throw new Error(`Empty tool guideline: ${definition.name}`);
35
+ requireDescription(definition.argsSchema, `${definition.name}.args`);
36
+ return { ...definition, ...contractReferenceSchema.parse({ id: definition.id }), guidelines: [...definition.guidelines] };
37
+ }
38
+ export function defineImplementation<S extends Json, T extends Json, A extends Json, O extends Json>(
39
+ definition: ImplementationContract<S, T, A, O>,
40
+ ): ImplementationContract<S, T, A, O> {
41
+ requireDescription(definition.targetSchema, `${definition.id}.target`);
42
+ requireDescription(definition.schema, `${definition.id}.result`);
43
+ return definition;
44
+ }
package/src/objects.ts ADDED
@@ -0,0 +1,89 @@
1
+ import type { Json } from "./json.ts";
2
+ import type { DataContract, Presentation } from "./presentation.ts";
3
+ import type { RecordContract } from "./records.ts";
4
+ import type { ResourceRef, ResourceCreationResult, ResourceSnapshot, SessionSnapshot } from "./workspace.ts";
5
+ import type { ContentReader, ContentScope } from "./content-runtime.ts";
6
+ import type { EventPublisher } from "./events.ts";
7
+ import type { ActionContract, CreationContract } from "./commands.ts";
8
+
9
+ export interface ResourceCreationOptions {
10
+ signal?: AbortSignal;
11
+ /** Read eligibility for content referenced by the input; defaults to the current session content facade when omitted. */
12
+ content?: ContentReader;
13
+ /** Link the new resource to this recipient, committed atomically with creation. */
14
+ linkTo?: ResourceRef;
15
+ /** Initial resource links received by the new resource, committed atomically with creation. */
16
+ resources?: readonly ResourceRef[];
17
+ }
18
+ export interface ResourceActionOptions {
19
+ signal?: AbortSignal;
20
+ /** The valid content source for this action's input; defaults to the caller's bound session/object content facade when omitted. */
21
+ content?: ContentReader;
22
+ }
23
+
24
+ /** Data and event writes within a synchronous transaction; the host limits the validity of writer calls. */
25
+ export interface SessionWriter {
26
+ /** Append to the current timeline tip at commit time and advance the cursor; previously obtained read-only snapshots do not determine the append position. */
27
+ append<T extends Json>(contract: RecordContract<T>, data: NoInfer<T>): string;
28
+ set<T extends Json>(contract: DataContract<T>, data: NoInfer<T>): void;
29
+ }
30
+ export type ObjectWriter = SessionWriter;
31
+ export interface SessionReader {
32
+ readonly id: string;
33
+ readonly content: ContentReader;
34
+ snapshot(): SessionSnapshot;
35
+ get<T extends Json>(contract: DataContract<T>): T | null;
36
+ readonly title: { get(): string };
37
+ }
38
+ /** Generic execution facade at the session level; resource operations use explicit references, and the specific call entry point binds the caller's perspective. */
39
+ export interface SessionExecutor extends SessionReader {
40
+ create<I, S extends Json, O extends Json>(contract: CreationContract<I, S, O>, input: NoInfer<I> & Json, options?: ResourceCreationOptions): Promise<ResourceCreationResult>;
41
+ /** Invoke a resource's public action; returning means action handling is complete, while the resource manages any independent managed work itself. */
42
+ invoke<I, O extends Json>(contract: ActionContract<I, O>, target: ResourceRef, input: NoInfer<I> & Json, options?: ResourceActionOptions): Promise<void>;
43
+ /** Link a resource to a recipient; both ends are resources, and the recipient's resource type declares support for receiving links. */
44
+ link(receiver: ResourceRef, resource: ResourceRef): void;
45
+ /** Unlink the specified direction; the reverse link remains independently. */
46
+ unlink(receiver: ResourceRef, resource: ResourceRef): void;
47
+ /** Remove a settled object and all its links; preserve history and external entities. */
48
+ remove(resource: ResourceRef): void;
49
+ /** Append a resource rename event when the title changes; return the title with leading and trailing whitespace removed. */
50
+ rename(resource: ResourceRef, title: string): string;
51
+ commit(work: (writer: SessionWriter) => undefined): void;
52
+ /** Append a host title assignment atomically, with optional plugin-owned detail. */
53
+ readonly title: SessionReader["title"] & { set(text: string, detail?: Presentation): void };
54
+ }
55
+ /**
56
+ * An operation facade for the same session from the perspective of the tool caller and this request.
57
+ * The host binds the originating resource, the effective link scope at request start, and the tool execution scope, handling identity and execution validation consistently.
58
+ * Operations originate from the initiating resource: newly created resources link to it, while removal and rename targets resolve within this request's effective link scope.
59
+ * New operations use this perspective, explicitly defining implicitly bound endpoints, the operable resource scope, and the execution scope.
60
+ */
61
+ export interface ToolSession extends SessionReader {
62
+ create<I, S extends Json, O extends Json>(contract: CreationContract<I, S, O>, input: NoInfer<I> & Json, options?: {
63
+ resources?: readonly string[];
64
+ /** Link the bound initiator to the new resource, committed atomically with creation; defaults to false when omitted. */
65
+ exposeSelf?: boolean;
66
+ }): Promise<ResourceCreationResult>;
67
+ remove(resourceId: string): void;
68
+ rename(resourceId: string, title: string): string;
69
+ }
70
+ export interface ObjectReader {
71
+ readonly ref: ResourceRef;
72
+ readonly content: ContentReader;
73
+ readonly session: SessionReader;
74
+ /** The resource snapshot bound to the call; null when identity has not yet been established within the creation transaction. */
75
+ snapshot(): ResourceSnapshot | null;
76
+ /** Current data is independent of the fixed snapshot and is not restored by cursor rewinding or branching. */
77
+ get<T extends Json>(contract: DataContract<T>): T | null;
78
+ }
79
+ export interface ObjectExecutor extends ObjectReader {
80
+ readonly events: EventPublisher;
81
+ /** Staging is valid only within the host execution scope; reads are validated against content already owned by the object or supplied in this input. */
82
+ readonly content: ContentScope;
83
+ readonly session: SessionExecutor;
84
+ commit(work: (writer: ObjectWriter) => undefined): void;
85
+ /** Mark asynchronous work that depends on the current timeline path; nested holds release independently, and cancellation waits for work to actually end. */
86
+ withBusy<T>(work: (signal: AbortSignal) => Promise<T>, options?: { signal?: AbortSignal }): Promise<T>;
87
+ /** Notify the host to query the projection again after in-memory display data changes; does not append history or save resource data. */
88
+ invalidate(): void;
89
+ }
package/src/plugin.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { z } from "zod";
2
+ import { uiTextSchema } from "./i18n.ts";
3
+ import type { Json } from "./json.ts";
4
+ import { bindMessages, type SourceMessage, type UiMessage } from "./i18n.ts";
5
+ import { defineData, type DataContract } from "./presentation.ts";
6
+ import { defineResource, type ResourceContract } from "./module-contract.ts";
7
+ import { defineConfig, type ConfigContract } from "./config.ts";
8
+ import { defineEvent, type EventContract, type EventDefinition } from "./events.ts";
9
+
10
+ const pluginIdPattern = /^[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*)+$/u;
11
+ const shortNamePattern = /^[a-z0-9][a-z0-9._-]*$/u;
12
+
13
+ /** Static information can be read without executing plugin code; the host resolves icon to an image within the package. */
14
+ export const pluginMetadataSchema = z.strictObject({
15
+ name: uiTextSchema.optional(), description: uiTextSchema.optional(),
16
+ version: z.string().optional(), author: z.string().optional(), icon: z.string().optional(),
17
+ });
18
+ export type PluginMetadata = z.infer<typeof pluginMetadataSchema>;
19
+
20
+ /**
21
+ * Plugin namespace: declare plugin identity only once here; contract files declare resource types and data contracts using short names,
22
+ * which are expanded here to `<pluginId>/<name>`. Pure contracts shared by frontend and backend.
23
+ */
24
+ export interface PluginNamespace {
25
+ readonly id: string;
26
+ messages<T extends Record<string, SourceMessage>>(sourceLocale: string, definitions: T): { readonly [K in keyof T]: UiMessage };
27
+ name(short: string): string;
28
+ resource<S extends Json>(name: string, stateSchema: z.ZodType<S>): ResourceContract<S>;
29
+ data<T extends Json>(name: string, schema: z.ZodType<T>): DataContract<T>;
30
+ event<I, D extends Json>(name: string, definition: EventDefinition<I, D>): EventContract<I, D>;
31
+ /** The plugin's sole user configuration contract. */
32
+ config<T extends Json>(schema: z.ZodType<T>, defaults: NoInfer<T>): ConfigContract<T>;
33
+ }
34
+ export function pluginNamespace(id: string): PluginNamespace {
35
+ if (!pluginIdPattern.test(id)) throw new Error(`Invalid plugin ID: ${id}`);
36
+ const name = (short: string) => {
37
+ if (!shortNamePattern.test(short)) throw new Error(`Invalid short name in plugin ${id}: ${short}`);
38
+ return `${id}/${short}`;
39
+ };
40
+ return {
41
+ id, name,
42
+ messages: (sourceLocale, definitions) => bindMessages(id, sourceLocale, definitions),
43
+ resource: (short, stateSchema) => defineResource(name(short), stateSchema),
44
+ data: (short, schema) => defineData(name(short), schema),
45
+ event: (short, definition) => defineEvent(name(short), definition),
46
+ config: (schema, defaults) => defineConfig(id, schema, defaults),
47
+ };
48
+ }
49
+ export function belongsTo(pluginId: string, qualified: string): boolean {
50
+ return qualified.startsWith(`${pluginId}/`) && qualified.length > pluginId.length + 1;
51
+ }
52
+ /** Extract the source plugin from a namespaced ID; return null when there is no namespace. */
53
+ export function pluginOf(qualified: string): string | null {
54
+ const index = qualified.indexOf("/");
55
+ return index > 0 ? qualified.slice(0, index) : null;
56
+ }
57
+ /** Resource projection for a missing implementation: preserves raw data and the source plugin for fallback UI display. */
58
+ export const missingResourceContract = { id: "asterism.host/missing-resource" } as const;
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+ import { jsonSchema, type Json } from "./json.ts";
3
+ import { encodeJson } from "./data-encoding.ts";
4
+
5
+ export const contractReferenceSchema = z.strictObject({ id: z.string().min(1).max(160) });
6
+ export type ContractReference = z.infer<typeof contractReferenceSchema>;
7
+ /** The schema decodes stored or transported JSON into T; producers encode T through the same schema. */
8
+ export interface DataContract<T extends Json = Json> extends ContractReference { schema: z.ZodType<T> }
9
+ export const presentationSchema = z.strictObject({ contract: contractReferenceSchema, data: jsonSchema });
10
+ export type Presentation = z.infer<typeof presentationSchema>;
11
+
12
+ /** Shared declarations depend on no process or UI; each end registers its own implementation. */
13
+ export function defineData<T extends Json>(id: string, schema: z.ZodType<T>): DataContract<T> {
14
+ return { ...contractReferenceSchema.parse({ id }), schema };
15
+ }
16
+ export function reference(contract: ContractReference): ContractReference {
17
+ return { id: contract.id };
18
+ }
19
+ export function present<T extends Json>(contract: DataContract<T>, value: NoInfer<T>): Presentation {
20
+ return { contract: reference(contract), data: encodeJson(contract.schema, value, contract.id) };
21
+ }