@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.
- package/LICENSE +9 -0
- package/README.md +20 -2
- package/docs/data-and-lifecycle.md +79 -0
- package/docs/getting-started.md +37 -0
- package/docs/manual.md +38 -0
- package/docs/minimal-plugin.md +35 -0
- package/docs/project-structure.md +33 -0
- package/docs/resources-and-tools.md +43 -0
- package/docs/session-entries.md +31 -0
- package/docs/views-and-settings.md +43 -0
- package/examples/author.ts +158 -0
- package/examples/consume-usage.ts +29 -0
- package/examples/content-author.ts +29 -0
- package/examples/content-contract.ts +10 -0
- package/examples/content-renderer.ts +26 -0
- package/examples/counter/docs/usage.md +9 -0
- package/examples/counter/package.json +27 -0
- package/examples/counter/src/contracts.ts +34 -0
- package/examples/counter/src/panel.module.css +2 -0
- package/examples/counter/src/renderer.tsx +80 -0
- package/examples/counter/src/runtime.ts +49 -0
- package/examples/counter/src/storage.ts +4 -0
- package/examples/counter/src/styles.d.ts +4 -0
- package/examples/counter/tsconfig.json +16 -0
- package/examples/counter-contract.ts +18 -0
- package/examples/data-codecs.ts +18 -0
- package/examples/events.ts +37 -0
- package/examples/i18n.ts +18 -0
- package/examples/model-completion.ts +15 -0
- package/examples/model-input.ts +16 -0
- package/examples/plugin-calls.ts +22 -0
- package/examples/provider-usage-headers.ts +20 -0
- package/examples/provider-usage.ts +34 -0
- package/examples/renderer.ts +47 -0
- package/examples/session-resources.ts +29 -0
- package/examples/settings-contract.ts +8 -0
- package/examples/settings-renderer.ts +30 -0
- package/examples/settings-runtime.ts +11 -0
- package/examples/storage.ts +26 -0
- package/examples/tool-renderer.ts +47 -0
- package/examples/web-provider.ts +27 -0
- package/package.json +39 -4
- package/src/calls.ts +28 -0
- package/src/card.ts +7 -0
- package/src/channels.ts +20 -0
- package/src/commands.ts +28 -0
- package/src/config.ts +25 -0
- package/src/content-runtime.ts +11 -0
- package/src/content.ts +6 -0
- package/src/contracts.ts +39 -0
- package/src/data-encoding.ts +14 -0
- package/src/error-details.ts +78 -0
- package/src/events.ts +91 -0
- package/src/i18n.ts +58 -0
- package/src/json.ts +4 -0
- package/src/mcp.ts +80 -0
- package/src/model-catalog.ts +18 -0
- package/src/model-cost-estimate.ts +49 -0
- package/src/model-cost.ts +44 -0
- package/src/model-input.ts +19 -0
- package/src/model-media.ts +11 -0
- package/src/model-provider.ts +44 -0
- package/src/model-records.ts +56 -0
- package/src/model-selection.ts +34 -0
- package/src/models.ts +134 -0
- package/src/module-contract.ts +44 -0
- package/src/objects.ts +89 -0
- package/src/plugin.ts +58 -0
- package/src/presentation.ts +21 -0
- package/src/provider-usage.ts +47 -0
- package/src/records.ts +60 -0
- package/src/renderer-calls.ts +15 -0
- package/src/renderer-card.ts +29 -0
- package/src/renderer-content.ts +9 -0
- package/src/renderer-creation.ts +83 -0
- package/src/renderer-i18n.ts +49 -0
- package/src/renderer-island.ts +66 -0
- package/src/renderer-records.ts +49 -0
- package/src/renderer-services.ts +89 -0
- package/src/renderer-tools.ts +103 -0
- package/src/renderer-view-state.ts +33 -0
- package/src/renderer.ts +174 -0
- package/src/runtime-agent-catalog.ts +27 -0
- package/src/runtime-application.ts +7 -0
- package/src/runtime-calls.ts +22 -0
- package/src/runtime-channels.ts +24 -0
- package/src/runtime-contracts.ts +207 -0
- package/src/runtime-mcp.ts +24 -0
- package/src/runtime-model-providers.ts +16 -0
- package/src/runtime-resources.ts +117 -0
- package/src/runtime-sessions.ts +36 -0
- package/src/runtime-shell.ts +18 -0
- package/src/runtime-terminal.ts +24 -0
- package/src/runtime-tools.ts +31 -0
- package/src/runtime-usage.ts +54 -0
- package/src/runtime-web.ts +36 -0
- package/src/runtime.ts +53 -0
- package/src/session-title.ts +8 -0
- package/src/shell.ts +76 -0
- package/src/terminal.ts +36 -0
- package/src/tool-result.ts +45 -0
- package/src/tool-validation.ts +14 -0
- package/src/usage-state.ts +20 -0
- package/src/web.ts +75 -0
- package/src/workspace.ts +43 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { creationEntry, type RendererObjectContext } from "@asterism-app/sdk/renderer";
|
|
2
|
+
import { CreateDocument, ReplaceDocument } from "./content-contract.ts";
|
|
3
|
+
|
|
4
|
+
/** This entry point accepts dropped files (with or without local paths) and clipboard images on the workbench. */
|
|
5
|
+
export const importDocument = creationEntry(CreateDocument, {
|
|
6
|
+
id: "example.content/import", label: "Import document",
|
|
7
|
+
triggers: { drop: {
|
|
8
|
+
accepts: { files: {} },
|
|
9
|
+
async toInput(item, content) {
|
|
10
|
+
if (item.kind !== "file" || item.directory) return null;
|
|
11
|
+
return { file: await content.stage(item.file), name: item.name };
|
|
12
|
+
},
|
|
13
|
+
} },
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/** The host closes the draft after a successful command; failure preserves it for retry, and abandoning it calls close. */
|
|
17
|
+
export function replacementInput(object: RendererObjectContext) {
|
|
18
|
+
const draft = object.input(ReplaceDocument);
|
|
19
|
+
return {
|
|
20
|
+
async submit(file: File) {
|
|
21
|
+
const ref = await draft.stage(file);
|
|
22
|
+
return object.invoke(ReplaceDocument, { file: ref, name: file.name }, { content: draft });
|
|
23
|
+
},
|
|
24
|
+
cancel: () => draft.close(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Use the counter plugin
|
|
2
|
+
|
|
3
|
+
The counter plugin adds a resource that stores an integer, a card for incrementing it, and a read operation for agents. It also has a settings page for choosing the increment step.
|
|
4
|
+
|
|
5
|
+
Create a Counter from the workbench creation menu. Its initial value is zero. Use Increment on the card to add the configured step. In plugin settings, change the step or read it back from the backend.
|
|
6
|
+
|
|
7
|
+
Link the counter resource to an agent to make its read operation available under that agent's tool policy. The operation reports the current value.
|
|
8
|
+
|
|
9
|
+
Incrementing saves the current value and records the change in history. Browsing an earlier history position does not restore the counter value. Removing the resource removes its current workbench presence while preserving its history.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "asterism-example-counter",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": { "typecheck": "tsc --noEmit" },
|
|
7
|
+
"peerDependencies": {
|
|
8
|
+
"@asterism-app/sdk": "*",
|
|
9
|
+
"react": "^19.3.0",
|
|
10
|
+
"react-intl": "^12.1.4",
|
|
11
|
+
"zod": "^4.6.5"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@asterism-app/sdk": "0.48.0",
|
|
15
|
+
"typescript": "^7.0.2",
|
|
16
|
+
"@types/node": "^26.6.1",
|
|
17
|
+
"@types/react": "^19.3.0"
|
|
18
|
+
},
|
|
19
|
+
"asterism": {
|
|
20
|
+
"id": "example.counter",
|
|
21
|
+
"displayName": "Example counter",
|
|
22
|
+
"runtime": "src/runtime.ts",
|
|
23
|
+
"renderer": "src/renderer.tsx",
|
|
24
|
+
"storage": "src/storage.ts",
|
|
25
|
+
"manual": ["docs"]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
export const StartSession = defineCall(ns.name("start-session"), {
|
|
22
|
+
scope: "plugin", input: z.strictObject({ key: z.string().min(1) }), output: z.string(),
|
|
23
|
+
});
|
|
24
|
+
const Read = defineTool({
|
|
25
|
+
id: ns.name("read-counter"), name: "read_counter",
|
|
26
|
+
description: "Read the current value of a counter.", guidelines: [],
|
|
27
|
+
argsSchema: z.null().describe("No arguments."),
|
|
28
|
+
});
|
|
29
|
+
export const ReadCounter = defineImplementation({
|
|
30
|
+
...ns.data("read-result", z.number().describe("The current counter value.")),
|
|
31
|
+
resource: Counter, tool: Read,
|
|
32
|
+
targetSchema: z.null().describe("The whole counter."),
|
|
33
|
+
effect: "read", execution: "parallel",
|
|
34
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
defineRendererPlugin, cardRenderer, creationEntry, toolRenderer,
|
|
4
|
+
useRendererObject, useRendererConfiguration, useRendererCalls, useI18n, type SessionEntryProps,
|
|
5
|
+
} from "@asterism-app/sdk/renderer";
|
|
6
|
+
import { ns, card, Counter, View, Create, Increment, Settings, ReadStep, ReadCounter, StartSession } 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
|
+
function CounterEntry({ ready, open }: SessionEntryProps) {
|
|
56
|
+
const calls = useRendererCalls(), retryKey = useRef<string | null>(null);
|
|
57
|
+
return <Operation label="Start a counter workspace" disabled={!ready} run={async () => {
|
|
58
|
+
retryKey.current ??= crypto.randomUUID();
|
|
59
|
+
const sessionId = await calls.invoke(StartSession, { key: retryKey.current }, {});
|
|
60
|
+
retryKey.current = null;
|
|
61
|
+
open(sessionId);
|
|
62
|
+
}} />;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default defineRendererPlugin({
|
|
66
|
+
sessionEntries: [{ id: "counter", title: "Counter workspace", Body: CounterEntry }],
|
|
67
|
+
settings: [{ id: "counter", title: "Counter", summary: "Configure the counter step.", Body: SettingsPage }],
|
|
68
|
+
modules: [{
|
|
69
|
+
resources: [{ kind: Counter.kind, label: "Counter", icon: null, card }],
|
|
70
|
+
creations: [creationEntry(Create, {
|
|
71
|
+
id: ns.name("create-menu"), label: "Counter",
|
|
72
|
+
triggers: { menu: { initialInput: null } },
|
|
73
|
+
})],
|
|
74
|
+
cards: [cardRenderer(View, { ...card, Body: CounterBody })],
|
|
75
|
+
tools: [toolRenderer(ReadCounter, {
|
|
76
|
+
label: "Read counter",
|
|
77
|
+
result: { bodyRenderer: ({ result }) => <output>{result.data ?? "—"}</output> },
|
|
78
|
+
})],
|
|
79
|
+
}],
|
|
80
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
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, StartSession,
|
|
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: [
|
|
18
|
+
implementCall(ReadStep, async () => settings.get().step),
|
|
19
|
+
implementCall(StartSession, async ({ key }, { signal }) => plugin.createSession(key, async preparation => {
|
|
20
|
+
const counter = await preparation.prepare(Create, null);
|
|
21
|
+
return { title: "Counter workspace", initialize(session) { session.create(counter); } };
|
|
22
|
+
}, { signal })),
|
|
23
|
+
],
|
|
24
|
+
resources: [implementResource(Counter, View, {
|
|
25
|
+
card, records: [Changed],
|
|
26
|
+
template: { current: [Value] },
|
|
27
|
+
creation: implementCreation(Create, {
|
|
28
|
+
create: () => ({ title: "Counter", data: {} }),
|
|
29
|
+
}),
|
|
30
|
+
actions: [implementAction(Increment, {
|
|
31
|
+
async execute(_input, object) {
|
|
32
|
+
object.commit(writer => {
|
|
33
|
+
const next = current(object) + settings.get().step;
|
|
34
|
+
writer.set(Value, next);
|
|
35
|
+
writer.append(Changed, next);
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
})],
|
|
39
|
+
project: (_data, object) => current(object),
|
|
40
|
+
inspect: async (_data, object) => current(object),
|
|
41
|
+
})],
|
|
42
|
+
tools: [implementTool(ReadCounter, {
|
|
43
|
+
async call(_data, _target, _args, { object }) {
|
|
44
|
+
return { kind: "succeeded", data: current(object) };
|
|
45
|
+
},
|
|
46
|
+
})],
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -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
|
+
};
|
package/examples/i18n.ts
ADDED
|
@@ -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: { provider: "configured-provider", model: "configured-model" }, 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: [{ id: "general", title: "Labels", summary: "Configure shared labels.", Body: SettingsPage }] });
|
|
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
|
+
]);
|