@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
@@ -0,0 +1,31 @@
1
+ import { z } from "zod";
2
+ import {
3
+ pluginNamespace, defineCreation, defineAction, defineCall,
4
+ defineRecord, defineTool, defineImplementation,
5
+ } from "@asterism-app/sdk/contracts";
6
+
7
+ export const ns = pluginNamespace("example.counter");
8
+ export const card = { width: 280, height: 180 };
9
+ export const Counter = ns.resource("counter", z.strictObject({}));
10
+ export const Value = ns.data("value", z.number());
11
+ export const View = ns.data("view", z.number());
12
+ export const Changed = defineRecord(ns.name("changed"), { schema: z.number() });
13
+ export const Create = defineCreation(Counter, { id: ns.name("create"), schema: z.null() });
14
+ export const Increment = defineAction(Counter, { id: ns.name("increment"), schema: z.null() });
15
+ export const Settings = ns.config(
16
+ z.strictObject({ step: z.number().int().min(1).max(10) }), { step: 1 },
17
+ );
18
+ export const ReadStep = defineCall(ns.name("read-step"), {
19
+ scope: "plugin", input: z.null(), output: z.number(),
20
+ });
21
+ const Read = defineTool({
22
+ id: ns.name("read-counter"), name: "read_counter",
23
+ description: "Read the current value of a counter.", guidelines: [],
24
+ argsSchema: z.null().describe("No arguments."),
25
+ });
26
+ export const ReadCounter = defineImplementation({
27
+ ...ns.data("read-result", z.number().describe("The current counter value.")),
28
+ resource: Counter, tool: Read,
29
+ targetSchema: z.null().describe("The whole counter."),
30
+ effect: "read", execution: "parallel",
31
+ });
@@ -0,0 +1,2 @@
1
+ .panel { display: grid; gap: 8px; padding: 12px; color: var(--foreground); }
2
+ .panel button { font: inherit; }
@@ -0,0 +1,69 @@
1
+ import { useRef, useState } from "react";
2
+ import {
3
+ defineRendererPlugin, cardRenderer, creationEntry, toolRenderer,
4
+ useRendererObject, useRendererConfiguration, useRendererCalls, useI18n,
5
+ } from "@asterism-app/sdk/renderer";
6
+ import { ns, card, Counter, View, Create, Increment, Settings, ReadStep, ReadCounter } from "./contracts.ts";
7
+ import styles from "./panel.module.css";
8
+
9
+ function Operation({ label, disabled = false, run }: {
10
+ label: string; disabled?: boolean; run(): Promise<void>;
11
+ }) {
12
+ const pending = useRef(false);
13
+ const [busy, setBusy] = useState(false);
14
+ const [error, setError] = useState("");
15
+ return <div>
16
+ <button disabled={disabled || busy} onClick={() => {
17
+ if (pending.current) return;
18
+ pending.current = true; setBusy(true); setError("");
19
+ void run().catch(error => setError(String(error))).finally(() => {
20
+ pending.current = false; setBusy(false);
21
+ });
22
+ }}>{label}</button>
23
+ {error && <p role="alert">{error}</p>}
24
+ </div>;
25
+ }
26
+
27
+ function CounterBody({ data }: { data: number }) {
28
+ const object = useRendererObject();
29
+ const i18n = useI18n();
30
+ return <div className={styles.panel}>
31
+ <output>{data}</output>
32
+ <Operation label="Increment" disabled={!object.view.ready} run={async () => {
33
+ const receipt = await object.invoke(Increment, null);
34
+ if (!receipt.ok) throw new Error(i18n.text(receipt.error.display ?? receipt.error.message));
35
+ }} />
36
+ </div>;
37
+ }
38
+
39
+ function SettingsPage() {
40
+ const settings = useRendererConfiguration(Settings);
41
+ const calls = useRendererCalls();
42
+ const i18n = useI18n();
43
+ const [reported, setReported] = useState<number | null>(null);
44
+ return <div className={styles.panel}>
45
+ <p>Step: {settings.value.step}</p>
46
+ {settings.error && <p role="alert">{i18n.text(settings.error)}</p>}
47
+ <Operation label="Toggle step between 1 and 2" disabled={!settings.ready}
48
+ run={() => settings.save({ step: settings.value.step === 1 ? 2 : 1 })} />
49
+ <Operation label="Read step from backend"
50
+ run={async () => { setReported(await calls.invoke(ReadStep, null, {})); }} />
51
+ <output>{reported}</output>
52
+ </div>;
53
+ }
54
+
55
+ export default defineRendererPlugin({
56
+ settings: { Body: SettingsPage },
57
+ modules: [{
58
+ resources: [{ kind: Counter.kind, label: "Counter", icon: null, card }],
59
+ creations: [creationEntry(Create, {
60
+ id: ns.name("create-menu"), label: "Counter",
61
+ triggers: { menu: { initialInput: null } },
62
+ })],
63
+ cards: [cardRenderer(View, { ...card, Body: CounterBody })],
64
+ tools: [toolRenderer(ReadCounter, {
65
+ label: "Read counter",
66
+ result: { bodyRenderer: ({ result }) => <output>{result.data ?? "—"}</output> },
67
+ })],
68
+ }],
69
+ });
@@ -0,0 +1,43 @@
1
+ import {
2
+ definePlugin, implementResource, implementCreation,
3
+ implementAction, implementTool, implementCall, type ObjectReader,
4
+ } from "@asterism-app/sdk/runtime";
5
+ import {
6
+ ns, card, Counter, Value, View, Changed, Create, Increment,
7
+ Settings, ReadStep, ReadCounter,
8
+ } from "./contracts.ts";
9
+
10
+ const current = (object: ObjectReader) => object.get(Value) ?? 0;
11
+
12
+ export default definePlugin({
13
+ namespace: ns,
14
+ setup(plugin) {
15
+ const settings = plugin.config(Settings);
16
+ return {
17
+ calls: [implementCall(ReadStep, async () => settings.get().step)],
18
+ resources: [implementResource(Counter, View, {
19
+ card, records: [Changed],
20
+ template: { current: [Value] },
21
+ creation: implementCreation(Create, {
22
+ create: () => ({ title: "Counter", data: {} }),
23
+ }),
24
+ actions: [implementAction(Increment, {
25
+ async execute(_input, object) {
26
+ object.commit(writer => {
27
+ const next = current(object) + settings.get().step;
28
+ writer.set(Value, next);
29
+ writer.append(Changed, next);
30
+ });
31
+ },
32
+ })],
33
+ project: (_data, object) => current(object),
34
+ inspect: async (_data, object) => current(object),
35
+ })],
36
+ tools: [implementTool(ReadCounter, {
37
+ async call(_data, _target, _args, { object }) {
38
+ return { kind: "succeeded", data: current(object) };
39
+ },
40
+ })],
41
+ };
42
+ },
43
+ });
@@ -0,0 +1,4 @@
1
+ import { defineStorage } from "@asterism-app/sdk/runtime";
2
+ export default defineStorage([
3
+ { kind: "json", path: ".asterism/config.json", version: 1, migrations: {} },
4
+ ]);
@@ -0,0 +1,4 @@
1
+ declare module "*.module.css" {
2
+ const classes: Record<string, string>;
3
+ export default classes;
4
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2023", "DOM", "ESNext.Disposable"],
7
+ "types": ["node"],
8
+ "jsx": "react-jsx",
9
+ "strict": true,
10
+ "allowImportingTsExtensions": true,
11
+ "resolveJsonModule": true,
12
+ "skipLibCheck": true,
13
+ "noEmit": true
14
+ },
15
+ "include": ["src"]
16
+ }
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ import { pluginNamespace, defineCreation, defineAction, defineRecord, defineChannel } from "@asterism-app/sdk/contracts";
3
+
4
+ export const sample = pluginNamespace("example.counter");
5
+ export const Counter = sample.resource("counter", z.strictObject({}));
6
+ /** Shared by frontend and backend for creation placeholders and actual cards. */
7
+ export const counterCard = { width: 300, height: 200 };
8
+ export const Count = sample.data("count", z.strictObject({ value: z.number().int() }));
9
+ export const CounterView = sample.data("card", Count.schema);
10
+ export const CountChanged = defineRecord(sample.name("count-changed"), {
11
+ schema: Count.schema,
12
+ });
13
+ const createInput = z.strictObject({ title: z.string().trim().min(1).default("Counter") });
14
+ const actionInput = z.strictObject({ amount: z.number().int() });
15
+ export const CreateCounter = defineCreation(Counter, { id: sample.name("create"), schema: createInput });
16
+ export const ChangeCount = defineAction(Counter, { id: sample.name("change"), schema: actionInput });
17
+ /** Object-level duplex channel: the frontend requests live updates, and the backend sends current values per object; does not enter the timeline. */
18
+ export const CounterUpdates = defineChannel<{ type: "subscribe" }, { value: number }>(sample.name("updates"));
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ import { pluginNamespace, defineRecord, present } from "@asterism-app/sdk/contracts";
3
+ import type { ObjectExecutor } from "@asterism-app/sdk/runtime";
4
+
5
+ const ns = pluginNamespace("example.codec");
6
+ const current = z.strictObject({ text: z.string() });
7
+ /** Read older strings or current objects; write current objects while retaining immutable historical payloads. */
8
+ const textSchema = z.codec(z.union([z.string(), current]), current, {
9
+ decode: value => typeof value === "string" ? { text: value } : value,
10
+ encode: value => value,
11
+ });
12
+ export const Text = ns.data("text", textSchema);
13
+ export const TextChanged = defineRecord(ns.name("text-changed"), { schema: textSchema });
14
+ export function saveText(object: ObjectExecutor, text: string) {
15
+ const value = { text };
16
+ object.commit(writer => { writer.set(Text, value); writer.append(TextChanged, value); });
17
+ return present(Text, value);
18
+ }
@@ -0,0 +1,37 @@
1
+ import { z } from "zod";
2
+ import { pluginNamespace, ResourceStatusChanged } from "@asterism-app/sdk/contracts";
3
+ import { defineModule, type ObjectExecutor } from "@asterism-app/sdk/runtime";
4
+ import type { RendererModule } from "@asterism-app/sdk/renderer";
5
+
6
+ const ns = pluginNamespace("example.downloads");
7
+ export const Progress = ns.event("progress", {
8
+ schema: z.strictObject({ job: z.string(), completed: z.number(), total: z.number() }),
9
+ source: "resource",
10
+ semantics: { kind: "snapshot", key: ["job"] },
11
+ });
12
+ export const backend = defineModule(plugin => {
13
+ plugin.ipc.events.subscribe(Progress, { delivery: "latest" }, event => {
14
+ const completed: number = event.data.completed;
15
+ plugin.log.info(`Downloaded ${completed} bytes from ${event.source?.resource?.id}.`);
16
+ });
17
+ return { events: [Progress] };
18
+ });
19
+ export function report(object: ObjectExecutor): void {
20
+ object.events.publish(Progress, { job: "file", completed: 10, total: 100 });
21
+ }
22
+ /** Public status is registered by the host; plugins publish through their own resource handles. */
23
+ export function reportStatus(object: ObjectExecutor): void {
24
+ object.events.publish(ResourceStatusChanged, { title: "Download", status: "running" });
25
+ object.events.publish(ResourceStatusChanged, { title: "Download", status: "running", progress: { value: 0.6 } });
26
+ object.events.publish(ResourceStatusChanged, { title: "Download", status: "completed" });
27
+ }
28
+ export const renderer: RendererModule = {
29
+ resources: [], cards: [], tools: [],
30
+ async prepare({ events }) {
31
+ const subscription = events.subscribe(Progress, { delivery: "latest" }, event => {
32
+ const job: string = event.data.job;
33
+ void job;
34
+ });
35
+ await subscription.ready;
36
+ },
37
+ };
@@ -0,0 +1,18 @@
1
+ import { z } from "zod";
2
+ import { defineMessages, pluginNamespace, withValues, type UiText } from "@asterism-app/sdk/contracts";
3
+ import type { PluginContext } from "@asterism-app/sdk/runtime";
4
+ export const namespace = pluginNamespace("example.counter");
5
+ /** Public exports retain their source; ordinary strings require no catalog or initialization. */
6
+ export const messages = namespace.messages("en", defineMessages({
7
+ title: { id: "counter.title", defaultMessage: "Counter" },
8
+ items: { id: "counter.items", defaultMessage: "{count, plural, one {# item} other {# items}}", description: "Number of items in the counter" },
9
+ limit: { id: "counter.limit", defaultMessage: "Limit" },
10
+ exceeded: { id: "counter.exceeded", defaultMessage: "The limit is {limit, number}." },
11
+ }));
12
+ export const plainLabel: UiText = "Counter";
13
+ export const summary = withValues(messages.items, { count: 3 });
14
+ /** Configuration is independent of page layout; the page formats the messages above through useI18n. */
15
+ export const CounterConfig = namespace.config(z.object({ limit: z.number().int().positive() }), { limit: 20 });
16
+ export function configure(plugin: PluginContext) {
17
+ return plugin.config(CounterConfig);
18
+ }
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import { pluginNamespace, present, completionData, type ModelCompletion, type ModelRunRequest } from "@asterism-app/sdk/contracts";
3
+
4
+ const ns = pluginNamespace("example.agent");
5
+ const RequestTools = ns.data("request-tools", z.object({ names: z.array(z.string()) }));
6
+
7
+ /** Producers declare domain data; the public model service carries only typed envelopes. */
8
+ export const completionExtensions: NonNullable<ModelRunRequest["completionExtensions"]> = context => [
9
+ present(RequestTools, { names: context.tools.map(tool => tool.name) }),
10
+ ];
11
+
12
+ /** Consumers explicitly import the same contract; missing or unknown extensions do not affect public usage data. */
13
+ export function inspectCompletion(completion: ModelCompletion) {
14
+ return { usage: completion.usage, tools: completionData(completion, RequestTools)?.names ?? null };
15
+ }
@@ -0,0 +1,16 @@
1
+ import type { ModelItem } from "@asterism-app/sdk/contracts";
2
+ import type { ContentScope, ModelServices } from "@asterism-app/sdk/runtime";
3
+
4
+ /** content is already available during preparation; files need not become objects or history records before use in a one-off completion. */
5
+ export async function describeFile(models: ModelServices, content: ContentScope, bytes: Uint8Array, signal: AbortSignal) {
6
+ const file = await content.stage(bytes);
7
+ const messages: ModelItem[] = [{ kind: "input", parts: [
8
+ { kind: "text", text: "Describe the image." },
9
+ { kind: "file", file, mediaType: "image/png", filename: "screenshot.png" },
10
+ ] }];
11
+ const request = { target: { role: "utility" }, system: { blocks: [] }, messages, signal };
12
+ await models.validate(request, { content });
13
+ for await (const event of models.stream(request, { content })) {
14
+ if (event.type === "part.delta") void event.delta;
15
+ }
16
+ }
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ import { defineCall, pluginNamespace } from "@asterism-app/sdk/contracts";
3
+ import { defineModule, defineTopLevelTool, implementCall } from "@asterism-app/sdk/runtime";
4
+
5
+ const ns = pluginNamespace("example.interaction");
6
+ export const ReadState = defineCall(ns.name("read"), { scope: "object", input: z.null(), output: z.string() });
7
+ export const module = defineModule(() => {
8
+ // Share business state within the process; the frontend reads through typed calls without importing backend instances.
9
+ const values = new Map<string, string>();
10
+ return {
11
+ calls: [implementCall(ReadState, async (_input, { object }) => values.get(object.ref.id) ?? "")],
12
+ topLevelTools: [defineTopLevelTool({
13
+ id: ns.name("remember"), name: "remember", description: "Remember a short note for this agent.", guidelines: [],
14
+ argsSchema: z.strictObject({ text: z.string() }).describe("The note to remember."),
15
+ resultSchema: z.string().describe("The saved note."), effect: "write", execution: "sequential",
16
+ async execute({ text }, { object }) {
17
+ values.set(object.ref.id, text);
18
+ return { kind: "succeeded", data: text };
19
+ },
20
+ })],
21
+ };
22
+ });
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ import { pluginNamespace, type UsageItem } from "@asterism-app/sdk/contracts";
3
+ import { definePlugin, UsageQueryError, type HeadersUsageProvider } from "@asterism-app/sdk/runtime";
4
+
5
+ const ns = pluginNamespace("example.header-usage");
6
+ const balance = z.coerce.number();
7
+ const currency = z.string().regex(/^[A-Z]{3}$/u);
8
+ /** The example gateway puts account balances in inference response headers; the source and balance/quota result types are independent. */
9
+ const usage: HeadersUsageProvider = {
10
+ id: "example-header-balance", label: "Example gateway balance", source: "headers",
11
+ parseHeaders({ headers }) {
12
+ const amount = headers["x-account-balance"], unit = headers["x-account-currency"];
13
+ if (amount === undefined || unit === undefined) return null;
14
+ const value = balance.safeParse(amount), code = currency.safeParse(unit);
15
+ if (!amount.trim() || !value.success || !code.success) throw new UsageQueryError("protocol");
16
+ return [{ id: "wallet", type: "balance", scope: "account", label: "Account balance",
17
+ remaining: value.data, currency: code.data }] satisfies UsageItem[];
18
+ },
19
+ };
20
+ export default definePlugin({ namespace: ns, setup: () => ({ usageProviders: [usage] }) });
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+ import { pluginNamespace, type ModelProviderPreset, type UsageItem } from "@asterism-app/sdk/contracts";
3
+ import { definePlugin, UsageQueryError, type UsageProvider } from "@asterism-app/sdk/runtime";
4
+
5
+ const ns = pluginNamespace("example.allowance");
6
+ // Example site contract: GET /account/allowance returns the balance and remaining fraction of a five-hour window; the provider handles protocol parsing.
7
+ const response = z.object({ balance: z.number(), currency: z.string().regex(/^[A-Z]{3}$/u), remainingRatio: z.number().min(0).max(1), resetAt: z.number() });
8
+ const usage: UsageProvider = {
9
+ id: "example-plan", label: "Example plan",
10
+ async query({ connection, credential, signal }) {
11
+ if (!credential) throw new UsageQueryError("credential");
12
+ const result = await fetch(new URL("/account/allowance", connection.endpoint), {
13
+ headers: { Authorization: `Bearer ${credential}` }, signal, redirect: "error",
14
+ });
15
+ if (!result.ok) throw new UsageQueryError(result.status === 401 ? "authentication" : "server");
16
+ const parsed = response.safeParse(await result.json());
17
+ if (!parsed.success) throw new UsageQueryError("protocol");
18
+ const value = parsed.data;
19
+ return [
20
+ { id: "wallet", type: "balance", scope: "account", label: "Balance", remaining: value.balance, currency: value.currency },
21
+ { id: "five-hours", type: "quota", scope: "plan", label: "5h", value: { type: "ratio", remaining: value.remainingRatio },
22
+ window: { mode: "rolling", durationMs: 18_000_000, resetAt: value.resetAt } },
23
+ ] satisfies UsageItem[];
24
+ },
25
+ };
26
+ const preset: ModelProviderPreset = {
27
+ id: "example-plan", name: "Example plan", billing: "subscription",
28
+ connection: { protocol: "openai-responses", baseUrl: "https://example.invalid/v1", auth: "api_key", compatibility: [],
29
+ usage: usage.id, fixedFields: ["protocol", "baseUrl", "auth", "compatibility", "usage"] },
30
+ sources: ["https://example.invalid/docs"], verifiedOn: "2026-10-20",
31
+ };
32
+ export default definePlugin({ namespace: ns, setup: () => ({
33
+ usageProviders: [usage], modelProviderPresets: [{ type: "add", preset }],
34
+ }) });
@@ -0,0 +1,47 @@
1
+ import { Counter, CounterView as Card, CreateCounter, ChangeCount, CounterUpdates, counterCard } from "./counter-contract.ts";
2
+ /** Frontend author example: depends only on React, Zod, and the public SDK, without accessing window.host. */
3
+ import { createElement, useEffect, useState } from "react";
4
+ import { z } from "zod";
5
+ import { defineRendererPlugin, cardRenderer, useRendererCard, useRendererObject, type RendererModule, type RendererObjectContext } from "@asterism-app/sdk/renderer";
6
+
7
+ function CounterBody({ data }: { data: z.infer<typeof Card.schema> }) {
8
+ const { openChannel, invoke, view, session } = useRendererObject();
9
+ const [pending, setPending] = useState(false);
10
+ const card = useRendererCard();
11
+ const [live, setLive] = useState<number | null>(null);
12
+ useEffect(() => {
13
+ const channel = openChannel(CounterUpdates);
14
+ channel.onMessage(update => setLive(update.value));
15
+ channel.send({ type: "subscribe" });
16
+ return () => channel.close();
17
+ }, [openChannel]);
18
+ return createElement("div", null, createElement("button", {
19
+ disabled: !view.ready || pending,
20
+ onClick: () => {
21
+ setPending(true);
22
+ void invoke(ChangeCount, { amount: 1 }).finally(() => setPending(false));
23
+ },
24
+ }, `${data.value} · live ${live ?? "-"} · ${session.snapshot()?.records.all({ scope: "timeline" }).length ?? 0} records`),
25
+ createElement("button", { onClick: () => card.openPanel("details") }, "Details"));
26
+ }
27
+ function CounterDetails({ data }: { data: z.infer<typeof Card.schema> }) {
28
+ const card = useRendererCard();
29
+ return createElement("div", null, `Count: ${data.value}`,
30
+ createElement("button", { onClick: card.closePanel }, "Close"));
31
+ }
32
+ export const renderer = {
33
+ resources: [{ kind: Counter.kind, label: "Counter", icon: null, card: counterCard }],
34
+ cards: [cardRenderer(Card, { ...counterCard, Body: CounterBody,
35
+ panels: [{ id: "details", label: "Details", icon: null, Body: CounterDetails }] })],
36
+ tools: [],
37
+ } satisfies RendererModule;
38
+ export default defineRendererPlugin({ modules: [renderer] });
39
+
40
+ export function createCounter(object: RendererObjectContext) {
41
+ return object.session.create(CreateCounter, {}, { x: 0, y: 0 });
42
+ }
43
+
44
+ /** Frontend state retained per object: survives remounts and session switches; the host calls release when the object leaves the projection. */
45
+ export function counterNote(object: RendererObjectContext) {
46
+ return object.retained.get("example.counter/note", () => ({ text: "" }), note => { note.text = ""; });
47
+ }
@@ -0,0 +1,29 @@
1
+ import type { PluginContext, ResourceRef, ObjectReader } from "@asterism-app/sdk/runtime";
2
+ import { CreateCounter, ChangeCount } from "./counter-contract.ts";
3
+
4
+ /** Callers provide public creation input, and resource providers handle initialization; the return value contains only identity. */
5
+ export async function createCounter(plugin: PluginContext, sessionId: string, receiver: ResourceRef, signal: AbortSignal) {
6
+ const session = plugin.session(sessionId);
7
+ const resource = await session.create(CreateCounter, { title: "Counter" }, { signal, linkTo: receiver });
8
+ return resource;
9
+ }
10
+
11
+ /** References must belong to the bound session; removing a busy object fails explicitly without implicit cancellation. */
12
+ export function removeCounter(plugin: PluginContext, resource: ResourceRef) {
13
+ plugin.session(resource.sessionId).remove(resource);
14
+ }
15
+
16
+ /** Enumerating summaries neither switches the current view nor loads history. */
17
+ export function sessions(plugin: PluginContext) {
18
+ return plugin.sessions.list();
19
+ }
20
+
21
+ /** Read a fresh snapshot for each external event to follow the receiver's current direct links. */
22
+ export function linkedResources(object: ObjectReader) {
23
+ return object.session.snapshot().resources.linkedTo(object.ref);
24
+ }
25
+
26
+ /** Public actions are executed by the target resource; callers do not receive its private state or commit facade. */
27
+ export async function changeCounter(plugin: PluginContext, resource: ResourceRef, amount: number, signal: AbortSignal) {
28
+ await plugin.session(resource.sessionId).invoke(ChangeCount, resource, { amount }, { signal });
29
+ }
@@ -0,0 +1,8 @@
1
+ import { z } from "zod";
2
+ import { defineCall, pluginNamespace } from "@asterism-app/sdk/contracts";
3
+
4
+ export const settingsPlugin = pluginNamespace("example.settings");
5
+ export const Settings = settingsPlugin.config(z.object({ label: z.string().min(1) }), { label: "Example" });
6
+ export const ReadLabel = defineCall(settingsPlugin.name("read-label"), {
7
+ scope: "plugin", input: z.null(), output: z.string(),
8
+ });
@@ -0,0 +1,30 @@
1
+ import { createElement, useState } from "react";
2
+ import { defineRendererPlugin, useRendererCalls, useRendererConfiguration, useRendererSettings } from "@asterism-app/sdk/renderer";
3
+ import { ReadLabel, Settings, settingsPlugin } from "./settings-contract.ts";
4
+
5
+ function SettingsPage() {
6
+ const settings = useRendererConfiguration(Settings);
7
+ const calls = useRendererCalls();
8
+ const [label, setLabel] = useState("");
9
+ const [error, setError] = useState("");
10
+ const [busy, setBusy] = useState(false);
11
+ const run = async (work: () => Promise<void>) => {
12
+ setBusy(true);
13
+ try { await work(); setError(""); }
14
+ catch (error) { setError(String(error)); }
15
+ finally { setBusy(false); }
16
+ };
17
+ return createElement("div", null,
18
+ createElement("p", null, settings.value.label),
19
+ createElement("button", { disabled: !settings.ready || busy,
20
+ onClick: () => { void run(() => settings.save({ label: "Custom label" })); } }, "Use custom label"),
21
+ createElement("button", { disabled: busy,
22
+ onClick: () => { void run(async () => { setLabel(await calls.invoke(ReadLabel, null, {})); }); } }, "Read current label"),
23
+ createElement("p", null, label),
24
+ error ? createElement("p", { role: "alert" }, error) : null);
25
+ }
26
+ export default defineRendererPlugin({ settings: { Body: SettingsPage, locations: ["general"] } });
27
+ export function OpenSettings() {
28
+ const settings = useRendererSettings();
29
+ return createElement("button", { onClick: () => settings.open(settingsPlugin.id, "general") }, "Open settings");
30
+ }
@@ -0,0 +1,11 @@
1
+ import { definePlugin, implementCall } from "@asterism-app/sdk/runtime";
2
+ import { Settings, ReadLabel, settingsPlugin } from "./settings-contract.ts";
3
+
4
+ /** Plugins without feature modules can still maintain configuration and provide global business calls. */
5
+ export default definePlugin({
6
+ namespace: settingsPlugin,
7
+ setup(plugin) {
8
+ const settings = plugin.config(Settings);
9
+ return { calls: [implementCall(ReadLabel, async () => settings.get().label)] };
10
+ },
11
+ });
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+ import { defineStorage } from "@asterism-app/sdk/runtime";
3
+
4
+ /**
5
+ * Storage entry point: `"asterism": { "storage": "./src/storage.ts" }` in package.json points to this file.
6
+ * The host imports it separately before the plugin runtime, depending only on the format declarations themselves; each step interprets only the historical shape it receives.
7
+ */
8
+ export default defineStorage([
9
+ {
10
+ // Managed configuration: `plugin.config` handles the `{ version, data }` envelope on reads and writes, while the plugin still consumes configuration values.
11
+ kind: "json", path: ".asterism/config.json", version: 2,
12
+ migrations: {
13
+ 1: raw => {
14
+ const old = z.object({ greeting: z.string() }).parse(raw);
15
+ return { ...old, enabled: true };
16
+ },
17
+ },
18
+ },
19
+ {
20
+ // Managed database: the host writes the current `user_version` on initial creation; migrations execute synchronous SQL within a host transaction.
21
+ kind: "sqlite", path: ".asterism/db.sqlite", version: 2,
22
+ migrations: {
23
+ 1: db => db.exec("ALTER TABLE notes ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;"),
24
+ },
25
+ },
26
+ ]);
@@ -0,0 +1,47 @@
1
+ /** Tool view author example: the three states are declared in parallel, and the same component can be reused across phases. */
2
+ import { createElement } from "react";
3
+ import { z } from "zod";
4
+ import { defineData, type Json } from "@asterism-app/sdk/contracts";
5
+ import { toolRenderer, type ToolRenderProps } from "@asterism-app/sdk/renderer";
6
+
7
+ const Result = defineData("example.writer/write", z.object({ written: z.number() }));
8
+ const targetSchema = z.object({ path: z.string() });
9
+ const argsSchema = z.object({ content: z.string() });
10
+ type Props = ToolRenderProps<z.infer<typeof Result.schema>, z.infer<typeof targetSchema>, z.infer<typeof argsSchema>>;
11
+
12
+ function inputContent(args: Json): string {
13
+ return args !== null && typeof args === "object" && !Array.isArray(args) && typeof args.content === "string" ? args.content : "";
14
+ }
15
+
16
+ function Header(props: Props) {
17
+ let path: string | undefined;
18
+ if (props.phase === "input") {
19
+ const target = props.target;
20
+ if (target !== null && typeof target === "object" && !Array.isArray(target) && typeof target.path === "string") path = target.path;
21
+ } else path = props.target.path;
22
+ return createElement("span", null, path ?? "Write");
23
+ }
24
+ function Body(props: Props) {
25
+ if (props.phase === "input") {
26
+ return createElement("pre", null, inputContent(props.args).slice(-2_000));
27
+ }
28
+ return createElement("pre", null, props.result?.data?.written ?? props.args.content);
29
+ }
30
+ const view = { headerRenderer: Header, bodyRenderer: Body };
31
+ export const writerView = toolRenderer({ ...Result, targetSchema, tool: { argsSchema } }, {
32
+ label: "Write",
33
+ input: { ...view, streaming: ({ args }) => inputContent(args).length > 0 },
34
+ execution: view, result: view,
35
+ });
36
+
37
+ /** Plugins render progressive output themselves; the shell manages automatic expansion and collapse based on declarations and user settings. */
38
+ const Report = defineData("example.writer/report", z.object({ text: z.string() }));
39
+ function ReportBody({ phase, result }: ToolRenderProps<z.infer<typeof Report.schema>>) {
40
+ const text = result?.data?.text ?? "";
41
+ return createElement("pre", null, phase === "executing" ? text.slice(-2_000) : text);
42
+ }
43
+ export const reportView = toolRenderer(Report, {
44
+ label: "Report",
45
+ execution: { bodyRenderer: ReportBody, streaming: ({ result }) => !!result?.data?.text },
46
+ result: { bodyRenderer: ReportBody },
47
+ });
@@ -0,0 +1,27 @@
1
+ import { pluginNamespace, present, webResponseSchema } from "@asterism-app/sdk/contracts";
2
+ import { definePlugin, WebError, type WebProviderDefinition } from "@asterism-app/sdk/runtime";
3
+ import { z } from "zod";
4
+
5
+ const ns = pluginNamespace("example.web");
6
+ const Metadata = ns.data("metadata", z.object({ sourceId: z.string() }));
7
+ const responseSchema = z.object({ id: z.string(), title: z.string(), text: z.string() });
8
+ const provider: WebProviderDefinition = {
9
+ id: "example.web/pages", label: "Example pages", search: false, fetch: true,
10
+ urlPatterns: ["https://docs.example.test/*", "https://help.example.test/*"],
11
+ defaultBaseUrl: "https://api.example.test", credential: { kind: "api_key", keyUrl: "https://example.test/api-keys" },
12
+ create({ baseUrl, credential }) {
13
+ return { async fetch(request, { signal }) {
14
+ const response = await fetch(new URL("/read", baseUrl), {
15
+ method: "POST", signal, headers: { "Content-Type": "application/json", Authorization: `Bearer ${credential}` },
16
+ body: JSON.stringify({ url: request.url }),
17
+ });
18
+ if (!response.ok) throw new WebError("request_failed", `Page service returned HTTP ${response.status}.`);
19
+ const page = responseSchema.parse(await response.json());
20
+ return webResponseSchema.parse({ documents: [{ url: request.url, title: page.title,
21
+ content: [{ kind: "body", format: "markdown", text: page.text }], details: [present(Metadata, { sourceId: page.id })] }],
22
+ hints: request.query ? [{ code: "unsupported_parameter", parameter: "query", message: "This provider returned the page without query-focused excerpts." }] : [],
23
+ });
24
+ } };
25
+ },
26
+ };
27
+ export default definePlugin({ namespace: ns, setup: () => ({ webProviders: [provider] }) });