@asterism-app/sdk 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +20 -2
  3. package/docs/data-and-lifecycle.md +79 -0
  4. package/docs/getting-started.md +37 -0
  5. package/docs/manual.md +38 -0
  6. package/docs/minimal-plugin.md +35 -0
  7. package/docs/project-structure.md +33 -0
  8. package/docs/resources-and-tools.md +43 -0
  9. package/docs/session-entries.md +31 -0
  10. package/docs/views-and-settings.md +43 -0
  11. package/examples/author.ts +158 -0
  12. package/examples/consume-usage.ts +29 -0
  13. package/examples/content-author.ts +29 -0
  14. package/examples/content-contract.ts +10 -0
  15. package/examples/content-renderer.ts +26 -0
  16. package/examples/counter/docs/usage.md +9 -0
  17. package/examples/counter/package.json +27 -0
  18. package/examples/counter/src/contracts.ts +34 -0
  19. package/examples/counter/src/panel.module.css +2 -0
  20. package/examples/counter/src/renderer.tsx +80 -0
  21. package/examples/counter/src/runtime.ts +49 -0
  22. package/examples/counter/src/storage.ts +4 -0
  23. package/examples/counter/src/styles.d.ts +4 -0
  24. package/examples/counter/tsconfig.json +16 -0
  25. package/examples/counter-contract.ts +18 -0
  26. package/examples/data-codecs.ts +18 -0
  27. package/examples/events.ts +37 -0
  28. package/examples/i18n.ts +18 -0
  29. package/examples/model-completion.ts +15 -0
  30. package/examples/model-input.ts +16 -0
  31. package/examples/plugin-calls.ts +22 -0
  32. package/examples/provider-usage-headers.ts +20 -0
  33. package/examples/provider-usage.ts +34 -0
  34. package/examples/renderer.ts +47 -0
  35. package/examples/session-resources.ts +29 -0
  36. package/examples/settings-contract.ts +8 -0
  37. package/examples/settings-renderer.ts +30 -0
  38. package/examples/settings-runtime.ts +11 -0
  39. package/examples/storage.ts +26 -0
  40. package/examples/tool-renderer.ts +47 -0
  41. package/examples/web-provider.ts +27 -0
  42. package/package.json +39 -4
  43. package/src/calls.ts +28 -0
  44. package/src/card.ts +7 -0
  45. package/src/channels.ts +20 -0
  46. package/src/commands.ts +28 -0
  47. package/src/config.ts +25 -0
  48. package/src/content-runtime.ts +11 -0
  49. package/src/content.ts +6 -0
  50. package/src/contracts.ts +39 -0
  51. package/src/data-encoding.ts +14 -0
  52. package/src/error-details.ts +78 -0
  53. package/src/events.ts +91 -0
  54. package/src/i18n.ts +58 -0
  55. package/src/json.ts +4 -0
  56. package/src/mcp.ts +80 -0
  57. package/src/model-catalog.ts +18 -0
  58. package/src/model-cost-estimate.ts +49 -0
  59. package/src/model-cost.ts +44 -0
  60. package/src/model-input.ts +19 -0
  61. package/src/model-media.ts +11 -0
  62. package/src/model-provider.ts +44 -0
  63. package/src/model-records.ts +56 -0
  64. package/src/model-selection.ts +34 -0
  65. package/src/models.ts +134 -0
  66. package/src/module-contract.ts +44 -0
  67. package/src/objects.ts +89 -0
  68. package/src/plugin.ts +58 -0
  69. package/src/presentation.ts +21 -0
  70. package/src/provider-usage.ts +47 -0
  71. package/src/records.ts +60 -0
  72. package/src/renderer-calls.ts +15 -0
  73. package/src/renderer-card.ts +29 -0
  74. package/src/renderer-content.ts +9 -0
  75. package/src/renderer-creation.ts +83 -0
  76. package/src/renderer-i18n.ts +49 -0
  77. package/src/renderer-island.ts +66 -0
  78. package/src/renderer-records.ts +49 -0
  79. package/src/renderer-services.ts +89 -0
  80. package/src/renderer-tools.ts +103 -0
  81. package/src/renderer-view-state.ts +33 -0
  82. package/src/renderer.ts +174 -0
  83. package/src/runtime-agent-catalog.ts +27 -0
  84. package/src/runtime-application.ts +7 -0
  85. package/src/runtime-calls.ts +22 -0
  86. package/src/runtime-channels.ts +24 -0
  87. package/src/runtime-contracts.ts +207 -0
  88. package/src/runtime-mcp.ts +24 -0
  89. package/src/runtime-model-providers.ts +16 -0
  90. package/src/runtime-resources.ts +117 -0
  91. package/src/runtime-sessions.ts +36 -0
  92. package/src/runtime-shell.ts +18 -0
  93. package/src/runtime-terminal.ts +24 -0
  94. package/src/runtime-tools.ts +31 -0
  95. package/src/runtime-usage.ts +54 -0
  96. package/src/runtime-web.ts +36 -0
  97. package/src/runtime.ts +53 -0
  98. package/src/session-title.ts +8 -0
  99. package/src/shell.ts +76 -0
  100. package/src/terminal.ts +36 -0
  101. package/src/tool-result.ts +45 -0
  102. package/src/tool-validation.ts +14 -0
  103. package/src/usage-state.ts +20 -0
  104. package/src/web.ts +75 -0
  105. package/src/workspace.ts +43 -0
package/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chien Zhang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,21 @@
1
- # Temporary Holding Version
1
+ # Asterism SDK
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The SDK defines the shared language between Asterism and plugins: contracts, host capabilities, and contributions.
4
+
5
+ Pure declarations can be shared across execution environments. Backend interfaces support work and persistence; frontend interfaces support presentation and interaction.
6
+
7
+ The SDK describes generic mechanisms. Agent conversation, naming, and other domain behavior belong to the plugin that implements them.
8
+
9
+ Public interfaces should express a capability an author needs. Internal decomposition is not itself a reason to expose another abstraction.
10
+
11
+ ## Installation
12
+
13
+ Install with `npm install --save-dev --save-exact @asterism-app/sdk@latest` to target the stable application, or `@next` to target the preview application. Declare `@asterism-app/sdk` as a peer dependency with `"*"` in the plugin package.
14
+
15
+ The package ships TypeScript source. Asterism compiles plugin entries and supplies its own SDK instance at runtime. Use declarations compatible with the target application; installing a newer SDK does not update the application's APIs.
16
+
17
+ ## Authoring material
18
+
19
+ The source package includes `docs/` for plugin development guides and `examples/` for public API usage. `examples/counter/` is a complete starter package; the other examples are focused fragments to compose into a plugin. The application's manual exposes these directories directly.
20
+
21
+ Examples participate in repository type checking. Internal compile-time counterexamples live in `typechecks/` and are excluded from package contents and the manual. Built-in domain guides and examples belong to `@asterism-app/builtin-api`.
@@ -0,0 +1,79 @@
1
+ # Manage data and lifecycle
2
+
3
+ Choose storage from the meaning of the data. Latest values, historical facts, immutable bytes, and running processes have different lifetimes.
4
+
5
+ ## Separate current state from history
6
+
7
+ Current data records the latest session or object value. Historical records preserve facts needed to understand or reconstruct past work.
8
+
9
+ Saving a current value does not automatically create history. Moving to an earlier history position does not restore that value. A resource that supports restoration defines the facts and operations that make restoration meaningful.
10
+
11
+ Plugin-wide private data belongs to the plugin. Temporary interface choices belong to a view or an object-retained view scope, according to how long they should survive.
12
+
13
+ ## Encode data at boundaries
14
+
15
+ Data schemas describe the values used by plugin code and how they are decoded from stored or transported JSON. Writers, resource creation, tool results, and presentations accept the decoded value and encode it before storage or publication. Typed readers and views decode the saved value.
16
+
17
+ Use an ordinary Zod schema when both representations are the same. Use a synchronous `z.codec` when they differ. Both the domain value and its encoding remain JSON-compatible. One-way `.transform()` belongs in input preparation or an explicit conversion before writing; Zod cannot encode it in reverse.
18
+
19
+ Resource creation returns a card title and plugin-owned `data`. State can be any JSON value, including arrays, scalars, or `null`; object fields such as `id`, `type`, and `title` retain their business meaning. The host stores resource identity and card title separately. Renaming a card changes its host title, and creating resources from a template gives them new host identities while preserving the encoded plugin state.
20
+
21
+ Configuration defaults are complete decoded values. Reading an older configuration may fill absent fields through ordinary `.default()` declarations. Configuration updates receive decoded values, and saving an unchanged value preserves a correctly defined codec's domain value. The host handles encoding of configuration files and frontend responses.
22
+
23
+ Codecs own their conversion semantics. Verify meaningful round trips when supplying custom encoders, decoders, or normalization callbacks. Tool results reserve `null` for absent output, so a non-null tool value needs a non-null encoding.
24
+
25
+ ## Reuse resources in templates
26
+
27
+ A resource opts into data-copy templates with `template: { current: [Settings, Content] }` in its implementation. The list contains existing data contracts owned by the resource's plugin. An empty list copies only the resource's creation state. Resources without this declaration remain available for ordinary use and do not appear in the template picker.
28
+
29
+ Templates retain encoded creation state and the listed current values that exist at save time. Unlisted current data, history, private files, and running instances are excluded. Instantiation creates new host identities and remaps host links; it preserves domain values and content references without rewriting embedded resource IDs or duplicating external services. Creation callbacks are not rerun, and ordinary presence lifetimes apply to the new objects.
30
+
31
+ To give each template instance a new title, add a synchronous `createTitle(): string` function to the resource's `template` declaration. Saving then omits the source title, including a user-assigned title. The host calls the function once for each new object when instantiating a template, and uses its result even if an older template contains a title. Saving and previewing templates do not call it. Without this function, templates preserve source titles; a saved entry without a title requires the generator to remain available.
32
+
33
+ Opt in when those copied values are reusable under new identities. For example, a note can retain its body, and an agent can retain its configuration while leaving submission receipts and drafts behind. Resources needing external cloning, reference mapping, or private-state reconstruction need an explicit domain operation. Historical observation does not perform restoration.
34
+
35
+ The installed resource must still support templates and decode all saved state and selected current data when the template is used. Unsupported or invalid saved entries fail explicitly before a new session is created.
36
+
37
+ ## Commit and observe
38
+
39
+ Use the host's bound read and execution capabilities. Complete network and file processing outside synchronous transactions, then commit the resulting state.
40
+
41
+ A historical snapshot fixes what is observed. A later write acts in the current execution context. Holding a snapshot does not redirect later writes into the past.
42
+
43
+ Transient events announce change. A consumer needing current state establishes its subscription and reads the current value; later notifications prompt another read.
44
+
45
+ Typed calls describe request and response values with ordinary Zod schemas. The caller supplies the input schema's input; the implementation receives its parsed output, including defaults. The implementation returns JSON accepted by the output schema, and the caller receives that schema's parsed output. These types are inferred from the shared declaration, so a conversion such as string to number does not require separate type annotations.
46
+
47
+ ## Retain file content
48
+
49
+ Stage immutable bytes through the content service, then store their reference in persistent data or a record that owns them.
50
+
51
+ A reference identifies content. Copying its identifier does not by itself grant access or retain the bytes. Pass it through the supported operation scope so ownership remains explicit.
52
+
53
+ Retention follows references in the JSON actually saved. Preserve content references in that encoding when their bytes must remain available; synthesizing a reference during decoding does not retain content or grant access.
54
+
55
+ ## End asynchronous work
56
+
57
+ Bind shared services to the plugin lifetime and object-owned instances to the resource lifetime. Register cleanup when creating the owned work.
58
+
59
+ Cancellation requests that work stop. Wait for actual completion before releasing what that work still uses.
60
+
61
+ Timeline-dependent operations need the host's activity tracking so history changes and object removal can respect active work.
62
+
63
+ ## Evolve stored data
64
+
65
+ Persistent file formats need explicit preparation when they change. Keep that preparation independent of ordinary service startup.
66
+
67
+ Historical records retain their original payloads. Supporting an older shape through decoding is different from rewriting the original historical fact.
68
+
69
+ A codec can accept old and current payload shapes while encoding new writes in the current shape:
70
+
71
+ ```ts
72
+ const current = z.strictObject({ text: z.string() });
73
+ const textSchema = z.codec(z.union([z.string(), current]), current, {
74
+ decode: value => typeof value === "string" ? { text: value } : value,
75
+ encode: value => value,
76
+ });
77
+ ```
78
+
79
+ This schema reads an old string as a current object. Plugin code writes the object; historical reads leave the original string untouched.
@@ -0,0 +1,37 @@
1
+ # Prepare for plugin development
2
+
3
+ A plugin adds resources, tools, views, settings pages, Home entries, or services through Asterism's public software development kit (SDK). Development includes preparing a compatible package, verifying its capabilities and lifecycle in the application, and checking the packaged result before sharing it.
4
+
5
+ ## Prepare a project
6
+
7
+ Use a TypeScript package and the public SDK compatible with the application you intend to run. A package declaration identifies the plugin and its backend, frontend, and optional storage entries.
8
+
9
+ Set the plugin's release version in `package.json.version`. That version identifies distributed releases and updates; it does not affect loading or execution. The runtime definition declares its namespace and behavior. Keep ordinary runtime libraries in `dependencies`; npm or another package manager installs them using its usual dependency and lockfile rules.
10
+
11
+ Declare host-provided packages such as `@asterism-app/sdk` in `peerDependencies` with `"*"`. The running plugin always uses the host's instance, so a narrower range adds maintenance without changing behavior. Install a version of the SDK compatible with the target application in `devDependencies` for editor support and type checking. Extensions to built-in features can use the same arrangement for `@asterism-app/builtin-api`.
12
+
13
+ npm distribution tags follow the application's release channels: `latest` matches the newest stable application and `next` matches the newest preview application. Install the tag for the application you target, for example `npm install --save-dev --save-exact @asterism-app/sdk@latest`; add `@asterism-app/builtin-api` with the same tag when using built-in domain APIs. Saving an exact version keeps the declarations stable until an intentional update. The packages include TypeScript source, guides, and examples. Installing a newer package does not update the APIs provided by the running application.
14
+
15
+ Asterism binds shared public imports to the active host or plugin provider. Dependency ranges do not reject loading; missing modules, missing exports, invalid contributions, and initialization errors produce loading failures. Check the APIs your plugin actually uses with TypeScript and exercise them in the target application.
16
+
17
+ The `asterism` field declares a namespaced `id` and the needed `runtime`, `renderer`, and `storage` entry paths, relative to the package root. Runtime and renderer entries default-export their SDK plugin definitions. The runtime namespace must match the manifest ID. The required `displayName` is the plugin's name wherever the application shows it, including plugin management, settings attribution, and session entries; use a message reference owned by the plugin to localize it. The host reads it from the package without running plugin code, so disabled and failed plugins keep their names.
18
+
19
+ Keep shared declarations separate from executable backend and frontend code. Backend work can use local system capabilities; frontend code works through the host's view and communication facilities.
20
+
21
+ Start with one capability that demonstrates the plugin's purpose. Add further parts when that capability needs them rather than creating an empty framework in advance.
22
+
23
+ ## Load and verify
24
+
25
+ Prepare the package and its dependencies, run its type checks, then add its local package directory in Asterism's plugin settings. Restart to load changes.
26
+
27
+ The host compiles TypeScript, TSX, and imported CSS at startup. A separate JavaScript build is optional. Generate CSS with the plugin's own toolchain first when using Tailwind, Sass, or another preprocessor. Source changes take effect after restart.
28
+
29
+ Check the reported loading state and exercise the capability. Verify its normal result, meaningful errors, and cleanup when an object disappears or the application closes.
30
+
31
+ Type checking establishes interface consistency. Actual loading and use establish whether the package works in the intended host.
32
+
33
+ ## Prepare for sharing
34
+
35
+ Include the entries, assets, and dependencies the package needs. Preserve imports of shared public contracts so the host and consumers use the same declarations.
36
+
37
+ Test the resulting package in an installed application. Development files available on the author's machine should not be necessary to use the distributed plugin.
package/docs/manual.md ADDED
@@ -0,0 +1,38 @@
1
+ # Expose plugin documentation
2
+
3
+ A plugin can make its documentation and examples available through Asterism's Manual resource. The manual exposes the declared directories directly. The same files remain usable in an editor or another development environment.
4
+
5
+ ## Declare directories
6
+
7
+ Add a `manual` array to the plugin's existing `asterism` manifest:
8
+
9
+ ```json
10
+ {
11
+ "asterism": {
12
+ "id": "example.bridge",
13
+ "manual": ["docs", "examples"]
14
+ }
15
+ }
16
+ ```
17
+
18
+ Each entry names an existing package-relative subdirectory using `/` separators. Entries must be normalized, must not overlap, and must resolve inside the package. Include those directories in the package's distributed files. A documentation declaration does not require a runtime entry.
19
+
20
+ Declare only directories intended for readers. Source examples can be exposed alongside prose; internal test fixtures and credentials belong outside the declared directories.
21
+
22
+ A public API library named in the plugin's `asterism.publicPackages` can declare its own `asterism.manual` directories. The library needs no plugin identity or executable entry for this declaration. Its documentation remains inside its own package and is available while its implementation provider is loaded.
23
+
24
+ ## Read through the manual
25
+
26
+ When the plugin loads successfully, its directories appear under `plugins/<plugin-id>/`. For this example, an article at `docs/usage.md` appears as `plugins/example.bridge/docs/usage.md`. The manual supports directory listing, file discovery, content search, and reading selected lines.
27
+
28
+ Public-library directories appear under `plugins/<plugin-id>/public/<package-name>/`. The host exposes the same source catalog through the SDK for plugins that provide another documentation interface.
29
+
30
+ The manual reads current files at their original locations. It creates no copied document tree or index. Source declarations follow the loaded package set: changes to declarations, enabling, disabling, or removing a plugin take effect after restart. Plugins with package, startup, or storage failures do not contribute documents.
31
+
32
+ A search at a virtual parent covers the directories below it. Globs apply within each selected physical search directory. Results include the complete virtual path, so identical filenames in different plugins remain distinguishable. Reading a file or following an explicitly selected symbolic link remains within the declared directory.
33
+
34
+ ## Write independent articles
35
+
36
+ Each article should explain its purpose, relevant concepts, prerequisites, and working method without depending on a reading sequence. Keep the context needed to understand the topic in the article itself. Use examples to clarify operations and leave complete interface definitions to the public types.
37
+
38
+ Maintain documentation with the domain that owns its meaning. Generic host capabilities belong to the SDK; a plugin's resource behavior and domain extensions belong to that plugin or its public API package. Documentation is reference material and does not become a system instruction merely by appearing in the manual.
@@ -0,0 +1,35 @@
1
+ # Build a small plugin
2
+
3
+ A counter plugin demonstrates the complete path from shared declarations to backend behavior and a resource card. The SDK includes it as an ordinary package directory under `examples/counter`, with source files, a package manifest, and a TypeScript configuration. Its code is the maintained example; this guide explains how to use and adapt it.
4
+
5
+ ## Prepare the package
6
+
7
+ Copy the counter directory to a writable development location. Its development dependency selects the SDK version used by the example. Use an Asterism application that provides those APIs. The SDK peer dependency describes a host-provided package, and the host does not use its version range to gate loading. React, React Intl, and Zod are also shared public dependencies supplied by the host when the plugin runs.
8
+
9
+ Run `npm install` and `npm run typecheck` in the copied directory. The host compiles the TypeScript, TSX, and imported CSS entries, so a separate JavaScript build is optional. Add the package directory in Asterism's plugin settings and restart the application to load it.
10
+
11
+ ## Follow the data
12
+
13
+ Shared declarations identify the counter, its private current value, projected view, change record, creation entry, increment action, read tool, settings, and typed call. Backend and frontend code import the same declarations. Copying a declaration under the same name does not establish shared runtime identity.
14
+
15
+ Creation establishes the object. Incrementing reads the configured step, saves the current value, and appends a historical change in one synchronous commit. The example keeps the latest counter value when browsing history; it does not implement restoration. Network and file work belongs outside synchronous commits.
16
+
17
+ The card reads projected data and invokes the public increment action. Each button owns its pending state and displays failures. Actions return receipts that the view checks; typed calls and settings saves reject on failure. A settings page edits the step and reads it back through the backend call.
18
+
19
+ The example also contributes a Home entry. It prepares a counter, then initializes a session containing it in one transaction. This entry works with the builtin paradigm disabled and requires no conversation input.
20
+
21
+ The model tool reads the same counter value through its resource implementation. Registering the tool does not grant access: an agent needs a link to the counter and a tool policy that allows the operation.
22
+
23
+ ## Adapt the package
24
+
25
+ Choose a distinct plugin identity before using the example as a new plugin. Keep shared declarations independent of backend and frontend implementations. Add resource behavior through actions and expose an operation as a model tool when the model needs to call it.
26
+
27
+ Imported CSS Modules keep the counter's class names local. Host theme variables provide colors. The storage entry declares the managed configuration format; add consecutive migrations when that format changes. Credentials belong in the secret service, and retained file content uses the content service.
28
+
29
+ Object-owned external connections belong to the resource's attachment lifetime. Register their cleanup when opening them. Shared services belong to the plugin lifetime. Managed tasks own asynchronous work and pass cancellation to their I/O.
30
+
31
+ ## Verify the result
32
+
33
+ Open the plugin settings page, change the step, and read it back. Create a counter, increment it, and verify the card and history. Link the counter to an agent and use its read tool.
34
+
35
+ After edits, run type checking and restart Asterism. Verify invalid input, failure messages, cleanup, and reopening saved objects. Type checking establishes interface consistency; loading and using the package establishes its behavior in the host.
@@ -0,0 +1,33 @@
1
+ # Organize a plugin
2
+
3
+ A plugin is the unit of installation, identity, settings, and lifetime. Its internal modules group capabilities that share that ownership.
4
+
5
+ ## Separate responsibilities
6
+
7
+ Shared contracts describe transferable data and operations. Backend implementations perform work. Frontend views present state and collect input.
8
+
9
+ Keep shared declarations independent of backend-only or frontend-only libraries. A view reaches its backend through typed communication rather than importing the backend implementation.
10
+
11
+ Group a capability's declarations, implementation, and presentation so they remain understandable together. Compose capabilities at the plugin entry through explicit dependencies.
12
+
13
+ ## Use public extension points
14
+
15
+ The general SDK supplies host services and contribution interfaces. Built-in domain APIs expose extensions specific to existing features, such as Agent interaction.
16
+
17
+ Use a domain extension when adding to that feature. A plugin with its own resource can define its own behavior and presentation.
18
+
19
+ A paradigm plugin can own a Home entry, configuration, request assembly, and execution behavior. The host supplies sessions, concrete model access, interactive terminals, command execution, MCP, provider usage, and raw skill and instruction catalogs. Compose these capabilities according to the workflow; an Agent resource or conversation card is optional.
20
+
21
+ ## Share contracts and services
22
+
23
+ A public package can expose declarations or services for other plugins. Consumers declare ordinary package dependencies in `package.json` and import the original public values. The host derives initialization and cleanup dependencies from those imports, including imports through intermediate packages.
24
+
25
+ Expose shared values through `./public` in the package's standard exports. Public imports bind to the enabled provider's actual exports, including imports through intermediate npm packages. The host uses the import graph for initialization order and reports missing exports or unavailable providers. Package versions and dependency ranges remain package-management metadata; local development copies do not replace the active provider.
26
+
27
+ For a separate API library, declare its package name in the implementation plugin's `asterism.publicPackages` array. Install that library as an ordinary dependency and declare its public entry points through standard package exports. Consumers resolve those entries from the enabled provider's installation. Package names cannot be claimed by competing enabled providers across these two declaration forms.
28
+
29
+ Libraries that depend on native binaries or files relative to their installed module can be listed in `asterism.runtimeExternals`. The host preserves their resolved installed locations instead of relocating their code into the plugin cache.
30
+
31
+ A shared declaration has runtime identity as well as a name. Copying it under the same name does not establish shared identity. Backend and frontend environments are separate even when they use the same contract.
32
+
33
+ Shared service initialization and cleanup belong to the provider. A consumer should depend on the capability it needs rather than the provider's internal organization.
@@ -0,0 +1,43 @@
1
+ # Add resources and tools
2
+
3
+ A resource represents an object with identity and lifetime. A tool represents a model-callable operation. They can evolve independently.
4
+
5
+ ## Add an object
6
+
7
+ Use a resource for something that belongs in a session and can be inspected or operated on. Define how it is created, what state it owns, and what information it presents.
8
+
9
+ The resource's lifetime follows its presence in the session. A hidden card does not imply that its backend instance should stop.
10
+
11
+ Creation supplies initial state. Actions change the object through typed application or plugin operations. Views present the resulting state.
12
+
13
+ Resource titles belong to session history. Use the session's rename operation with a resource reference to change a title; the host appends the event and reconstructs names along each history path. Tool sessions accept a resource ID within the request's captured links. Existing snapshots keep their captured titles.
14
+
15
+ Declare `links: { receive: true }` in the backend resource implementation when the object can receive directed links. The host supplies this capability to the workbench's connection controls and authorization panel. Renderer resource descriptions supply labels, icons, and card geometry; link reception needs no frontend declaration.
16
+
17
+ Use a session snapshot to query the resources directly linked to a receiver. Results contain resource references, titles, and public data on that snapshot's timeline path. Reverse links and links through another resource are independent. For external event handling, such as forwarding network messages to linked agents, read a fresh snapshot for each event. Session observers can also query their supplied snapshot after commits or cursor changes. The query exposes relationships without granting access to private state or expanding a model request's operation scope.
18
+
19
+ ## Add a model operation
20
+
21
+ Use a resource tool when an operation targets a linked object. A shared tool can have implementations for different resource types, each with an appropriate internal target and result.
22
+
23
+ Use a top-level tool for operations on the execution context or shared resource lifecycle, such as creation and renaming. The host supplies caller identity and execution scope.
24
+
25
+ Describe the operation's purpose and observable behavior. Parameters should explain business meaning that names and types alone do not express. Results should distinguish known success, failure, cancellation, partial output, and uncertain effects.
26
+
27
+ A tool contribution can capture resource-specific configuration and access at the start of each model request. Return a bound contribution using the same implementation contract; the host uses it for that request's tool batch. This is useful for a resource whose selected network or reachable objects can change while a model is responding. Capture addressing and authorization facts, while reading ordinary mutable content when the operation runs.
28
+
29
+ ## Connect other interactions
30
+
31
+ Typed calls connect request and response between services or views. Events announce transient changes. Object channels support sustained data such as an interactive screen.
32
+
33
+ An action or service call is not automatically a model tool. Expose it deliberately when the model needs that operation.
34
+
35
+ ## Use shell capabilities
36
+
37
+ Backend plugins access shell profiles, one-shot execution, and interactive terminals through `plugin.shell`. Profiles are shared host configuration: `profiles.get()` reads that configuration, while `profiles.list()` resolves executable availability. Plugins choose how to expose these capabilities through their resources and tools.
38
+
39
+ Use `shell.execute(cwd, args, options)` for a noninteractive command with piped output, progress, and a final result. Each execution owns its process; cancellation or timeout stops that execution without undoing completed effects.
40
+
41
+ Use `shell.terminals.create(config, events)` for a persistent interactive session. Start the returned handle when needed and register its disposal with the owning resource or plugin lifetime. Closing a terminal permits reopening the handle; disposing it releases it permanently. Input and readiness waits are separate operations, and cancelling a wait leaves the shell running.
42
+
43
+ A terminal can serve several views through its typed input/output messages. Plugins connect viewers through object channels and choose their own presentation. Backend reads expose screen text, retained output, or character-cell image data. The host owns the PTY and screen state; a terminal handle does not create a workbench resource or model tool by itself.
@@ -0,0 +1,31 @@
1
+ # Provide a session entry
2
+
3
+ A session contains a history tree and a workbench. A plugin can start work with any supported resources, or with an empty workbench. An initial message and an Agent resource are optional domain choices.
4
+
5
+ ## Own the starting interface
6
+
7
+ Declare a session entry in the renderer plugin. Its body owns the controls, draft, pending state, and error presentation. The host supplies readiness and an operation that opens the resulting session.
8
+
9
+ Home displays a single available entry directly. Users choose among several entries in General settings. An explicit selection that becomes unavailable stays unresolved until the user chooses another entry. Resource templates remain available independently of the selected entry.
10
+
11
+ ## Prepare and initialize
12
+
13
+ Use a typed plugin call for submission. Its backend implementation performs business validation and calls `plugin.createSession` with a retry key. Prepare each required resource through the supplied preparation context, using the original registered creation contract.
14
+
15
+ Preparation can perform asynchronous work. It returns an initial title and a synchronous initializer. The initializer creates prepared resources and writes initial records or current data through authorized object handles. Session identity, resources, initial writes, and the retry receipt commit together. A failed preparation or initializer leaves no partial session.
16
+
17
+ When initial work uses an existing resource action contract, prepare that action against the resource token before initialization. This applies its parsing and registered preparers to the initial input as well. The pending resource has its final identity, but no persisted state yet. The owning plugin validates its initial business state and uses the prepared input in the initializer.
18
+
19
+ Reuse the key for a retry of the same accepted submission. Keys belong to the contributing plugin. A successful retry returns the existing session without repeating preparation or initialization. A new piece of work uses a new key.
20
+
21
+ ## Carry attachments
22
+
23
+ Obtain a renderer content input for the submission call. Pass that input as the call's content option. The backend receives a content reader separately from business input and forwards it to session creation.
24
+
25
+ Content IDs alone grant no access. Only authorized referenced bytes enter preparation and initial writes. Persisted references retain those bytes after the temporary draft closes.
26
+
27
+ ## Name the work
28
+
29
+ The entry supplies the initial title. Later user or plugin title assignments append host history. Continuing from an older position restores that path's latest title, or the initial title when the path has no assignment.
30
+
31
+ A paradigm can attach its own typed detail to a title assignment and implement automatic naming policy. The host preserves that detail and provides generic title operations.
@@ -0,0 +1,43 @@
1
+ # Build views and settings
2
+
3
+ Views present state and accept input. Backend services own the work, results, and resource lifetime.
4
+
5
+ ## Present objects and operations
6
+
7
+ A resource card shows the object's content. A tool view shows one operation, including generated input, execution progress, and the final outcome.
8
+
9
+ Input previews can be incomplete. Displaying arguments does not establish that execution has begun. A final result should reflect what actually happened.
10
+
11
+ Use host presentation facilities for the surrounding card or operation. Keep domain content within the view rather than rebuilding application navigation and containment.
12
+
13
+ ## Style plugin views
14
+
15
+ Import CSS from the renderer entry or its components. Asterism builds the imported styles and loads them before frontend preparation and view registration. CSS references to bundled images and fonts keep their relative paths. Style loading failures appear as frontend plugin failures.
16
+
17
+ Use CSS Modules (`.module.css`) for component-local class names, importing the generated names into the component. Plain CSS shares the application's document, so global selectors and resets affect other views. Host theme variables can supply colors and spacing without redefining the application theme.
18
+
19
+ Tailwind and other preprocessors belong to the plugin's build setup. Generate ordinary CSS and import that output; the host does not process the plugin's Tailwind or PostCSS configuration. Utility classes used only by an external plugin need its own generated CSS.
20
+
21
+ Styles remain loaded while the plugin frontend is active, including when individual cards unmount. Failed initialization releases that plugin's stylesheet; ending the frontend lifetime releases all loaded plugin styles.
22
+
23
+ ## Add settings
24
+
25
+ A plugin can declare several settings pages independently of sessions and resources. Each has its own identity, title, summary, optional icon, and body. The icon is a component the host draws at its own size and color; the host draws the card, page heading, and attribution, and the plugin supplies only the body. Entries appear on settings home with the contributing plugin's display name. Plugin management is a separate list for installation and enablement.
26
+
27
+ The page owns its edit drafts and pending interactions; the plugin service owns shared configuration. Navigation targets a plugin and page, with an optional declared location inside the page. A page failure is isolated and can be retried without disabling other settings pages.
28
+
29
+ Managed configuration suits schema-validated settings. Other plugin data can use a storage model appropriate to its purpose.
30
+
31
+ Closing a page releases its view subscriptions. It does not end every service the plugin uses.
32
+
33
+ ## Start work from Home
34
+
35
+ A session entry supplies the complete starting interface for a plugin's workflow. It owns drafts, validation feedback, submission, and the initial title. Submit through a typed plugin call, prepare resources in the backend, and initialize the session atomically. Open the resulting session using the entry's host-provided callback.
36
+
37
+ Keep a retry identity until submission succeeds. Attachments travel through the call's content capability independently of its JSON input. The host handles entry selection and retains access to templates when no entry is available.
38
+
39
+ ## Write interface text
40
+
41
+ Plain text is valid. Message declarations and translation catalogs support plugins that need localized interfaces.
42
+
43
+ Keep protocol identifiers, code, and raw diagnostics distinct from interface explanations. UI language changes should not reset drafts or running work.
@@ -0,0 +1,158 @@
1
+ import { sample, Counter, Count, CounterView, CountChanged, CreateCounter, ChangeCount, CounterUpdates, counterCard } from "./counter-contract.ts";
2
+ /** Independent examples of session records, resource behavior, and model execution. */
3
+ import { z } from "zod";
4
+ import { pluginNamespace, defineAction, resourceCall, defineRecord, defineTool, defineImplementation, modelRunRecordSchema, type ModelItem } from "@asterism-app/sdk/contracts";
5
+ import {
6
+ defineModule, definePlugin, implementCreation, implementAction, implementPreparer, implementResource, implementTool, implementChannel,
7
+ type PluginContext, type ObjectReader, type ObjectExecutor, type SessionObserver, type SessionSnapshot, type ResourceRef,
8
+ } from "@asterism-app/sdk/runtime";
9
+
10
+ /** Scenario one: uses only session events and lifecycles, without resources, models, or Agents. */
11
+ const journal = pluginNamespace("example.journal");
12
+ const Annotation = defineRecord(journal.name("annotation"), {
13
+ schema: z.strictObject({ text: z.string() }),
14
+ });
15
+ export const journalPlugin = definePlugin({
16
+ namespace: journal, modules: [defineModule(plugin => ({
17
+ records: [Annotation],
18
+ observers: [{
19
+ id: "observe-annotations",
20
+ committed({ records }) {
21
+ for (const record of records) {
22
+ const entry = Annotation.decode(record);
23
+ if (entry?.status === "known") plugin.log.info("Annotation committed.");
24
+ }
25
+ },
26
+ cursorChanged({ snapshot, session }) {
27
+ const annotation = snapshot.records.last(Annotation, { scope: "timeline" });
28
+ if (annotation?.status !== "known") return;
29
+ session.commit(writer => { writer.set(CurrentSummary, { text: annotation.data.text }); });
30
+ },
31
+ } satisfies SessionObserver],
32
+ }))],
33
+ });
34
+ export function annotate(plugin: PluginContext, sessionId: string, text: string) {
35
+ plugin.session(sessionId).commit(writer => { writer.append(Annotation, { text }); });
36
+ }
37
+ /** Locate the latest typed record by scope and contract; leave unknown for domain consumers to handle. */
38
+ export function latestAnnotation(plugin: PluginContext, sessionId: string) {
39
+ return plugin.session(sessionId).snapshot().records.last(Annotation, { scope: "session" });
40
+ }
41
+ const CurrentSummary = journal.data("summary", z.strictObject({ text: z.string() }));
42
+ /** Current session data does not enter the timeline; it is stored separately from object data and private plugin databases. */
43
+ export function saveSummary(plugin: PluginContext, sessionId: string, text: string) {
44
+ const session = plugin.session(sessionId);
45
+ session.commit(writer => { writer.set(CurrentSummary, { text }); });
46
+ return session.get(CurrentSummary);
47
+ }
48
+
49
+ /** Scenario two: creation, state, events, actions, and tools for an independent resource, without model capabilities. */
50
+ const counterSettings = z.strictObject({
51
+ maxChange: z.number().int().min(1).max(100).meta({
52
+ title: "Maximum change", description: "Largest absolute increment accepted by a counter action.",
53
+ }),
54
+ }).meta({ title: "Counter settings" });
55
+ const CounterConfig = sample.config(counterSettings, { maxChange: 10 });
56
+ const Read = defineTool({
57
+ id: "example.counter/read_counter", name: "read_counter", description: "Read the current counter value.", guidelines: [],
58
+ argsSchema: z.strictObject({}).describe("No arguments are required."),
59
+ });
60
+ const CounterRead = defineImplementation({
61
+ ...sample.data("read", Count.schema.describe("The current integer value.")),
62
+ resource: Counter, tool: Read,
63
+ targetSchema: z.null().describe("Pass null to select this counter."),
64
+ effect: "read", execution: "parallel",
65
+ });
66
+ function currentCount(object: ObjectReader) {
67
+ const value = object.get(Count);
68
+ if (value === null) throw new Error("Counter state is unavailable.");
69
+ return value;
70
+ }
71
+ /** Feature modules receive only their own settings read capability; configuration declarations remain at the assembly site. */
72
+ function counterChange(limit: () => number) {
73
+ return implementAction(ChangeCount, {
74
+ prepare(input) {
75
+ return Math.abs(input.amount) > limit()
76
+ ? { action: "reject", code: "change_too_large", message: "Change exceeds the limit." }
77
+ : { action: "accept" };
78
+ },
79
+ async execute(input, object) {
80
+ object.commit(writer => {
81
+ const value = currentCount(object).value + input.amount;
82
+ writer.set(Count, { value });
83
+ writer.append(CountChanged, { value });
84
+ });
85
+ },
86
+ });
87
+ }
88
+ export const counterPlugin = definePlugin({
89
+ namespace: sample, modules: [defineModule(plugin => {
90
+ const settings = plugin.config(CounterConfig);
91
+ return {
92
+ resources: [implementResource(Counter, CounterView, {
93
+ card: counterCard,
94
+ records: [CountChanged],
95
+ creation: implementCreation(CreateCounter, {
96
+ create(input, object) {
97
+ object.commit(writer => { writer.set(Count, { value: 0 }); });
98
+ return { title: input.title, data: {} };
99
+ },
100
+ }),
101
+ actions: [counterChange(() => settings.get().maxChange)],
102
+ project(_data, object) { return currentCount(object); },
103
+ async inspect(_data, object) { return currentCount(object); },
104
+ })],
105
+ tools: [implementTool(CounterRead, {
106
+ async call(_data, _target, _args, context) {
107
+ context.signal.throwIfAborted();
108
+ return { kind: "succeeded", data: currentCount(context.object) };
109
+ },
110
+ })],
111
+ // The channel follows the object's lifetime: the host aborts signal when the object is removed or disappears after rewinding.
112
+ channels: [implementChannel(CounterUpdates, channel => {
113
+ channel.onMessage(() => channel.send(currentCount(channel.object)));
114
+ const timer = setInterval(() => channel.send(currentCount(channel.object)), 1000);
115
+ channel.signal.addEventListener("abort", () => clearInterval(timer), { once: true });
116
+ })],
117
+ };
118
+ })],
119
+ });
120
+
121
+ /** Scenario three: the plugin's own Agent drives the loop, with context projected from the timeline each time. */
122
+ const research = pluginNamespace("example.research");
123
+ const Execution = defineRecord(research.name("execution"), { schema: modelRunRecordSchema });
124
+ export const modelRecordsModule = defineModule(() => ({ records: [Execution] }));
125
+ /** The domain plugin constructs the projection from its own input/execution events; it is not stored in Agent or run. */
126
+ export async function modelWork(plugin: PluginContext, object: ObjectExecutor, signal: AbortSignal,
127
+ projectOwnContext: (snapshot: SessionSnapshot, object: ResourceRef) => ModelItem[]) {
128
+ for (let request = 0; request < 8; request++) {
129
+ const snapshot = object.session.snapshot();
130
+ const result = await plugin.models.run({
131
+ object: object.ref, requestParentId: snapshot.position.nodeId, target: { provider: "configured-provider", model: "configured-model" },
132
+ system: { blocks: [{ id: research.name("instruction"), text: "Inspect linked materials and report findings." }] },
133
+ messages: projectOwnContext(snapshot, object.ref), cache: { expectHit: request > 0 }, signal,
134
+ observer: { event(event) {
135
+ if (event.type === "progress") return;
136
+ object.commit(writer => { writer.append(Execution, modelRunRecordSchema.parse(event)); });
137
+ } },
138
+ });
139
+ if (result.reason !== "completed" || result.tools.length === 0) return result;
140
+ }
141
+ throw new Error("The model request limit was reached.");
142
+ }
143
+
144
+ /** External preparers also infer input from the action contract they hold, without redeclaring the schema. */
145
+ export const counterLimit = implementPreparer(ChangeCount, {
146
+ id: "clamp-change",
147
+ prepare(input) { return { action: "replace", input: { amount: Math.max(-10, Math.min(10, input.amount)) } }; },
148
+ });
149
+
150
+ /** Invocation and replacement receive raw strings; preparation and execution receive numbers transformed by the schema. */
151
+ export const FromText = defineAction(Counter, { id: sample.name("from-text"),
152
+ schema: z.string().regex(/^\d+$/u).transform(Number),
153
+ });
154
+ export const parsedAction = implementAction(FromText, {
155
+ prepare(value) { return value > 10 ? { action: "replace", input: "10" } : { action: "accept" }; },
156
+ async execute(value, object) { object.commit(writer => { writer.set(Count, { value }); }); },
157
+ });
158
+ export const parsedInput = resourceCall(FromText, "5");
@@ -0,0 +1,29 @@
1
+ import type { StreamRequest, UsageItem } from "@asterism-app/sdk/contracts";
2
+ import type { PluginContext } from "@asterism-app/sdk/runtime";
3
+
4
+ /** Example consumer: actively queries before a request and parses response headers against the current catalog for the response's connection. */
5
+ export async function* streamWithUsage(plugin: PluginContext, connectionId: string, request: StreamRequest,
6
+ receive: (items: UsageItem[]) => void) {
7
+ const connection = plugin.modelProviders.get(connectionId);
8
+ const usage = connection?.enabled && connection.usage ? plugin.usageProviders.get(connection.usage) : null;
9
+ if (connection && usage && usage.source !== "headers") {
10
+ const credential = await plugin.modelProviders.credential(connection.id, { signal: request.signal });
11
+ const items = await plugin.usages.query(usage.id, { connection: { id: connection.id, endpoint: connection.baseUrl }, credential, signal: request.signal });
12
+ receive(items);
13
+ }
14
+ for await (const event of plugin.models.stream(request)) {
15
+ const response = event.type === "finish" ? event.response : null;
16
+ if (response) {
17
+ const current = plugin.modelProviders.get(response.connection.id);
18
+ const parser = current?.enabled && current.usage ? plugin.usageProviders.get(current.usage) : null;
19
+ if (parser?.source === "headers") {
20
+ try {
21
+ const items = plugin.usages.parseHeaders(parser.id, { connection: { id: response.connection.id, endpoint: response.endpoint },
22
+ model: response.model, status: response.status, receivedAt: response.receivedAt, headers: response.headers });
23
+ if (items !== null) receive(items);
24
+ } catch { plugin.log.warn("Usage response parsing failed."); }
25
+ }
26
+ }
27
+ yield event;
28
+ }
29
+ }
@@ -0,0 +1,29 @@
1
+ import { defineModule, definePlugin, implementAction, implementCreation, implementResource } from "@asterism-app/sdk/runtime";
2
+ import { CreateDocument, Document, DocumentContent, Imported, ReplaceDocument, files } from "./content-contract.ts";
3
+
4
+ /** The session owns content, and plugins only interpret business formats; this example does not access host source code or private paths. */
5
+ export default definePlugin({ namespace: files, modules: [defineModule(() => ({
6
+ resources: [implementResource(Document, DocumentContent, {
7
+ records: [Imported],
8
+ creation: implementCreation(CreateDocument, {
9
+ async prepare(input, { content }) {
10
+ // Format checks can read a bounded range or use open to stream large files.
11
+ await content.read(input.file, { length: 512 });
12
+ return { action: "accept" };
13
+ },
14
+ create(input, object) {
15
+ object.commit(writer => {
16
+ writer.set(DocumentContent, input);
17
+ // This example explicitly retains the originally imported content; resources that store only the current value can omit this event.
18
+ writer.append(Imported, input);
19
+ });
20
+ return { title: input.name, data: {} };
21
+ },
22
+ }),
23
+ actions: [implementAction(ReplaceDocument, {
24
+ async execute(input, object) { object.commit(writer => { writer.set(DocumentContent, input); }); },
25
+ })],
26
+ project: (_data, object) => object.get(DocumentContent)!,
27
+ inspect: async (_data, object) => object.get(DocumentContent),
28
+ })],
29
+ }))] });
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ import { contentRefSchema, defineAction, defineCreation, defineRecord, pluginNamespace } from "@asterism-app/sdk/contracts";
3
+
4
+ /** Content identities can be embedded in ordinary contracts; authors need not declare a separate list of persistent attachments. */
5
+ export const files = pluginNamespace("example.content");
6
+ export const Document = files.resource("document", z.object({}));
7
+ export const DocumentContent = files.data("content", z.object({ file: contentRefSchema, name: z.string() }));
8
+ export const Imported = defineRecord(files.name("imported"), { schema: DocumentContent.schema });
9
+ export const CreateDocument = defineCreation(Document, { id: files.name("create"), schema: DocumentContent.schema });
10
+ export const ReplaceDocument = defineAction(Document, { id: files.name("replace"), schema: DocumentContent.schema });