@asterism-app/sdk 0.0.0-stage → 0.47.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 (90) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +20 -2
  3. package/docs/data-and-lifecycle.md +77 -0
  4. package/docs/getting-started.md +37 -0
  5. package/docs/manual.md +34 -0
  6. package/docs/minimal-plugin.md +33 -0
  7. package/docs/project-structure.md +27 -0
  8. package/docs/resources-and-tools.md +29 -0
  9. package/docs/views-and-settings.md +35 -0
  10. package/examples/author.ts +158 -0
  11. package/examples/consume-usage.ts +29 -0
  12. package/examples/content-author.ts +29 -0
  13. package/examples/content-contract.ts +10 -0
  14. package/examples/content-renderer.ts +26 -0
  15. package/examples/counter/docs/usage.md +9 -0
  16. package/examples/counter/package.json +27 -0
  17. package/examples/counter/src/contracts.ts +31 -0
  18. package/examples/counter/src/panel.module.css +2 -0
  19. package/examples/counter/src/renderer.tsx +69 -0
  20. package/examples/counter/src/runtime.ts +43 -0
  21. package/examples/counter/src/storage.ts +4 -0
  22. package/examples/counter/src/styles.d.ts +4 -0
  23. package/examples/counter/tsconfig.json +16 -0
  24. package/examples/counter-contract.ts +18 -0
  25. package/examples/data-codecs.ts +18 -0
  26. package/examples/events.ts +37 -0
  27. package/examples/i18n.ts +18 -0
  28. package/examples/model-completion.ts +15 -0
  29. package/examples/model-input.ts +16 -0
  30. package/examples/plugin-calls.ts +22 -0
  31. package/examples/provider-usage-headers.ts +20 -0
  32. package/examples/provider-usage.ts +34 -0
  33. package/examples/renderer.ts +47 -0
  34. package/examples/session-resources.ts +29 -0
  35. package/examples/settings-contract.ts +8 -0
  36. package/examples/settings-renderer.ts +30 -0
  37. package/examples/settings-runtime.ts +11 -0
  38. package/examples/storage.ts +26 -0
  39. package/examples/tool-renderer.ts +47 -0
  40. package/examples/web-provider.ts +27 -0
  41. package/package.json +39 -4
  42. package/src/calls.ts +27 -0
  43. package/src/card.ts +7 -0
  44. package/src/channels.ts +20 -0
  45. package/src/commands.ts +28 -0
  46. package/src/config.ts +25 -0
  47. package/src/content-runtime.ts +11 -0
  48. package/src/content.ts +6 -0
  49. package/src/contracts.ts +29 -0
  50. package/src/data-encoding.ts +14 -0
  51. package/src/error-details.ts +78 -0
  52. package/src/events.ts +91 -0
  53. package/src/i18n.ts +58 -0
  54. package/src/json.ts +4 -0
  55. package/src/model-cost-estimate.ts +49 -0
  56. package/src/model-cost.ts +44 -0
  57. package/src/model-input.ts +19 -0
  58. package/src/model-media.ts +11 -0
  59. package/src/model-records.ts +56 -0
  60. package/src/model-selection.ts +34 -0
  61. package/src/models.ts +137 -0
  62. package/src/module-contract.ts +44 -0
  63. package/src/objects.ts +89 -0
  64. package/src/plugin.ts +58 -0
  65. package/src/presentation.ts +21 -0
  66. package/src/provider-usage.ts +87 -0
  67. package/src/records.ts +60 -0
  68. package/src/renderer-calls.ts +11 -0
  69. package/src/renderer-card.ts +29 -0
  70. package/src/renderer-content.ts +9 -0
  71. package/src/renderer-creation.ts +83 -0
  72. package/src/renderer-i18n.ts +49 -0
  73. package/src/renderer-island.ts +66 -0
  74. package/src/renderer-records.ts +49 -0
  75. package/src/renderer-services.ts +75 -0
  76. package/src/renderer-tools.ts +103 -0
  77. package/src/renderer-view-state.ts +33 -0
  78. package/src/renderer.ts +158 -0
  79. package/src/runtime-calls.ts +20 -0
  80. package/src/runtime-channels.ts +24 -0
  81. package/src/runtime-contracts.ts +188 -0
  82. package/src/runtime-providers.ts +60 -0
  83. package/src/runtime-resources.ts +117 -0
  84. package/src/runtime-tools.ts +31 -0
  85. package/src/runtime-web.ts +36 -0
  86. package/src/runtime.ts +44 -0
  87. package/src/tool-result.ts +45 -0
  88. package/src/tool-validation.ts +14 -0
  89. package/src/web.ts +75 -0
  90. package/src/workspace.ts +43 -0
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "@asterism-app/sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.47.0",
4
+ "description": "Public contracts and host capabilities for Asterism plugins",
5
+ "license": "MIT",
6
+ "author": "Chien Zhang",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/d3ara1n/asterism.git",
10
+ "directory": "packages/sdk"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public",
14
+ "tag": "next",
15
+ "registry": "https://registry.npmjs.org/"
16
+ },
17
+ "type": "module",
18
+ "exports": {
19
+ "./package.json": "./package.json",
20
+ "./contracts": "./src/contracts.ts",
21
+ "./runtime": "./src/runtime.ts",
22
+ "./renderer": "./src/renderer.ts"
23
+ },
24
+ "peerDependencies": {
25
+ "zod": "^4.6.5",
26
+ "react": "^19.3.0",
27
+ "react-intl": "^12.1.4"
28
+ },
29
+ "peerDependenciesMeta": {
30
+ "react-intl": {
31
+ "optional": true
32
+ }
33
+ },
34
+ "files": [
35
+ "src",
36
+ "!src/**/*.test.ts",
37
+ "docs",
38
+ "examples",
39
+ "README.md"
40
+ ]
41
+ }
package/src/calls.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type { z } from "zod";
2
+ import type { Json } from "./json.ts";
3
+ import { contractReferenceSchema } from "./presentation.ts";
4
+ import type { ResourceRef } from "./workspace.ts";
5
+
6
+ export type CallScope = "plugin" | "object";
7
+ /** Scope is declared by the contract; callers cannot downgrade an object-level operation by omitting the object. */
8
+ export interface CallContract<Input extends z.ZodType<Json> = z.ZodType<Json>, Output extends z.ZodType<Json> = z.ZodType<Json>, S extends CallScope = CallScope> {
9
+ readonly id: string;
10
+ readonly scope: S;
11
+ readonly input: Input;
12
+ readonly output: Output;
13
+ }
14
+ export function defineCall<Input extends z.ZodType<Json>, Output extends z.ZodType<Json>, S extends CallScope>(id: string, definition: {
15
+ scope: S; input: Input; output: Output;
16
+ }): CallContract<Input, Output, S> {
17
+ if (definition.scope !== "plugin" && definition.scope !== "object") throw new Error("A call must declare plugin or object scope.");
18
+ return Object.freeze({ ...contractReferenceSchema.parse({ id }), ...definition });
19
+ }
20
+ export type CallOptions<S extends CallScope = CallScope> = { signal?: AbortSignal } &
21
+ (S extends "object" ? { object: ResourceRef } : { object?: never });
22
+ export interface PluginCalls {
23
+ /** Submit JSON matching the input schema's input; receive the output schema's parsed result. */
24
+ invoke<Input extends z.ZodType<Json>, Output extends z.ZodType<Json>, S extends CallScope>(
25
+ contract: CallContract<Input, Output, S>, input: NoInfer<z.input<Input>> & Json, options: CallOptions<NoInfer<S>>,
26
+ ): Promise<z.output<Output>>;
27
+ }
package/src/card.ts ADDED
@@ -0,0 +1,7 @@
1
+ /** Card geometry declarations shared by frontend and backend, measured in canvas units. */
2
+ export interface CardAppearance {
3
+ width: number;
4
+ height: number;
5
+ minWidth?: number;
6
+ minHeight?: number;
7
+ }
@@ -0,0 +1,20 @@
1
+ import { contractReferenceSchema } from "./presentation.ts";
2
+
3
+ /** Channel payloads: JSON values and bytes supported by structured cloning, with arbitrary nesting. The host does not validate each frame. */
4
+ export type ChannelValue = null | boolean | number | string | Uint8Array | readonly ChannelValue[] | { readonly [key: string]: ChannelValue };
5
+ /** Channel end reasons: closed means either end closed it explicitly; removed means the object left the current set; unavailable means the contract or object is unavailable; disconnected means the connection was lost. */
6
+ export type ChannelEnd = { reason: "closed" | "removed" | "unavailable" | "disconnected"; message?: string };
7
+
8
+ /**
9
+ * Object-level duplex channel contract: opened by the frontend and routed by the backend to the object's owning plugin according to the contract.
10
+ * Up carries frontend-to-backend messages, and Down carries backend-to-frontend messages; both ends deliver in order without per-frame acknowledgments.
11
+ */
12
+ export interface ChannelContract<Up extends ChannelValue = ChannelValue, Down extends ChannelValue = ChannelValue> {
13
+ readonly id: string;
14
+ readonly kind: "channel";
15
+ /** For type inference only. */
16
+ readonly __types?: { up: Up; down: Down };
17
+ }
18
+ export function defineChannel<Up extends ChannelValue, Down extends ChannelValue>(id: string): ChannelContract<Up, Down> {
19
+ return Object.freeze({ ...contractReferenceSchema.parse({ id }), kind: "channel" as const });
20
+ }
@@ -0,0 +1,28 @@
1
+ import type { z } from "zod";
2
+ import { jsonSchema, type Json } from "./json.ts";
3
+ import type { ResourceContract } from "./module-contract.ts";
4
+ import { contractReferenceSchema, type ContractReference } from "./presentation.ts";
5
+
6
+ /** I is the invocation input and O is the schema parsing result; both are inferred automatically from the declared schema. */
7
+ export interface CreationContract<I = Json, S extends Json = Json, O extends Json = Json> extends ContractReference {
8
+ readonly kind: "creation";
9
+ readonly resource: ResourceContract<S>;
10
+ readonly schema: z.ZodType<O, I>;
11
+ }
12
+ export interface ActionContract<I = Json, O extends Json = Json> extends ContractReference {
13
+ readonly kind: "action";
14
+ readonly resource: ResourceContract;
15
+ readonly schema: z.ZodType<O, I>;
16
+ }
17
+ export function defineCreation<S extends Json, I, O extends Json>(resource: ResourceContract<S>, definition: ContractReference & { schema: z.ZodType<O, I> }): CreationContract<I, S, O> {
18
+ return { ...contractReferenceSchema.parse({ id: definition.id }), kind: "creation", resource, schema: definition.schema };
19
+ }
20
+ export function defineAction<I, O extends Json>(resource: ResourceContract, definition: ContractReference & { schema: z.ZodType<O, I> }): ActionContract<I, O> {
21
+ return { ...contractReferenceSchema.parse({ id: definition.id }), kind: "action", resource, schema: definition.schema };
22
+ }
23
+ /** Command envelopes carry only stable identity and raw JSON input; the host parses them using assembled declarations. */
24
+ export const resourceCallSchema = contractReferenceSchema.extend({ input: jsonSchema });
25
+ export type ResourceCall = z.infer<typeof resourceCallSchema>;
26
+ export function resourceCall<I, O extends Json>(contract: CreationContract<I, Json, O> | ActionContract<I, O>, input: NoInfer<I> & Json): ResourceCall {
27
+ return { id: contract.id, input };
28
+ }
package/src/config.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { z } from "zod";
2
+ import type { Json } from "./json.ts";
3
+ import { encodeJson } from "./data-encoding.ts";
4
+
5
+ /**
6
+ * Plugin configuration contract: settings agreed between users and plugins, with one configuration per plugin shared by frontend and backend.
7
+ * Identity is the plugin ID; the host handles storage, validation, and change notifications, while the plugin provides the page. Preferences such as the last selection are not configuration.
8
+ */
9
+ export interface ConfigContract<T extends Json = Json> {
10
+ readonly plugin: string;
11
+ /** Decodes stored and transported configuration; updates and defaults use T. */
12
+ readonly schema: z.ZodType<T>;
13
+ readonly defaults: T;
14
+ }
15
+ export function defineConfig<T extends Json>(plugin: string, schema: z.ZodType<T>, defaults: NoInfer<T>): ConfigContract<T> {
16
+ const parsed = schema.safeParse(encodeJson(schema, structuredClone(defaults), `${plugin}.configuration defaults`));
17
+ if (!parsed.success) throw new Error(`Configuration defaults of ${plugin} do not match the schema.`);
18
+ return Object.freeze({ plugin, schema, defaults: structuredClone(parsed.data) });
19
+ }
20
+ /** Decode saved values according to the contract; fall back to defaults when invalid or missing, leaving status display to the caller. */
21
+ export function readConfig<T extends Json>(contract: ConfigContract<T>, raw: unknown): { value: T; status: "saved" | "defaults" | "invalid" } {
22
+ if (raw === undefined) return { value: structuredClone(contract.defaults), status: "defaults" };
23
+ const parsed = contract.schema.safeParse(raw);
24
+ return parsed.success ? { value: parsed.data, status: "saved" } : { value: structuredClone(contract.defaults), status: "invalid" };
25
+ }
@@ -0,0 +1,11 @@
1
+ import type { ContentRef, ContentRange } from "./content.ts";
2
+
3
+ /** Individual reads have a size limit; large content uses bounded byte chunks through open. */
4
+ export interface ContentReader {
5
+ read(ref: ContentRef, range?: ContentRange): Promise<Uint8Array>;
6
+ open(ref: ContentRef, range?: ContentRange): AsyncIterable<Uint8Array>;
7
+ }
8
+ /** Temporary workspace managed by the host; stage neither modifies the session nor establishes persistent references. */
9
+ export interface ContentScope extends ContentReader {
10
+ stage(source: Uint8Array | AsyncIterable<Uint8Array>): Promise<ContentRef>;
11
+ }
package/src/content.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { z } from "zod";
2
+
3
+ /** A JSON reference value reserved by the SDK; identity contains no path and grants no right to read or retain content. */
4
+ export const contentRefSchema = z.strictObject({ "$asterism.content": z.string().uuid() });
5
+ export type ContentRef = z.infer<typeof contentRefSchema>;
6
+ export interface ContentRange { offset?: number; length?: number }
@@ -0,0 +1,29 @@
1
+ /** SDK pure contract entry point: does not depend on Node, Electron, React, or host source code. */
2
+ export * from "./json.ts";
3
+ export * from "./error-details.ts";
4
+ export * from "./presentation.ts";
5
+ export * from "./module-contract.ts";
6
+ export type { CardAppearance } from "./card.ts";
7
+ export * from "./plugin.ts";
8
+ export * from "./records.ts";
9
+ export * from "./events.ts";
10
+ export * from "./calls.ts";
11
+ export * from "./channels.ts";
12
+ export * from "./tool-result.ts";
13
+ export * from "./workspace.ts";
14
+ export type * from "./models.ts";
15
+ export { thinkingLevels } from "./models.ts";
16
+ export * from "./model-records.ts";
17
+ export { validationMessage, jsonArgumentHint } from "./tool-validation.ts";
18
+ export * from "./commands.ts";
19
+ export * from "./i18n.ts";
20
+ export * from "./config.ts";
21
+ export * from "./content.ts";
22
+ export * from "./model-input.ts";
23
+
24
+ export * from "./model-selection.ts";
25
+ export * from "./model-cost.ts";
26
+ export * from "./model-cost-estimate.ts";
27
+ export * from "./model-media.ts";
28
+ export * from "./provider-usage.ts";
29
+ export * from "./web.ts";
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+ import { jsonSchema, type Json } from "./json.ts";
3
+ import { validationMessage } from "./tool-validation.ts";
4
+
5
+ /** Data producers supply decoded values; storage and transport retain the JSON encoding. */
6
+ export function encodeJson<T extends Json>(schema: z.ZodType<T>, value: T, subject: string): Json {
7
+ try { return jsonSchema.parse(z.encode(schema, value)); }
8
+ catch (error) {
9
+ if (!(error instanceof z.ZodError) && !(error instanceof z.core.$ZodEncodeError)) throw error;
10
+ const detail = error instanceof z.ZodError ? validationMessage(error, subject)
11
+ : `${subject}: ${error.message}`;
12
+ throw new Error(`Cannot encode data: ${detail}`, { cause: error });
13
+ }
14
+ }
@@ -0,0 +1,78 @@
1
+ import { z } from "zod";
2
+
3
+ /** Transferable raw error data; response bodies and additional data remain text to avoid being scanned as business references. */
4
+ export type ErrorDetails = {
5
+ name?: string;
6
+ message: string;
7
+ code?: string | number;
8
+ statusCode?: number;
9
+ /** The actual HTTP status received; statusCode preserves the error object's original value, which may be inferred by the SDK. */
10
+ httpStatus?: number;
11
+ responseBody?: string;
12
+ /** The streaming response fragment actually received by the transport when the SDK fails before delivering the first model block. */
13
+ rawResponse?: string;
14
+ rawEvent?: string;
15
+ responseHeaders?: Record<string, string>;
16
+ data?: string;
17
+ stack?: string;
18
+ causes?: ErrorDetails[];
19
+ };
20
+ export const errorDetailsSchema: z.ZodType<ErrorDetails> = z.strictObject({
21
+ name: z.string().optional(), message: z.string(), code: z.union([z.string(), z.number()]).optional(),
22
+ statusCode: z.number().optional(), httpStatus: z.number().optional(), responseBody: z.string().optional(),
23
+ rawResponse: z.string().optional(), rawEvent: z.string().optional(),
24
+ responseHeaders: z.record(z.string(), z.string()).optional(), data: z.string().optional(),
25
+ stack: z.string().optional(), causes: z.lazy(() => errorDetailsSchema.array()).optional(),
26
+ });
27
+
28
+ /** Extract the error itself and response data; request bodies, request headers, and credentials are not error details. */
29
+ export function describeError(error: unknown): ErrorDetails {
30
+ const seen = new Set<object>();
31
+ const encode = (value: unknown): string => {
32
+ if (typeof value === "string") return value;
33
+ const ancestors: object[] = [];
34
+ return JSON.stringify(value, function (_key, item: unknown) {
35
+ if (typeof item === "bigint") return String(item);
36
+ if (item && typeof item === "object") {
37
+ while (ancestors.length && ancestors.at(-1) !== this) ancestors.pop();
38
+ if (ancestors.includes(item)) return "[Circular]";
39
+ ancestors.push(item);
40
+ }
41
+ return item;
42
+ }) ?? String(value);
43
+ };
44
+ const visit = (value: unknown): ErrorDetails => {
45
+ if (!value || typeof value !== "object") return { message: typeof value === "string" ? value : encode(value) };
46
+ if (seen.has(value)) return { message: "[Circular error cause]" };
47
+ seen.add(value);
48
+ const record = value as Record<string, unknown>;
49
+ const attached = errorDetailsSchema.safeParse(record.details);
50
+ if (attached.success) return attached.data;
51
+ const { requestBodyValues: _values, requestBody: _body, requestHeaders: _headers, responseHeaders: _responseHeaders, ...raw } = record;
52
+ const details: ErrorDetails = {
53
+ message: typeof record.message === "string" ? record.message : encode(raw),
54
+ };
55
+ if (typeof record.name === "string") details.name = record.name;
56
+ if (typeof record.code === "string" || typeof record.code === "number") details.code = record.code;
57
+ if (typeof record.stack === "string") details.stack = record.stack;
58
+ if (typeof record.statusCode === "number") details.statusCode = record.statusCode;
59
+ if (typeof record.httpStatus === "number") details.httpStatus = record.httpStatus;
60
+ if (typeof record.responseBody === "string") details.responseBody = record.responseBody;
61
+ if (typeof record.rawResponse === "string") details.rawResponse = record.rawResponse;
62
+ if (typeof record.rawEvent === "string") details.rawEvent = record.rawEvent;
63
+ if (record.responseHeaders && typeof record.responseHeaders === "object") {
64
+ details.responseHeaders = Object.fromEntries(Object.entries(record.responseHeaders)
65
+ .filter((entry): entry is [string, string] => typeof entry[1] === "string"
66
+ && !/^(authorization|proxy-authorization|cookie|set-cookie|x-api-key)$/iu.test(entry[0])));
67
+ }
68
+ if (record.data !== undefined) details.data = encode(record.data);
69
+ else if (!(value instanceof Error)) details.data = encode(raw);
70
+ const causes = [
71
+ ...(record.cause !== undefined ? [record.cause] : []),
72
+ ...(Array.isArray(record.errors) ? record.errors : []),
73
+ ];
74
+ if (causes.length) details.causes = causes.map(visit);
75
+ return details;
76
+ };
77
+ return visit(error);
78
+ }
package/src/events.ts ADDED
@@ -0,0 +1,91 @@
1
+ import { z } from "zod";
2
+ import type { Json } from "./json.ts";
3
+ import type { Source } from "./workspace.ts";
4
+ import { uiTextSchema } from "./i18n.ts";
5
+
6
+ export type EventSourceKind = "plugin" | "resource" | "any" | "host";
7
+ export interface EventEnvelope<D extends Json = Json> {
8
+ readonly contract: { readonly id: string };
9
+ readonly emittedAt: string;
10
+ readonly source: Source;
11
+ readonly data: D;
12
+ }
13
+ export type EventSemantics = { readonly kind: "occurrence" } | { readonly kind: "snapshot"; readonly key: readonly string[] };
14
+ export interface EventContract<I = unknown, D extends Json = Json> {
15
+ readonly id: string;
16
+ readonly schema: z.ZodType<D, I>;
17
+ readonly source: EventSourceKind;
18
+ readonly semantics: EventSemantics;
19
+ }
20
+ export interface EventDefinition<I, D extends Json> {
21
+ schema: z.ZodType<D, I>;
22
+ source: EventSourceKind;
23
+ semantics: { kind: "occurrence" } | { kind: "snapshot"; key: readonly Extract<keyof D, string>[] };
24
+ }
25
+ export function defineEvent<I, D extends Json>(id: string, definition: EventDefinition<I, D>): EventContract<I, D> {
26
+ const semantics = definition.semantics.kind === "snapshot"
27
+ ? Object.freeze({ kind: "snapshot" as const, key: Object.freeze([...definition.semantics.key]) })
28
+ : Object.freeze({ kind: "occurrence" as const });
29
+ return Object.freeze({ id, schema: definition.schema, source: definition.source, semantics });
30
+ }
31
+ /** Source conditions filter only the host envelope; they neither interpret domain payloads nor grant resource operation permissions. */
32
+ export interface EventFilter { plugin?: string; sessionId?: string; resourceId?: string; resourceKind?: string }
33
+ export interface EventSubscribeOptions {
34
+ delivery?: "all" | "latest";
35
+ source?: EventFilter;
36
+ signal?: AbortSignal;
37
+ }
38
+ export interface SubscriptionEnd {
39
+ readonly reason: "closed" | "aborted" | "unavailable" | "invalid_contract" | "consumer_failed" | "disconnected";
40
+ readonly message?: string;
41
+ }
42
+ export interface EventSubscription {
43
+ readonly ready: Promise<void>;
44
+ readonly closed: Promise<SubscriptionEnd>;
45
+ close(): void;
46
+ }
47
+ export type EventHandler<D extends Json> = (event: EventEnvelope<D>, context: { signal: AbortSignal }) => void | Promise<void>;
48
+ export interface EventSubscriber {
49
+ subscribe<I, D extends Json>(contract: EventContract<I, D>, options: EventSubscribeOptions, handler: EventHandler<NoInfer<D>>): EventSubscription;
50
+ }
51
+ export interface EventPublisher {
52
+ /** Deliver immediately after synchronous validation, without waiting for consumption or rolling back with the transaction; publish after a successful commit when effects must be confirmed. */
53
+ publish<I, D extends Json>(contract: EventContract<I, D>, data: NoInfer<I> & Json): void;
54
+ }
55
+ export interface PluginEvents extends EventSubscriber, EventPublisher {}
56
+
57
+ /** Public event published by the application itself after a session title actually changes, with no plugin or resource source. */
58
+ export const SessionTitleChanged = defineEvent("asterism/session-title", {
59
+ schema: z.strictObject({ sessionId: z.string(), title: z.string() }),
60
+ source: "host",
61
+ semantics: { kind: "snapshot", key: ["sessionId"] },
62
+ });
63
+
64
+ /** Transient changes to the current resource set; used to release UI state across sessions. */
65
+ export const ResourcePresenceChanged = defineEvent("asterism/resource-presence", {
66
+ schema: z.strictObject({
67
+ sessionId: z.string(),
68
+ appeared: z.array(z.string()),
69
+ disappeared: z.array(z.string()),
70
+ }),
71
+ source: "host",
72
+ semantics: { kind: "occurrence" },
73
+ });
74
+
75
+ /** Each publication fully replaces resource status; running remains visible, while other states are consumed when the session is viewed. */
76
+ export const ResourceStatusChanged = defineEvent("asterism/resource-status", {
77
+ schema: z.discriminatedUnion("status", [
78
+ z.strictObject({
79
+ title: uiTextSchema,
80
+ status: z.literal("running"),
81
+ progress: z.strictObject({ value: z.number().min(0).max(1) }).optional(),
82
+ }),
83
+ z.strictObject({
84
+ title: uiTextSchema,
85
+ status: z.enum(["failed", "awaiting", "completed"]),
86
+ }),
87
+ ]),
88
+ source: "resource",
89
+ semantics: { kind: "snapshot", key: [] },
90
+ });
91
+ export type ResourceStatus = z.infer<typeof ResourceStatusChanged.schema>;
package/src/i18n.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { z } from "zod";
2
+
3
+ export interface SourceMessage {
4
+ readonly id: string;
5
+ readonly defaultMessage: string;
6
+ readonly description?: string;
7
+ }
8
+
9
+ export type MessageValues = Readonly<Record<string, string | number | boolean>>;
10
+
11
+ export interface UiMessage extends SourceMessage {
12
+ readonly kind: "message";
13
+ readonly owner: string;
14
+ readonly sourceLocale: string;
15
+ readonly values?: MessageValues;
16
+ }
17
+
18
+ export type UiText = string | UiMessage;
19
+
20
+ export const localeSchema = z.string().refine(value => {
21
+ try { Intl.getCanonicalLocales(value); return true; }
22
+ catch { return false; }
23
+ }, { message: "Invalid language tag.", abort: true })
24
+ .overwrite(value => Intl.getCanonicalLocales(value)[0]!);
25
+
26
+ export const messageValuesSchema = z.record(z.string(), z.union([z.string(), z.number().finite(), z.boolean()]));
27
+ export const uiMessageSchema = z.strictObject({
28
+ kind: z.literal("message"),
29
+ owner: z.string().min(1),
30
+ sourceLocale: localeSchema,
31
+ id: z.string().min(1),
32
+ defaultMessage: z.string().refine(value => !!value.trim(), "A default message is required."),
33
+ description: z.string().optional(),
34
+ values: messageValuesSchema.optional(),
35
+ });
36
+ export const uiTextSchema = z.union([z.string(), uiMessageSchema]);
37
+
38
+ /** Standard static descriptor shape for extraction by the official FormatJS tooling. */
39
+ export function defineMessages<const T extends Record<string, SourceMessage>>(messages: T): T {
40
+ return messages;
41
+ }
42
+
43
+ export function bindMessages<T extends Record<string, SourceMessage>>(
44
+ owner: string,
45
+ sourceLocale: string,
46
+ definitions: T,
47
+ ): { readonly [K in keyof T]: UiMessage } {
48
+ const locale = localeSchema.parse(sourceLocale);
49
+ const entries = Object.entries(definitions).map(([key, value]) => [
50
+ key,
51
+ Object.freeze(uiMessageSchema.parse({ ...value, kind: "message", owner, sourceLocale: locale })),
52
+ ]);
53
+ return Object.fromEntries(entries) as { readonly [K in keyof T]: UiMessage };
54
+ }
55
+
56
+ export function withValues(message: UiMessage, values: MessageValues): UiMessage {
57
+ return uiMessageSchema.parse({ ...message, values: { ...message.values, ...values } });
58
+ }
package/src/json.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { z } from "zod";
2
+
3
+ export type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
4
+ export const jsonSchema: z.ZodType<Json> = z.json();
@@ -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;