@asterism-app/sdk 0.47.0 → 1.0.1
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/README.md +1 -1
- package/docs/data-and-lifecycle.md +2 -0
- package/docs/getting-started.md +5 -3
- package/docs/manual.md +4 -0
- package/docs/minimal-plugin.md +2 -0
- package/docs/project-structure.md +6 -0
- package/docs/resources-and-tools.md +14 -0
- package/docs/session-entries.md +31 -0
- package/docs/views-and-settings.md +9 -1
- package/examples/author.ts +2 -2
- package/examples/counter/package.json +1 -1
- package/examples/counter/src/contracts.ts +3 -0
- package/examples/counter/src/renderer.tsx +14 -3
- package/examples/counter/src/runtime.ts +8 -2
- package/examples/model-input.ts +1 -1
- package/examples/settings-renderer.ts +1 -1
- package/package.json +2 -2
- package/src/calls.ts +4 -3
- package/src/contracts.ts +10 -0
- package/src/mcp.ts +80 -0
- package/src/model-catalog.ts +18 -0
- package/src/model-provider.ts +44 -0
- package/src/models.ts +4 -7
- package/src/objects.ts +3 -3
- package/src/plugin.ts +1 -1
- package/src/provider-usage.ts +2 -42
- package/src/renderer-calls.ts +7 -3
- package/src/renderer-services.ts +15 -1
- package/src/renderer.ts +20 -4
- package/src/runtime-agent-catalog.ts +27 -0
- package/src/runtime-application.ts +7 -0
- package/src/runtime-calls.ts +2 -0
- package/src/runtime-contracts.ts +23 -4
- package/src/runtime-mcp.ts +24 -0
- package/src/runtime-model-providers.ts +16 -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-providers.ts → runtime-usage.ts} +9 -15
- package/src/runtime.ts +10 -1
- package/src/session-title.ts +8 -0
- package/src/shell.ts +76 -0
- package/src/terminal.ts +36 -0
- package/src/usage-state.ts +20 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Public interfaces should express a capability an author needs. Internal decompos
|
|
|
10
10
|
|
|
11
11
|
## Installation
|
|
12
12
|
|
|
13
|
-
Install
|
|
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
14
|
|
|
15
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
16
|
|
|
@@ -28,6 +28,8 @@ A resource opts into data-copy templates with `template: { current: [Settings, C
|
|
|
28
28
|
|
|
29
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
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
|
+
|
|
31
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.
|
|
32
34
|
|
|
33
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.
|
package/docs/getting-started.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Prepare for plugin development
|
|
2
2
|
|
|
3
|
-
A plugin adds resources, tools, views, settings, 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.
|
|
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
4
|
|
|
5
5
|
## Prepare a project
|
|
6
6
|
|
|
@@ -10,14 +10,16 @@ Set the plugin's release version in `package.json.version`. That version identif
|
|
|
10
10
|
|
|
11
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
12
|
|
|
13
|
-
|
|
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
14
|
|
|
15
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
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.
|
|
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
18
|
|
|
19
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
20
|
|
|
21
|
+
The `.test.` filename marker is reserved for tests, such as `count.test.ts` or `card.test.tsx`. Tests can remain alongside implementation files. Automatic shared-module discovery and application/package distribution exclude `*.test.*`; runtime entries and their imports contain application code only. Plugin compilation reports explicit imports of test files or Vitest instead of silently removing dependencies.
|
|
22
|
+
|
|
21
23
|
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
24
|
|
|
23
25
|
## Load and verify
|
package/docs/manual.md
CHANGED
|
@@ -19,10 +19,14 @@ Each entry names an existing package-relative subdirectory using `/` separators.
|
|
|
19
19
|
|
|
20
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
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
|
+
|
|
22
24
|
## Read through the manual
|
|
23
25
|
|
|
24
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.
|
|
25
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
|
+
|
|
26
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.
|
|
27
31
|
|
|
28
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.
|
package/docs/minimal-plugin.md
CHANGED
|
@@ -16,6 +16,8 @@ Creation establishes the object. Incrementing reads the configured step, saves t
|
|
|
16
16
|
|
|
17
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
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
|
+
|
|
19
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.
|
|
20
22
|
|
|
21
23
|
## Adapt the package
|
|
@@ -16,12 +16,18 @@ The general SDK supplies host services and contribution interfaces. Built-in dom
|
|
|
16
16
|
|
|
17
17
|
Use a domain extension when adding to that feature. A plugin with its own resource can define its own behavior and presentation.
|
|
18
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
|
+
|
|
19
21
|
## Share contracts and services
|
|
20
22
|
|
|
21
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.
|
|
22
24
|
|
|
23
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.
|
|
24
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
|
+
|
|
25
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.
|
|
26
32
|
|
|
27
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.
|
|
@@ -12,6 +12,8 @@ Creation supplies initial state. Actions change the object through typed applica
|
|
|
12
12
|
|
|
13
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
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
|
+
|
|
15
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.
|
|
16
18
|
|
|
17
19
|
## Add a model operation
|
|
@@ -22,8 +24,20 @@ Use a top-level tool for operations on the execution context or shared resource
|
|
|
22
24
|
|
|
23
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.
|
|
24
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
|
+
|
|
25
29
|
## Connect other interactions
|
|
26
30
|
|
|
27
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.
|
|
28
32
|
|
|
29
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.
|
|
@@ -22,12 +22,20 @@ Styles remain loaded while the plugin frontend is active, including when individ
|
|
|
22
22
|
|
|
23
23
|
## Add settings
|
|
24
24
|
|
|
25
|
-
A plugin can
|
|
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.
|
|
26
28
|
|
|
27
29
|
Managed configuration suits schema-validated settings. Other plugin data can use a storage model appropriate to its purpose.
|
|
28
30
|
|
|
29
31
|
Closing a page releases its view subscriptions. It does not end every service the plugin uses.
|
|
30
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
|
+
|
|
31
39
|
## Write interface text
|
|
32
40
|
|
|
33
41
|
Plain text is valid. Message declarations and translation catalogs support plugins that need localized interfaces.
|
package/examples/author.ts
CHANGED
|
@@ -128,8 +128,8 @@ export async function modelWork(plugin: PluginContext, object: ObjectExecutor, s
|
|
|
128
128
|
for (let request = 0; request < 8; request++) {
|
|
129
129
|
const snapshot = object.session.snapshot();
|
|
130
130
|
const result = await plugin.models.run({
|
|
131
|
-
object: object.ref, requestParentId: snapshot.position.nodeId, target: {
|
|
132
|
-
system: { blocks: [{ id: research.name("instruction"),
|
|
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
133
|
messages: projectOwnContext(snapshot, object.ref), cache: { expectHit: request > 0 }, signal,
|
|
134
134
|
observer: { event(event) {
|
|
135
135
|
if (event.type === "progress") return;
|
|
@@ -18,6 +18,9 @@ export const Settings = ns.config(
|
|
|
18
18
|
export const ReadStep = defineCall(ns.name("read-step"), {
|
|
19
19
|
scope: "plugin", input: z.null(), output: z.number(),
|
|
20
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
|
+
});
|
|
21
24
|
const Read = defineTool({
|
|
22
25
|
id: ns.name("read-counter"), name: "read_counter",
|
|
23
26
|
description: "Read the current value of a counter.", guidelines: [],
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { useRef, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
defineRendererPlugin, cardRenderer, creationEntry, toolRenderer,
|
|
4
|
-
useRendererObject, useRendererConfiguration, useRendererCalls, useI18n,
|
|
4
|
+
useRendererObject, useRendererConfiguration, useRendererCalls, useI18n, type SessionEntryProps,
|
|
5
5
|
} from "@asterism-app/sdk/renderer";
|
|
6
|
-
import { ns, card, Counter, View, Create, Increment, Settings, ReadStep, ReadCounter } from "./contracts.ts";
|
|
6
|
+
import { ns, card, Counter, View, Create, Increment, Settings, ReadStep, ReadCounter, StartSession } from "./contracts.ts";
|
|
7
7
|
import styles from "./panel.module.css";
|
|
8
8
|
|
|
9
9
|
function Operation({ label, disabled = false, run }: {
|
|
@@ -52,8 +52,19 @@ function SettingsPage() {
|
|
|
52
52
|
</div>;
|
|
53
53
|
}
|
|
54
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
|
+
|
|
55
65
|
export default defineRendererPlugin({
|
|
56
|
-
|
|
66
|
+
sessionEntries: [{ id: "counter", title: "Counter workspace", Body: CounterEntry }],
|
|
67
|
+
settings: [{ id: "counter", title: "Counter", summary: "Configure the counter step.", Body: SettingsPage }],
|
|
57
68
|
modules: [{
|
|
58
69
|
resources: [{ kind: Counter.kind, label: "Counter", icon: null, card }],
|
|
59
70
|
creations: [creationEntry(Create, {
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "@asterism-app/sdk/runtime";
|
|
5
5
|
import {
|
|
6
6
|
ns, card, Counter, Value, View, Changed, Create, Increment,
|
|
7
|
-
Settings, ReadStep, ReadCounter,
|
|
7
|
+
Settings, ReadStep, ReadCounter, StartSession,
|
|
8
8
|
} from "./contracts.ts";
|
|
9
9
|
|
|
10
10
|
const current = (object: ObjectReader) => object.get(Value) ?? 0;
|
|
@@ -14,7 +14,13 @@ export default definePlugin({
|
|
|
14
14
|
setup(plugin) {
|
|
15
15
|
const settings = plugin.config(Settings);
|
|
16
16
|
return {
|
|
17
|
-
calls: [
|
|
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
|
+
],
|
|
18
24
|
resources: [implementResource(Counter, View, {
|
|
19
25
|
card, records: [Changed],
|
|
20
26
|
template: { current: [Value] },
|
package/examples/model-input.ts
CHANGED
|
@@ -8,7 +8,7 @@ export async function describeFile(models: ModelServices, content: ContentScope,
|
|
|
8
8
|
{ kind: "text", text: "Describe the image." },
|
|
9
9
|
{ kind: "file", file, mediaType: "image/png", filename: "screenshot.png" },
|
|
10
10
|
] }];
|
|
11
|
-
const request = { target: {
|
|
11
|
+
const request = { target: { provider: "configured-provider", model: "configured-model" }, system: { blocks: [] }, messages, signal };
|
|
12
12
|
await models.validate(request, { content });
|
|
13
13
|
for await (const event of models.stream(request, { content })) {
|
|
14
14
|
if (event.type === "part.delta") void event.delta;
|
|
@@ -23,7 +23,7 @@ function SettingsPage() {
|
|
|
23
23
|
createElement("p", null, label),
|
|
24
24
|
error ? createElement("p", { role: "alert" }, error) : null);
|
|
25
25
|
}
|
|
26
|
-
export default defineRendererPlugin({ settings: {
|
|
26
|
+
export default defineRendererPlugin({ settings: [{ id: "general", title: "Labels", summary: "Configure shared labels.", Body: SettingsPage }] });
|
|
27
27
|
export function OpenSettings() {
|
|
28
28
|
const settings = useRendererSettings();
|
|
29
29
|
return createElement("button", { onClick: () => settings.open(settingsPlugin.id, "general") }, "Open settings");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asterism-app/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Public contracts and host capabilities for Asterism plugins",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Chien Zhang",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"files": [
|
|
35
35
|
"src",
|
|
36
|
-
"
|
|
36
|
+
"!**/*.test.*",
|
|
37
37
|
"docs",
|
|
38
38
|
"examples",
|
|
39
39
|
"README.md"
|
package/src/calls.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { z } from "zod";
|
|
|
2
2
|
import type { Json } from "./json.ts";
|
|
3
3
|
import { contractReferenceSchema } from "./presentation.ts";
|
|
4
4
|
import type { ResourceRef } from "./workspace.ts";
|
|
5
|
+
import type { ContentReader } from "./content-runtime.ts";
|
|
5
6
|
|
|
6
7
|
export type CallScope = "plugin" | "object";
|
|
7
8
|
/** Scope is declared by the contract; callers cannot downgrade an object-level operation by omitting the object. */
|
|
@@ -17,11 +18,11 @@ export function defineCall<Input extends z.ZodType<Json>, Output extends z.ZodTy
|
|
|
17
18
|
if (definition.scope !== "plugin" && definition.scope !== "object") throw new Error("A call must declare plugin or object scope.");
|
|
18
19
|
return Object.freeze({ ...contractReferenceSchema.parse({ id }), ...definition });
|
|
19
20
|
}
|
|
20
|
-
export type CallOptions<S extends CallScope = CallScope> = { signal?: AbortSignal } &
|
|
21
|
+
export type CallOptions<S extends CallScope = CallScope, Content = ContentReader> = { signal?: AbortSignal; content?: Content } &
|
|
21
22
|
(S extends "object" ? { object: ResourceRef } : { object?: never });
|
|
22
|
-
export interface PluginCalls {
|
|
23
|
+
export interface PluginCalls<Content = ContentReader> {
|
|
23
24
|
/** Submit JSON matching the input schema's input; receive the output schema's parsed result. */
|
|
24
25
|
invoke<Input extends z.ZodType<Json>, Output extends z.ZodType<Json>, S extends CallScope>(
|
|
25
|
-
contract: CallContract<Input, Output, S>, input: NoInfer<z.input<Input>> & Json, options: CallOptions<NoInfer<S
|
|
26
|
+
contract: CallContract<Input, Output, S>, input: NoInfer<z.input<Input>> & Json, options: CallOptions<NoInfer<S>, Content>,
|
|
26
27
|
): Promise<z.output<Output>>;
|
|
27
28
|
}
|
package/src/contracts.ts
CHANGED
|
@@ -22,8 +22,18 @@ export * from "./content.ts";
|
|
|
22
22
|
export * from "./model-input.ts";
|
|
23
23
|
|
|
24
24
|
export * from "./model-selection.ts";
|
|
25
|
+
export * from "./model-catalog.ts";
|
|
25
26
|
export * from "./model-cost.ts";
|
|
26
27
|
export * from "./model-cost-estimate.ts";
|
|
27
28
|
export * from "./model-media.ts";
|
|
29
|
+
export * from "./model-provider.ts";
|
|
28
30
|
export * from "./provider-usage.ts";
|
|
29
31
|
export * from "./web.ts";
|
|
32
|
+
|
|
33
|
+
export * from "./session-title.ts";
|
|
34
|
+
|
|
35
|
+
export * from "./usage-state.ts";
|
|
36
|
+
|
|
37
|
+
export * from "./shell.ts";
|
|
38
|
+
export * from "./terminal.ts";
|
|
39
|
+
export * from "./mcp.ts";
|
package/src/mcp.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineCall } from "./calls.ts";
|
|
3
|
+
import { defineEvent } from "./events.ts";
|
|
4
|
+
import { jsonSchema } from "./json.ts";
|
|
5
|
+
|
|
6
|
+
export const SERVER_ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/u;
|
|
7
|
+
export const serverIdSchema = z.string().min(1).max(64).regex(SERVER_ID_PATTERN);
|
|
8
|
+
export const mcpServerSchema = z.strictObject({
|
|
9
|
+
id: serverIdSchema, name: z.string().trim().min(1), enabled: z.boolean(),
|
|
10
|
+
mode: z.enum(["eager", "lazy"]), idleSeconds: z.number().int().min(1).max(86_400),
|
|
11
|
+
transport: z.enum(["stdio", "http"]), command: z.string(), args: z.array(z.string()), cwd: z.string(),
|
|
12
|
+
url: z.string(), auth: z.enum(["none", "oauth"]),
|
|
13
|
+
}).superRefine((value, ctx) => {
|
|
14
|
+
if (value.transport === "stdio" && !value.command.trim()) ctx.addIssue({ code: "custom", path: ["command"], message: "An executable is required." });
|
|
15
|
+
if (value.transport === "http") {
|
|
16
|
+
try { const url = new URL(value.url); if (!["http:", "https:"].includes(url.protocol)) throw new Error(); }
|
|
17
|
+
catch { ctx.addIssue({ code: "custom", path: ["url"], message: "An HTTP or HTTPS MCP URL is required." }); }
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
export type McpServerConfig = z.infer<typeof mcpServerSchema>;
|
|
21
|
+
export const mcpSettingsSchema = z.object({ servers: z.array(mcpServerSchema).refine(values =>
|
|
22
|
+
new Set(values.map(value => value.id)).size === values.length, "Server IDs are unique.") });
|
|
23
|
+
export const defaultMcpSettings = { servers: [] };
|
|
24
|
+
export const credentialsSchema = z.record(z.string(), z.string());
|
|
25
|
+
export const connectionPhases = ["disconnected", "connecting", "awaiting_auth", "authorizing", "connected", "error"] as const;
|
|
26
|
+
export type ConnectionPhase = typeof connectionPhases[number];
|
|
27
|
+
export const mcpServerStateSchema = z.object({
|
|
28
|
+
id: z.string(), name: z.string(), enabled: z.boolean(), mode: z.enum(["eager", "lazy"]),
|
|
29
|
+
connection: z.enum(connectionPhases),
|
|
30
|
+
indexed: z.boolean(), toolCount: z.number(), error: z.string().nullable(), authorizationUrl: z.string().nullable(),
|
|
31
|
+
authorizationExpiresAt: z.number().nullable(),
|
|
32
|
+
});
|
|
33
|
+
export type McpServerState = z.infer<typeof mcpServerStateSchema>;
|
|
34
|
+
export const cachedMcpToolSchema = z.object({
|
|
35
|
+
name: z.string(), title: z.string().optional(), description: z.string().optional(),
|
|
36
|
+
inputSchema: z.record(z.string(), jsonSchema), outputSchema: z.record(z.string(), jsonSchema).optional(),
|
|
37
|
+
annotations: z.record(z.string(), jsonSchema).optional(),
|
|
38
|
+
});
|
|
39
|
+
export type CachedMcpTool = z.infer<typeof cachedMcpToolSchema>;
|
|
40
|
+
export const mcpDirectorySchema = z.object({ tools: z.array(cachedMcpToolSchema), instructions: z.string().optional() });
|
|
41
|
+
export type McpDirectory = z.infer<typeof mcpDirectorySchema>;
|
|
42
|
+
export interface McpCatalogServer { config: McpServerConfig; state: McpServerState; directory?: McpDirectory }
|
|
43
|
+
|
|
44
|
+
export const McpChanged = defineEvent("core.mcp.changed", { schema: z.object({}), source: "host", semantics: { kind: "snapshot", key: [] } });
|
|
45
|
+
export const ReadMcp = defineCall("core.mcp.status", { scope: "plugin", input: z.object({}), output: z.array(mcpServerStateSchema) });
|
|
46
|
+
/** keep retains saved environment or headers; a new server with keep starts without any. */
|
|
47
|
+
export const mcpCredentialUpdateSchema = z.discriminatedUnion("action", [
|
|
48
|
+
z.strictObject({ action: z.literal("keep") }), z.strictObject({ action: z.literal("replace"), values: credentialsSchema }),
|
|
49
|
+
]);
|
|
50
|
+
export type McpCredentialUpdate = z.infer<typeof mcpCredentialUpdateSchema>;
|
|
51
|
+
export const SaveMcpServer = defineCall("core.mcp.save-server", { scope: "plugin",
|
|
52
|
+
input: z.object({ server: mcpServerSchema, create: z.boolean(), credentials: mcpCredentialUpdateSchema }), output: z.null() });
|
|
53
|
+
export const ReadMcpCredentials = defineCall("core.mcp.read-credentials", { scope: "plugin", input: z.object({ serverId: z.string() }), output: credentialsSchema });
|
|
54
|
+
export const mcpToolSummarySchema = z.object({ name: z.string(), title: z.string().optional(), description: z.string().optional() });
|
|
55
|
+
export type McpToolSummary = z.infer<typeof mcpToolSummarySchema>;
|
|
56
|
+
export const ReadMcpTools = defineCall("core.mcp.read-tools", { scope: "plugin", input: z.object({ serverId: z.string() }),
|
|
57
|
+
output: z.object({ tools: z.array(mcpToolSummarySchema), instructions: z.string().optional() }).nullable() });
|
|
58
|
+
export const SetMcpServerEnabled = defineCall("core.mcp.set-enabled", { scope: "plugin", input: z.object({ serverId: z.string(), enabled: z.boolean() }), output: z.null() });
|
|
59
|
+
export const RemoveMcpServer = defineCall("core.mcp.remove-server", { scope: "plugin", input: z.object({ serverId: z.string() }), output: z.null() });
|
|
60
|
+
export const ConnectMcpServer = defineCall("core.mcp.connect-server", { scope: "plugin", input: z.object({ serverId: z.string() }), output: z.null() });
|
|
61
|
+
export const AuthorizeMcpServer = defineCall("core.mcp.authorize-server", { scope: "plugin", input: z.object({ serverId: z.string() }), output: z.object({ url: z.string() }) });
|
|
62
|
+
export const DisconnectMcpServer = defineCall("core.mcp.disconnect-server", { scope: "plugin", input: z.object({ serverId: z.string() }), output: z.null() });
|
|
63
|
+
export const ResetMcpAuth = defineCall("core.mcp.reset-auth", { scope: "plugin", input: z.object({ serverId: z.string() }), output: z.null() });
|
|
64
|
+
|
|
65
|
+
export type McpErrorCode = "invalid_tool_id" | "server_not_found" | "server_disabled" | "tool_not_found" | "invalid_tool_arguments"
|
|
66
|
+
| "service_unavailable" | "connection_failed" | "authorization_expired" | "request_failed" | "tool_error";
|
|
67
|
+
export class McpError extends Error {
|
|
68
|
+
readonly code: McpErrorCode;
|
|
69
|
+
constructor(code: McpErrorCode, message: string) { super(message); this.code = code; }
|
|
70
|
+
}
|
|
71
|
+
export class McpInterrupted extends Error {
|
|
72
|
+
readonly outcome: "cancelled" | "unknown";
|
|
73
|
+
constructor(outcome: "cancelled" | "unknown", message: string) { super(message); this.outcome = outcome; }
|
|
74
|
+
}
|
|
75
|
+
export function mcpToolIdentity(serverId: string, name: string): string { return `${serverId}/${name}`; }
|
|
76
|
+
export function parseMcpToolIdentity(value: string): [string, string] {
|
|
77
|
+
const split = value.indexOf("/"), serverId = value.slice(0, split), name = value.slice(split + 1);
|
|
78
|
+
if (split < 1 || !name || !SERVER_ID_PATTERN.test(serverId)) throw new McpError("invalid_tool_id", "Tool identity uses the form server_id/tool_name.");
|
|
79
|
+
return [serverId, name];
|
|
80
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineData } from "./presentation.ts";
|
|
3
|
+
import { modelCostSchema } from "./model-cost.ts";
|
|
4
|
+
import { thinkingSchema } from "./model-selection.ts";
|
|
5
|
+
|
|
6
|
+
export const catalogModelSchema = z.object({
|
|
7
|
+
providerId: z.string(), modelId: z.string(), name: z.string(), enabled: z.boolean(),
|
|
8
|
+
contextWindow: z.number().nullable(), cost: modelCostSchema.nullable(),
|
|
9
|
+
generation: z.object({ maxOutputTokens: z.number().nullable() }),
|
|
10
|
+
capabilities: z.object({ reasoning: z.boolean(), tools: z.boolean(), thinkingLevels: z.array(thinkingSchema) }),
|
|
11
|
+
});
|
|
12
|
+
/** Configured model metadata for plugin selectors and diagnostics; credentials remain backend capabilities. */
|
|
13
|
+
export const ModelCatalog = defineData("asterism.models/catalog", z.object({
|
|
14
|
+
modelProviders: z.array(z.object({ id: z.string(), name: z.string(), enabled: z.boolean() })),
|
|
15
|
+
models: z.array(catalogModelSchema),
|
|
16
|
+
}));
|
|
17
|
+
export type ModelCatalogSnapshot = z.infer<typeof ModelCatalog.schema>;
|
|
18
|
+
export type CatalogModel = z.infer<typeof catalogModelSchema>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { uiTextSchema, type UiText } from "./i18n.ts";
|
|
3
|
+
import { jsonSchema, type Json } from "./json.ts";
|
|
4
|
+
import { providerUsageIdSchema } from "./provider-usage.ts";
|
|
5
|
+
|
|
6
|
+
export const modelProviderFixedFields = ["protocol", "baseUrl", "auth", "compatibility", "usage"] as const;
|
|
7
|
+
export interface ModelProviderConnection {
|
|
8
|
+
protocol: "openai-responses" | "openai-chat" | "anthropic";
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
auth: "api_key" | "none";
|
|
11
|
+
compatibility: { id: string; value?: Json }[];
|
|
12
|
+
usage?: string | null;
|
|
13
|
+
fixedFields?: (typeof modelProviderFixedFields)[number][];
|
|
14
|
+
}
|
|
15
|
+
export interface ModelProviderPreset {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
billing: "usage" | "subscription" | "none";
|
|
19
|
+
region?: UiText;
|
|
20
|
+
plan?: UiText;
|
|
21
|
+
connection: ModelProviderConnection;
|
|
22
|
+
keyUrl?: string;
|
|
23
|
+
sources: readonly [string, ...string[]];
|
|
24
|
+
verifiedOn: string;
|
|
25
|
+
keywords?: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
const webUrl = z.string().url().refine(value => ["http:", "https:"].includes(new URL(value).protocol));
|
|
28
|
+
export const modelProviderConnectionSchema = z.strictObject({
|
|
29
|
+
protocol: z.enum(["openai-responses", "openai-chat", "anthropic"]),
|
|
30
|
+
baseUrl: webUrl, auth: z.enum(["api_key", "none"]),
|
|
31
|
+
compatibility: z.array(z.strictObject({ id: z.string().min(1), value: jsonSchema.optional() })),
|
|
32
|
+
usage: providerUsageIdSchema.nullable().optional(),
|
|
33
|
+
fixedFields: z.array(z.enum(modelProviderFixedFields)).optional(),
|
|
34
|
+
});
|
|
35
|
+
export const modelProviderPresetSchema = z.strictObject({
|
|
36
|
+
id: z.string().trim().min(1).max(160).regex(/^[^/\s]+$/u), name: z.string().trim().min(1).max(160),
|
|
37
|
+
billing: z.enum(["usage", "subscription", "none"]), region: uiTextSchema.optional(), plan: uiTextSchema.optional(),
|
|
38
|
+
connection: modelProviderConnectionSchema, keyUrl: webUrl.optional(),
|
|
39
|
+
sources: z.tuple([webUrl]).rest(webUrl), verifiedOn: z.iso.date(), keywords: z.array(z.string()).optional(),
|
|
40
|
+
});
|
|
41
|
+
/** patch overrides only explicit fields; connection merges by field, arrays are replaced as a whole, and saved connections are not rewritten. */
|
|
42
|
+
export type ModelProviderPresetContribution =
|
|
43
|
+
| { type: "add"; preset: ModelProviderPreset }
|
|
44
|
+
| { type: "patch"; id: string; patch: Partial<Omit<ModelProviderPreset, "id" | "connection">> & { connection?: Partial<ModelProviderConnection> } };
|
package/src/models.ts
CHANGED
|
@@ -11,11 +11,9 @@ import type { ErrorDetails } from "./error-details.ts";
|
|
|
11
11
|
/** Standard levels shared across the application; protocol encoding does not change public values. */
|
|
12
12
|
export const thinkingLevels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
13
13
|
export type ThinkingLevel = typeof thinkingLevels[number];
|
|
14
|
-
export interface ModelTarget {
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
| { id: string; kind: "agent"; role: string; text: string }
|
|
18
|
-
| { id: string; kind: "context"; file: string; text: string };
|
|
14
|
+
export interface ModelTarget { provider: string; model: string; thinking?: ThinkingLevel }
|
|
15
|
+
export interface PromptBlock { id: string; text: string; source?: { id: string; plugin: string | null } }
|
|
16
|
+
export interface PromptFile { readonly file: string; readonly text: string }
|
|
19
17
|
export interface SystemPrompt { blocks: readonly PromptBlock[] }
|
|
20
18
|
/** Reasoning text is the plaintext delivered by the protocol, and summary is an independent summary; both can coexist or be empty, and neither fills the other.
|
|
21
19
|
* continuation carries only native data such as signatures/ciphertext; target projection determines whether it can be reused.
|
|
@@ -94,7 +92,7 @@ export interface ModelRunRequest {
|
|
|
94
92
|
userAgent?: string;
|
|
95
93
|
requestParentId: string | null;
|
|
96
94
|
target: ModelTarget;
|
|
97
|
-
system: SystemPrompt;
|
|
95
|
+
system: SystemPrompt | ((files: readonly PromptFile[]) => SystemPrompt);
|
|
98
96
|
messages: ModelItem[];
|
|
99
97
|
/** Filters plugin top-level tools and resource tools selected through actual links by name; does not affect fixed host entry points. */
|
|
100
98
|
tools?: { allow?: readonly string[]; exclude?: readonly string[] };
|
|
@@ -132,6 +130,5 @@ export interface ModelServices {
|
|
|
132
130
|
stream(request: StreamRequest, options?: ModelInputOptions): AsyncIterable<ModelEvent>;
|
|
133
131
|
/** One request and its tool batch; neither saves history nor drives the next request. */
|
|
134
132
|
run(request: ModelRunRequest): Promise<ModelRunResult>;
|
|
135
|
-
roles(): readonly { id: string; description: string; hidden: boolean; model: string | null; thinking: ThinkingLevel | null }[];
|
|
136
133
|
list(): readonly { model: string; provider: string; name: string; contextWindow: number | null; capabilities: { reasoning: boolean; tools: boolean; thinkingLevels: readonly ThinkingLevel[] } }[];
|
|
137
134
|
}
|
package/src/objects.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Json } from "./json.ts";
|
|
2
|
-
import type { DataContract } from "./presentation.ts";
|
|
2
|
+
import type { DataContract, Presentation } from "./presentation.ts";
|
|
3
3
|
import type { RecordContract } from "./records.ts";
|
|
4
4
|
import type { ResourceRef, ResourceCreationResult, ResourceSnapshot, SessionSnapshot } from "./workspace.ts";
|
|
5
5
|
import type { ContentReader, ContentScope } from "./content-runtime.ts";
|
|
@@ -49,8 +49,8 @@ export interface SessionExecutor extends SessionReader {
|
|
|
49
49
|
/** Append a resource rename event when the title changes; return the title with leading and trailing whitespace removed. */
|
|
50
50
|
rename(resource: ResourceRef, title: string): string;
|
|
51
51
|
commit(work: (writer: SessionWriter) => undefined): void;
|
|
52
|
-
/**
|
|
53
|
-
readonly title: SessionReader["title"] & { set(text: string): void };
|
|
52
|
+
/** Append a host title assignment atomically, with optional plugin-owned detail. */
|
|
53
|
+
readonly title: SessionReader["title"] & { set(text: string, detail?: Presentation): void };
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
56
|
* An operation facade for the same session from the perspective of the tool caller and this request.
|
package/src/plugin.ts
CHANGED
|
@@ -55,4 +55,4 @@ export function pluginOf(qualified: string): string | null {
|
|
|
55
55
|
return index > 0 ? qualified.slice(0, index) : null;
|
|
56
56
|
}
|
|
57
57
|
/** Resource projection for a missing implementation: preserves raw data and the source plugin for fallback UI display. */
|
|
58
|
-
export const missingResourceContract = { id: "asterism.
|
|
58
|
+
export const missingResourceContract = { id: "asterism.host/missing-resource" } as const;
|
package/src/provider-usage.ts
CHANGED
|
@@ -1,48 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { uiTextSchema
|
|
3
|
-
import { jsonSchema, type Json } from "./json.ts";
|
|
2
|
+
import { uiTextSchema } from "./i18n.ts";
|
|
4
3
|
|
|
5
|
-
/** Usage
|
|
4
|
+
/** Usage implementations have global catalog identities, independent of configured model connections. */
|
|
6
5
|
export const providerUsageIdSchema = z.string().trim().min(1).max(160).regex(/^\S+$/u);
|
|
7
|
-
export const modelProviderFixedFields = ["protocol", "baseUrl", "auth", "compatibility", "usage"] as const;
|
|
8
|
-
export interface ModelProviderConnection {
|
|
9
|
-
protocol: "openai-responses" | "openai-chat" | "anthropic";
|
|
10
|
-
baseUrl: string;
|
|
11
|
-
auth: "api_key" | "none";
|
|
12
|
-
compatibility: { id: string; value?: Json }[];
|
|
13
|
-
usage?: string | null;
|
|
14
|
-
fixedFields?: (typeof modelProviderFixedFields)[number][];
|
|
15
|
-
}
|
|
16
|
-
export interface ModelProviderPreset {
|
|
17
|
-
id: string;
|
|
18
|
-
name: string;
|
|
19
|
-
billing: "usage" | "subscription" | "none";
|
|
20
|
-
region?: UiText;
|
|
21
|
-
plan?: UiText;
|
|
22
|
-
connection: ModelProviderConnection;
|
|
23
|
-
keyUrl?: string;
|
|
24
|
-
sources: readonly [string, ...string[]];
|
|
25
|
-
verifiedOn: string;
|
|
26
|
-
keywords?: readonly string[];
|
|
27
|
-
}
|
|
28
|
-
const webUrl = z.string().url().refine(value => ["http:", "https:"].includes(new URL(value).protocol));
|
|
29
|
-
export const modelProviderConnectionSchema = z.strictObject({
|
|
30
|
-
protocol: z.enum(["openai-responses", "openai-chat", "anthropic"]),
|
|
31
|
-
baseUrl: webUrl, auth: z.enum(["api_key", "none"]),
|
|
32
|
-
compatibility: z.array(z.strictObject({ id: z.string().min(1), value: jsonSchema.optional() })),
|
|
33
|
-
usage: providerUsageIdSchema.nullable().optional(),
|
|
34
|
-
fixedFields: z.array(z.enum(modelProviderFixedFields)).optional(),
|
|
35
|
-
});
|
|
36
|
-
export const modelProviderPresetSchema = z.strictObject({
|
|
37
|
-
id: z.string().trim().min(1).max(160).regex(/^[^/\s]+$/u), name: z.string().trim().min(1).max(160),
|
|
38
|
-
billing: z.enum(["usage", "subscription", "none"]), region: uiTextSchema.optional(), plan: uiTextSchema.optional(),
|
|
39
|
-
connection: modelProviderConnectionSchema, keyUrl: webUrl.optional(),
|
|
40
|
-
sources: z.tuple([webUrl]).rest(webUrl), verifiedOn: z.iso.date(), keywords: z.array(z.string()).optional(),
|
|
41
|
-
});
|
|
42
|
-
/** patch overrides only explicit fields; connection merges by field, arrays are replaced as a whole, and saved connections are not rewritten. */
|
|
43
|
-
export type ModelProviderPresetContribution =
|
|
44
|
-
| { type: "add"; preset: ModelProviderPreset }
|
|
45
|
-
| { type: "patch"; id: string; patch: Partial<Omit<ModelProviderPreset, "id" | "connection">> & { connection?: Partial<ModelProviderConnection> } };
|
|
46
6
|
|
|
47
7
|
const identity = {
|
|
48
8
|
id: z.string().trim().min(1), label: uiTextSchema,
|
package/src/renderer-calls.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { createContext, useContext } from "react";
|
|
2
|
-
import type { PluginCalls } from "./calls.ts";
|
|
2
|
+
import type { CallContract, PluginCalls } from "./calls.ts";
|
|
3
|
+
import type { RendererContentInput } from "./renderer-content.ts";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
export interface RendererCalls extends PluginCalls<RendererContentInput> {
|
|
6
|
+
input(contract: CallContract): RendererContentInput;
|
|
7
|
+
}
|
|
8
|
+
const Calls = createContext<RendererCalls | null>(null);
|
|
5
9
|
export const RendererCallsProvider = Calls.Provider;
|
|
6
10
|
/** Invoke a registered backend contract; associating an object does not change resource ownership. */
|
|
7
|
-
export function useRendererCalls():
|
|
11
|
+
export function useRendererCalls(): RendererCalls {
|
|
8
12
|
const calls = useContext(Calls);
|
|
9
13
|
if (!calls) throw new Error("Renderer calls must be provided by the host.");
|
|
10
14
|
return calls;
|
package/src/renderer-services.ts
CHANGED
|
@@ -68,8 +68,22 @@ export function useRendererConfiguration<T extends Json>(contract: ConfigContrac
|
|
|
68
68
|
return { ...configs.get(contract), save: (value: T) => configs.save(contract, value), reset: () => configs.reset(contract), refresh: configs.refresh };
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
export interface RendererSettingsNavigation {
|
|
71
|
+
export interface RendererSettingsNavigation {
|
|
72
|
+
open(pluginId: string, pageId: string, location?: string): void;
|
|
73
|
+
openHost(pageId: string): void;
|
|
74
|
+
}
|
|
72
75
|
const SettingsNavigation = createContext<RendererSettingsNavigation | null>(null);
|
|
73
76
|
export const RendererSettingsNavigationProvider = SettingsNavigation.Provider;
|
|
74
77
|
/** Open plugin settings without binding the current resource or session. */
|
|
75
78
|
export function useRendererSettings(): RendererSettingsNavigation { return required(useContext(SettingsNavigation), "settings navigation"); }
|
|
79
|
+
|
|
80
|
+
/** Application-wide frontend capabilities shared by plugin interfaces. */
|
|
81
|
+
export interface RendererApplicationServices {
|
|
82
|
+
readonly platform: "darwin" | "win32" | "linux";
|
|
83
|
+
preferences(pluginId: string): import("./renderer-creation.ts").RendererPreferences;
|
|
84
|
+
pickDirectory(initialPath?: string): Promise<string | null>;
|
|
85
|
+
openLocalFile(path: string): Promise<void>;
|
|
86
|
+
}
|
|
87
|
+
const Application = createContext<RendererApplicationServices | null>(null);
|
|
88
|
+
export const RendererApplicationProvider = Application.Provider;
|
|
89
|
+
export function useRendererApplication(): RendererApplicationServices { return required(useContext(Application), "application"); }
|
package/src/renderer.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type { CreationEntry } from "./renderer-creation.ts";
|
|
|
26
26
|
import type { ToolRenderer } from "./renderer-tools.ts";
|
|
27
27
|
import type { EventSubscriber } from "./events.ts";
|
|
28
28
|
import type { PluginCalls } from "./calls.ts";
|
|
29
|
+
import type { RendererCalls } from "./renderer-calls.ts";
|
|
29
30
|
import type { ChannelContract, ChannelEnd, ChannelValue } from "./channels.ts";
|
|
30
31
|
|
|
31
32
|
/** The frontend end of a channel: messages are delivered in order and buffered until a listener registers; closed resolves when either end terminates. */
|
|
@@ -84,8 +85,6 @@ export interface ResourceDescription {
|
|
|
84
85
|
icon: ReactNode;
|
|
85
86
|
/** Default card dimensions available before creation, sharing a declaration with the card renderer. */
|
|
86
87
|
card?: CardAppearance;
|
|
87
|
-
/** This resource can be the endpoint of a directed link, matching backend links.receive. */
|
|
88
|
-
receivesLinks?: boolean;
|
|
89
88
|
}
|
|
90
89
|
export interface CardView extends CardAppearance {
|
|
91
90
|
body: ReactNode;
|
|
@@ -108,21 +107,38 @@ export interface RendererModule {
|
|
|
108
107
|
prepare?(context: RendererPrepareContext): Promise<void>;
|
|
109
108
|
}
|
|
110
109
|
export interface PluginSettingsPage {
|
|
110
|
+
/** Stable page identity within the contributing plugin. */
|
|
111
|
+
id: string;
|
|
112
|
+
title: UiText;
|
|
113
|
+
summary: UiText;
|
|
114
|
+
/** Icon component drawn by the host at its own size and color; the host uses a plugin icon when omitted. */
|
|
115
|
+
icon?: ComponentType<{ className?: string; "aria-hidden"?: boolean | "true" | "false" }>;
|
|
111
116
|
/** A directly addressable location within a page; an empty location means the root page. */
|
|
112
117
|
locations?: readonly string[];
|
|
113
118
|
Body: ComponentType<{ location?: string }>;
|
|
114
119
|
}
|
|
120
|
+
export interface SessionEntryProps {
|
|
121
|
+
active: boolean;
|
|
122
|
+
ready: boolean;
|
|
123
|
+
open(sessionId: string): void;
|
|
124
|
+
}
|
|
125
|
+
export interface PluginSessionEntry {
|
|
126
|
+
id: string;
|
|
127
|
+
title: UiText;
|
|
128
|
+
Body: ComponentType<SessionEntryProps>;
|
|
129
|
+
}
|
|
115
130
|
/** Frontend plugins own settings pages and shared service lifecycles; feature views are composed from modules. */
|
|
116
131
|
export interface RendererPlugin {
|
|
117
132
|
modules?: readonly RendererModule[];
|
|
118
|
-
settings?: PluginSettingsPage;
|
|
133
|
+
settings?: readonly PluginSettingsPage[];
|
|
134
|
+
sessionEntries?: readonly PluginSessionEntry[];
|
|
119
135
|
prepare?(context: RendererPrepareContext): Promise<void>;
|
|
120
136
|
closing?(): void | Promise<void>;
|
|
121
137
|
}
|
|
122
138
|
export function defineRendererPlugin(plugin: RendererPlugin): RendererPlugin { return plugin; }
|
|
123
139
|
export interface RendererPrepareContext {
|
|
124
140
|
/** Typed calls for frontend module-level services; cancellation propagates with the preparation scope. */
|
|
125
|
-
readonly calls:
|
|
141
|
+
readonly calls: RendererCalls;
|
|
126
142
|
/** Global subscriptions bound to the module lifecycle, cleaned up on failure, timeout, or application shutdown. */
|
|
127
143
|
readonly events: EventSubscriber;
|
|
128
144
|
/** Triggered when the host stops waiting due to timeout or shutdown; the module should stop promptly. */
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { PluginModule } from "./runtime-contracts.ts";
|
|
2
|
+
|
|
3
|
+
/** The host stamps plugin provenance; a null plugin identifies a user directory. */
|
|
4
|
+
export type AgentSource = { id: string; plugin: string | null; directory: string };
|
|
5
|
+
export type AgentSkill = { name: string; description: string; directory: string; source: AgentSource };
|
|
6
|
+
export type AgentSkillSource = AgentSource & {
|
|
7
|
+
skills: AgentSkill[];
|
|
8
|
+
issues: { file: string; message: string }[];
|
|
9
|
+
};
|
|
10
|
+
export type AgentInstruction = { id: string; plugin: string; text: string };
|
|
11
|
+
export interface AgentSkillDeclaration { id: string; path: string }
|
|
12
|
+
export interface AgentInstructionDeclaration { id: string; text: string }
|
|
13
|
+
export interface AgentCatalog {
|
|
14
|
+
skills(): readonly AgentSkillSource[];
|
|
15
|
+
instructions(): readonly AgentInstruction[];
|
|
16
|
+
refresh(signal: AbortSignal): readonly AgentSkillSource[];
|
|
17
|
+
/** Read current UTF-8 contents from the captured skill package, independently of later catalog refreshes. */
|
|
18
|
+
read(skill: AgentSkill, path: string, signal: AbortSignal): Promise<string>;
|
|
19
|
+
}
|
|
20
|
+
/** Package-relative skill directories; the host supplies the actual contributor identity. */
|
|
21
|
+
export function defineAgentSkills(entries: readonly AgentSkillDeclaration[]): PluginModule {
|
|
22
|
+
return { setup: () => ({ agentSkills: entries }) };
|
|
23
|
+
}
|
|
24
|
+
/** Fixed instruction contributions, assembled into requests by the consuming paradigm. */
|
|
25
|
+
export function defineAgentInstructions(entries: readonly AgentInstructionDeclaration[]): PluginModule {
|
|
26
|
+
return { setup: () => ({ agentInstructions: entries }) };
|
|
27
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** One directory exposed under a distinct relative path in the manual catalog. */
|
|
2
|
+
export interface ManualSource {
|
|
3
|
+
readonly path: string;
|
|
4
|
+
readonly directory: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ManualCatalog { sources(): readonly ManualSource[] }
|
|
7
|
+
export interface ApplicationIdentity { readonly name: string; readonly userAgent: string }
|
package/src/runtime-calls.ts
CHANGED
|
@@ -3,11 +3,13 @@ import type { z } from "zod";
|
|
|
3
3
|
import type { Json } from "./json.ts";
|
|
4
4
|
import type { ObjectReader } from "./objects.ts";
|
|
5
5
|
import type { Source } from "./workspace.ts";
|
|
6
|
+
import type { ContentReader } from "./content-runtime.ts";
|
|
6
7
|
|
|
7
8
|
export type CallContext<S extends CallScope = CallScope> = {
|
|
8
9
|
/** The host binds the source for backend plugin calls; null for desktop user calls. */
|
|
9
10
|
readonly source: Source;
|
|
10
11
|
readonly signal: AbortSignal;
|
|
12
|
+
readonly content: ContentReader;
|
|
11
13
|
} & (S extends "object" ? { readonly object: ObjectReader } : { readonly object?: never });
|
|
12
14
|
export interface CallContribution {
|
|
13
15
|
readonly contract: CallContract;
|
package/src/runtime-contracts.ts
CHANGED
|
@@ -20,9 +20,12 @@ import type { PluginCalls } from "./calls.ts";
|
|
|
20
20
|
import type { CallContribution } from "./runtime-calls.ts";
|
|
21
21
|
import type { ChannelContribution } from "./runtime-channels.ts";
|
|
22
22
|
import type { TopLevelTool } from "./runtime-tools.ts";
|
|
23
|
-
import type { ModelProviderServices
|
|
23
|
+
import type { ModelProviderServices } from "./runtime-model-providers.ts";
|
|
24
|
+
import type { UsageProviderServices, UsageProvider, UsageServices, ProviderUsage } from "./runtime-usage.ts";
|
|
24
25
|
import type { WebServices, WebProviderServices, WebProviderDefinition } from "./runtime-web.ts";
|
|
25
|
-
import type { ModelProviderPresetContribution } from "./provider
|
|
26
|
+
import type { ModelProviderPresetContribution } from "./model-provider.ts";
|
|
27
|
+
import type { AgentCatalog, AgentSkillDeclaration, AgentInstructionDeclaration } from "./runtime-agent-catalog.ts";
|
|
28
|
+
import type { SessionCreation } from "./runtime-sessions.ts";
|
|
26
29
|
export type * from "./objects.ts";
|
|
27
30
|
export type { ResourceRef, ResourceCreationResult, ResourceSnapshot, SessionSnapshot } from "./workspace.ts";
|
|
28
31
|
|
|
@@ -36,11 +39,19 @@ export interface Lifetime {
|
|
|
36
39
|
defer(cleanup: () => void | Promise<void>): void;
|
|
37
40
|
}
|
|
38
41
|
export interface PluginContext {
|
|
42
|
+
readonly mcp: import("./runtime-mcp.ts").McpServices;
|
|
43
|
+
readonly shell: import("./runtime-shell.ts").ShellServices;
|
|
44
|
+
readonly application: import("./runtime-application.ts").ApplicationIdentity;
|
|
45
|
+
readonly manual: import("./runtime-application.ts").ManualCatalog;
|
|
39
46
|
readonly id: string;
|
|
40
47
|
/** Plugin-level lifecycle: lasts as long as the host runtime instance; shared plugin services such as connection pools, subscriptions, and public registrations belong here. */
|
|
41
48
|
readonly lifetime: Lifetime;
|
|
42
49
|
readonly sessions: SessionDirectory;
|
|
50
|
+
readonly createSession: SessionCreation;
|
|
43
51
|
readonly dir: string;
|
|
52
|
+
/** Resolve a package-relative asset, independently of writable plugin data. */
|
|
53
|
+
readonly assets: { path(relativePath: string): string };
|
|
54
|
+
readonly agentCatalog: AgentCatalog;
|
|
44
55
|
readonly log: PluginLogger;
|
|
45
56
|
readonly ipc: { readonly events: PluginEvents; readonly calls: PluginCalls };
|
|
46
57
|
config<T extends Json>(contract: ConfigContract<T>): PluginConfig<T>;
|
|
@@ -51,9 +62,9 @@ export interface PluginContext {
|
|
|
51
62
|
readonly modelProviders: ModelProviderServices;
|
|
52
63
|
readonly usageProviders: UsageProviderServices;
|
|
53
64
|
readonly usages: UsageServices;
|
|
65
|
+
readonly providerUsage: ProviderUsage;
|
|
54
66
|
readonly webProviders: WebProviderServices;
|
|
55
67
|
readonly web: WebServices;
|
|
56
|
-
readonly catalogs: { get<T extends Json>(contract: DataContract<T>): T | null };
|
|
57
68
|
session(id: string): SessionExecutor;
|
|
58
69
|
/** Obtain an object handle using a named resource contract; the host validates plugin ownership of the contract, object existence, and matching type, and stamps records accordingly. */
|
|
59
70
|
object<S extends Json>(sessionId: string, contract: ResourceContract<S>, id: string): ObjectExecutor;
|
|
@@ -103,7 +114,11 @@ export interface ResourceContribution {
|
|
|
103
114
|
readonly card?: CardAppearance;
|
|
104
115
|
readonly records: readonly RecordContract[];
|
|
105
116
|
/** Opt into templates that copy encoded resource state and only these current-data contracts, preserving their values under new host identities. */
|
|
106
|
-
readonly template?: {
|
|
117
|
+
readonly template?: {
|
|
118
|
+
readonly current: readonly DataContract[];
|
|
119
|
+
/** Omit the source title when saving; generate a title for each template instance, overriding any previously saved title. */
|
|
120
|
+
createTitle?(): string;
|
|
121
|
+
};
|
|
107
122
|
/** Declare that this resource can receive links and provide the recipient's tool policy; independent of Agent domain types. */
|
|
108
123
|
readonly links?: { receive: boolean; tools?(object: ObjectReader): { allow?: readonly string[]; exclude?: readonly string[] } };
|
|
109
124
|
readonly creation?: CreationContribution;
|
|
@@ -135,9 +150,13 @@ export interface ToolInvocation extends ToolInvocationInfo {
|
|
|
135
150
|
}
|
|
136
151
|
export interface ToolContribution {
|
|
137
152
|
readonly contract: ImplementationContract<Json, Json, Json, Json>;
|
|
153
|
+
/** Bind resource-specific access and configuration at the model request boundary. The returned contribution handles that request's calls. */
|
|
154
|
+
capture?(resource: ResourceSnapshot, object: ObjectReader, caller: ToolCaller): ToolContribution;
|
|
138
155
|
prepare(resource: ResourceSnapshot, object: ObjectExecutor, target: Json, args: Json, caller: ToolCaller): ToolInvocation | PreparedResult;
|
|
139
156
|
}
|
|
140
157
|
export interface ModuleContribution {
|
|
158
|
+
agentSkills?: readonly AgentSkillDeclaration[];
|
|
159
|
+
agentInstructions?: readonly AgentInstructionDeclaration[];
|
|
141
160
|
webProviders?: readonly WebProviderDefinition[];
|
|
142
161
|
usageProviders?: readonly UsageProvider[];
|
|
143
162
|
modelProviderPresets?: readonly ModelProviderPresetContribution[];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Json } from "./json.ts";
|
|
2
|
+
import type { McpCatalogServer, McpServerState } from "./mcp.ts";
|
|
3
|
+
|
|
4
|
+
export type McpContent =
|
|
5
|
+
| { type: "text"; text: string }
|
|
6
|
+
| { type: "image" | "audio"; data: string; mimeType: string }
|
|
7
|
+
| { type: "resource"; resource: { uri: string; mimeType?: string } & ({ text: string } | { blob: string }) }
|
|
8
|
+
| { type: "resource_link"; uri: string; name: string; description?: string };
|
|
9
|
+
export interface McpCallResult { content: McpContent[]; structuredContent?: unknown; isError?: boolean }
|
|
10
|
+
export interface McpProgress { progress: number; total?: number; message?: string }
|
|
11
|
+
/** Bound to the server instance and tool identity captured by resolve. */
|
|
12
|
+
export interface McpToolHandle {
|
|
13
|
+
readonly serverId: string;
|
|
14
|
+
readonly serverName: string;
|
|
15
|
+
readonly toolName: string;
|
|
16
|
+
call(args: Record<string, Json>, options: { signal: AbortSignal; progress(value: McpProgress): void }): Promise<McpCallResult>;
|
|
17
|
+
}
|
|
18
|
+
export interface McpServices {
|
|
19
|
+
states(): McpServerState[];
|
|
20
|
+
catalog(): McpCatalogServer[];
|
|
21
|
+
connect(serverId: string, signal: AbortSignal): Promise<void>;
|
|
22
|
+
resolve(toolId: string): McpToolHandle;
|
|
23
|
+
subscribe(listener: () => void): () => void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ModelProviderConnection, ModelProviderPreset } from "./model-provider.ts";
|
|
2
|
+
|
|
3
|
+
export interface ModelProviderServices {
|
|
4
|
+
presets(): readonly ModelProviderPreset[];
|
|
5
|
+
list(): readonly ModelProviderInfo[];
|
|
6
|
+
get(id: string): ModelProviderInfo | null;
|
|
7
|
+
/** Backend only; reads the connection's current credentials, returning null for connections without authentication. */
|
|
8
|
+
credential(id: string, options?: { signal?: AbortSignal }): Promise<string | null>;
|
|
9
|
+
/** Connection or preset catalog changes; returns an unsubscribe function and unsubscribes automatically when the plugin closes. */
|
|
10
|
+
subscribe(listener: () => void): () => void;
|
|
11
|
+
}
|
|
12
|
+
export interface ModelProviderInfo extends ModelProviderConnection {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Json } from "./json.ts";
|
|
2
|
+
import type { ActionContract, CreationContract } from "./commands.ts";
|
|
3
|
+
import type { ResourceContract } from "./module-contract.ts";
|
|
4
|
+
import type { ContentReader } from "./content-runtime.ts";
|
|
5
|
+
import type { ObjectReader, ObjectWriter, SessionReader } from "./objects.ts";
|
|
6
|
+
import type { ResourceCreationResult } from "./workspace.ts";
|
|
7
|
+
|
|
8
|
+
declare const prepared: unique symbol;
|
|
9
|
+
/** Host-issued resource preparation, valid only within one session creation attempt. */
|
|
10
|
+
export interface PreparedCreation { readonly [prepared]: true }
|
|
11
|
+
export interface SessionPreparation {
|
|
12
|
+
readonly signal: AbortSignal;
|
|
13
|
+
prepare<I, S extends Json, O extends Json>(contract: CreationContract<I, S, O>, input: NoInfer<I> & Json): Promise<PreparedCreation>;
|
|
14
|
+
/** Run action parsing and registered preparers for a resource awaiting initialization; this does not execute the action. */
|
|
15
|
+
prepareAction<I, O extends Json>(resource: PreparedCreation, contract: ActionContract<I, O>, input: NoInfer<I> & Json): Promise<O>;
|
|
16
|
+
}
|
|
17
|
+
export interface ObjectInitializer extends ObjectReader {
|
|
18
|
+
commit(work: (writer: ObjectWriter) => undefined): void;
|
|
19
|
+
}
|
|
20
|
+
export interface SessionInitializer extends SessionReader {
|
|
21
|
+
/** Materialize each prepared resource once, preserving its preparation identity. */
|
|
22
|
+
create(prepared: PreparedCreation): ResourceCreationResult;
|
|
23
|
+
object<S extends Json>(contract: ResourceContract<S>, id: string): ObjectInitializer;
|
|
24
|
+
}
|
|
25
|
+
export interface SessionInitialization {
|
|
26
|
+
title: string;
|
|
27
|
+
/** Synchronous initialization commits session identity, resources, and initial records together. */
|
|
28
|
+
initialize(session: SessionInitializer): undefined;
|
|
29
|
+
}
|
|
30
|
+
export interface SessionCreation {
|
|
31
|
+
/** The key identifies one accepted creation within this plugin; retries return its existing session. */
|
|
32
|
+
(key: string, prepare: (context: SessionPreparation) => Promise<SessionInitialization>, options?: {
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
content?: ContentReader;
|
|
35
|
+
}): Promise<string>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Json } from "./json.ts";
|
|
2
|
+
import type { CallResult } from "./runtime-contracts.ts";
|
|
3
|
+
import type { ExecResult, ShellSettings, ShellProfileList } from "./shell.ts";
|
|
4
|
+
import type { TerminalSessions } from "./runtime-terminal.ts";
|
|
5
|
+
|
|
6
|
+
export interface ShellProfiles { get(): ShellSettings; list(): Promise<ShellProfileList> }
|
|
7
|
+
/** One noninteractive shell command with piped output and its own process lifetime. */
|
|
8
|
+
export interface ShellExecution {
|
|
9
|
+
inspect(): Promise<Json>;
|
|
10
|
+
execute(cwd: string, args: { command: string; timeout?: number }, options: {
|
|
11
|
+
signal: AbortSignal; publish(data: ExecResult): void;
|
|
12
|
+
}): Promise<CallResult<ExecResult>>;
|
|
13
|
+
}
|
|
14
|
+
/** Host shell capabilities share configured profiles; plugins own how they compose commands and terminal sessions. */
|
|
15
|
+
export interface ShellServices extends ShellExecution {
|
|
16
|
+
readonly profiles: ShellProfiles;
|
|
17
|
+
readonly terminals: TerminalSessions;
|
|
18
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { TerminalConfig, TerminalDown, TerminalUp, TerminalStatus, TerminalImage } from "./terminal.ts";
|
|
2
|
+
|
|
3
|
+
export interface TerminalViewer { send(frame: TerminalDown): void }
|
|
4
|
+
export class TerminalError extends Error { constructor(readonly code: string, message: string) { super(message); } }
|
|
5
|
+
export interface InteractiveTerminal {
|
|
6
|
+
readonly status: TerminalStatus;
|
|
7
|
+
readonly started: boolean;
|
|
8
|
+
readonly applicationCursor: boolean;
|
|
9
|
+
start(): Promise<void>;
|
|
10
|
+
close(): Promise<void>;
|
|
11
|
+
dispose(): Promise<void>;
|
|
12
|
+
attach(viewer: TerminalViewer): (message: TerminalUp) => void;
|
|
13
|
+
detach(viewer: TerminalViewer): void;
|
|
14
|
+
input(data: string, signal: AbortSignal): Promise<TerminalStatus>;
|
|
15
|
+
read(source: "screen" | "history" | "raw", offset?: number, limit?: number): Promise<TerminalStatus & {
|
|
16
|
+
source: "screen" | "history" | "raw"; text: string; cols: number; rows: number;
|
|
17
|
+
cursor: { x: number; y: number }; offset: number; nextOffset: number; truncated: boolean;
|
|
18
|
+
}>;
|
|
19
|
+
image(): Promise<TerminalImage>;
|
|
20
|
+
waitIdle(timeout: number, signal: AbortSignal): Promise<TerminalStatus & { reason: string; elapsedMs: number }>;
|
|
21
|
+
}
|
|
22
|
+
export interface TerminalSessions {
|
|
23
|
+
create(config: TerminalConfig, events: { failed(error: unknown): void; log(message: string, error: unknown): void }): InteractiveTerminal;
|
|
24
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ProviderUsageError, UsageItem, UsageProviderInfo } from "./provider-usage.ts";
|
|
2
2
|
import type { UiText } from "./i18n.ts";
|
|
3
|
+
import type { ProviderUsageState } from "./usage-state.ts";
|
|
4
|
+
import type { ModelResponse } from "./models.ts";
|
|
3
5
|
|
|
4
6
|
/** Consumers explicitly supply the connection and credentials required for the query. */
|
|
5
7
|
export interface UsageQueryContext {
|
|
@@ -33,20 +35,6 @@ export type UsageProvider = QueryUsageProvider | HeadersUsageProvider;
|
|
|
33
35
|
export class UsageQueryError extends Error {
|
|
34
36
|
constructor(readonly code: ProviderUsageError) { super(`Provider usage query failed: ${code}.`); }
|
|
35
37
|
}
|
|
36
|
-
export interface ModelProviderServices {
|
|
37
|
-
presets(): readonly ModelProviderPreset[];
|
|
38
|
-
list(): readonly ModelProviderInfo[];
|
|
39
|
-
get(id: string): ModelProviderInfo | null;
|
|
40
|
-
/** Backend only; reads the connection's current credentials, returning null for connections without authentication. */
|
|
41
|
-
credential(id: string, options?: { signal?: AbortSignal }): Promise<string | null>;
|
|
42
|
-
/** Connection or preset catalog changes; returns an unsubscribe function and unsubscribes automatically when the plugin closes. */
|
|
43
|
-
subscribe(listener: () => void): () => void;
|
|
44
|
-
}
|
|
45
|
-
export interface ModelProviderInfo extends ModelProviderConnection {
|
|
46
|
-
id: string;
|
|
47
|
-
name: string;
|
|
48
|
-
enabled: boolean;
|
|
49
|
-
}
|
|
50
38
|
export interface UsageProviderServices {
|
|
51
39
|
list(): readonly UsageProviderInfo[];
|
|
52
40
|
/** Obtain an implementation bound to the provider lifecycle; does not select connections, read credentials, or save results. */
|
|
@@ -58,3 +46,9 @@ export interface UsageServices {
|
|
|
58
46
|
query(providerId: string, context: UsageQueryContext): Promise<UsageItem[]>;
|
|
59
47
|
parseHeaders(providerId: string, context: UsageHeadersContext): UsageItem[] | null;
|
|
60
48
|
}
|
|
49
|
+
|
|
50
|
+
/** Host cache and shared queries; consumers supply response headers they obtained themselves. */
|
|
51
|
+
export interface ProviderUsage {
|
|
52
|
+
query(id: string, options?: { signal?: AbortSignal; refresh?: boolean }): Promise<ProviderUsageState>;
|
|
53
|
+
consume(response: ModelResponse): void;
|
|
54
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -7,8 +7,11 @@ export * from "./runtime-resources.ts";
|
|
|
7
7
|
export * from "./runtime-calls.ts";
|
|
8
8
|
export * from "./runtime-channels.ts";
|
|
9
9
|
export * from "./runtime-tools.ts";
|
|
10
|
-
export * from "./runtime-providers.ts";
|
|
10
|
+
export type * from "./runtime-model-providers.ts";
|
|
11
|
+
export * from "./runtime-usage.ts";
|
|
11
12
|
export * from "./runtime-web.ts";
|
|
13
|
+
export * from "./runtime-agent-catalog.ts";
|
|
14
|
+
export * from "./runtime-sessions.ts";
|
|
12
15
|
export type * from "./content-runtime.ts";
|
|
13
16
|
export type * from "./models.ts";
|
|
14
17
|
export { validationMessage, jsonArgumentHint } from "./tool-validation.ts";
|
|
@@ -42,3 +45,9 @@ export interface PluginTasks {
|
|
|
42
45
|
cancel(key: string): void;
|
|
43
46
|
has(key: string): boolean;
|
|
44
47
|
}
|
|
48
|
+
|
|
49
|
+
export type * from "./runtime-application.ts";
|
|
50
|
+
|
|
51
|
+
export * from "./runtime-shell.ts";
|
|
52
|
+
export * from "./runtime-terminal.ts";
|
|
53
|
+
export type * from "./runtime-mcp.ts";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineRecord } from "./records.ts";
|
|
3
|
+
import { presentationSchema } from "./presentation.ts";
|
|
4
|
+
|
|
5
|
+
/** Immutable title assignment; optional detail belongs to the plugin that produced it. */
|
|
6
|
+
export const SessionTitleAssigned = defineRecord("asterism.session/title", {
|
|
7
|
+
schema: z.strictObject({ title: z.string().trim().min(1).max(160), detail: presentationSchema.optional() }),
|
|
8
|
+
});
|
package/src/shell.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineCall } from "./calls.ts";
|
|
3
|
+
import { defineData } from "./presentation.ts";
|
|
4
|
+
|
|
5
|
+
export const OUTPUT_CHAR_LIMIT = 16_384;
|
|
6
|
+
export const MAX_TIMEOUT_SECONDS = 2_147_483_647 / 1000;
|
|
7
|
+
|
|
8
|
+
/** Startup arguments and command arguments are separate, allowing a profile to be shared across different shell usage modes. */
|
|
9
|
+
export const shellProfileSchema = z.strictObject({
|
|
10
|
+
id: z.string().trim().min(1).max(160).regex(/^[a-z0-9]+(?:[-_][a-z0-9]+)*$/u).refine(value => value !== "auto", "The profile ID 'auto' is reserved."),
|
|
11
|
+
executable: z.string().trim().min(1),
|
|
12
|
+
args: z.array(z.string()),
|
|
13
|
+
commandArgs: z.array(z.string()),
|
|
14
|
+
env: z.record(z.string(), z.string()),
|
|
15
|
+
});
|
|
16
|
+
export type ShellProfile = z.infer<typeof shellProfileSchema>;
|
|
17
|
+
|
|
18
|
+
export const shellPresets: ShellProfile[] = [
|
|
19
|
+
{ id: "pwsh", executable: "pwsh", args: [], commandArgs: ["-NonInteractive", "-Command"], env: {} },
|
|
20
|
+
{ id: "nu", executable: "nu", args: ["-l"], commandArgs: ["-c"], env: {} },
|
|
21
|
+
{ id: "fish", executable: "fish", args: [], commandArgs: ["-c"], env: {} },
|
|
22
|
+
{ id: "zsh", executable: "zsh", args: [], commandArgs: ["-c"], env: {} },
|
|
23
|
+
{ id: "bash", executable: "bash", args: [], commandArgs: ["-c"], env: {} },
|
|
24
|
+
{ id: "sh", executable: "sh", args: [], commandArgs: ["-c"], env: {} },
|
|
25
|
+
];
|
|
26
|
+
export const defaultShellSettings = { selected: "auto", profiles: shellPresets };
|
|
27
|
+
export const shellSettingsSchema = z.strictObject({
|
|
28
|
+
selected: z.string().min(1),
|
|
29
|
+
profiles: z.array(shellProfileSchema),
|
|
30
|
+
}).superRefine((value, ctx) => {
|
|
31
|
+
const ids = new Set<string>();
|
|
32
|
+
value.profiles.forEach((profile, index) => {
|
|
33
|
+
if (ids.has(profile.id)) ctx.addIssue({ code: "custom", path: ["profiles", index, "id"], message: "Shell profile IDs must be unique." });
|
|
34
|
+
ids.add(profile.id);
|
|
35
|
+
});
|
|
36
|
+
if (value.selected !== "auto" && !ids.has(value.selected))
|
|
37
|
+
ctx.addIssue({ code: "custom", path: ["selected"], message: "Select an existing shell profile or auto." });
|
|
38
|
+
});
|
|
39
|
+
export type ShellSettings = z.infer<typeof shellSettingsSchema>;
|
|
40
|
+
export const ShellProfilesCatalog = defineData("asterism.shell/profiles", shellSettingsSchema);
|
|
41
|
+
|
|
42
|
+
export const shellProfileListSchema = z.strictObject({
|
|
43
|
+
selected: z.string(),
|
|
44
|
+
resolvedProfile: z.string().nullable(),
|
|
45
|
+
profiles: z.array(z.strictObject({
|
|
46
|
+
id: z.string(),
|
|
47
|
+
executable: z.string(),
|
|
48
|
+
args: z.array(z.string()),
|
|
49
|
+
commandArgs: z.array(z.string()),
|
|
50
|
+
available: z.boolean(),
|
|
51
|
+
resolvedExecutable: z.string().nullable(),
|
|
52
|
+
})),
|
|
53
|
+
});
|
|
54
|
+
export type ShellProfileList = z.infer<typeof shellProfileListSchema>;
|
|
55
|
+
|
|
56
|
+
export const shellOutputSchema = z.strictObject({
|
|
57
|
+
output: z.string(),
|
|
58
|
+
truncation: z.strictObject({
|
|
59
|
+
totalChars: z.number().int().nonnegative(),
|
|
60
|
+
retainedChars: z.number().int().nonnegative(),
|
|
61
|
+
file: z.string(),
|
|
62
|
+
}).nullable(),
|
|
63
|
+
});
|
|
64
|
+
export type ShellOutput = z.infer<typeof shellOutputSchema>;
|
|
65
|
+
export const execResultSchema = shellOutputSchema.extend({
|
|
66
|
+
shell: z.string(),
|
|
67
|
+
termination: z.enum(["running", "exited", "timeout", "cancelled", "spawn_error", "io_error"]),
|
|
68
|
+
exitCode: z.number().int().nullable(),
|
|
69
|
+
signal: z.string().nullable(),
|
|
70
|
+
durationMs: z.number().nonnegative(),
|
|
71
|
+
});
|
|
72
|
+
export type ExecResult = z.infer<typeof execResultSchema>;
|
|
73
|
+
|
|
74
|
+
export const ReadShellProfiles = defineCall("core.shell.profiles.read", {
|
|
75
|
+
scope: "plugin", input: z.strictObject({}), output: shellProfileListSchema,
|
|
76
|
+
});
|
package/src/terminal.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const terminalStateSchema = z.enum(["inactive", "starting", "idle", "running", "unknown", "exited", "failed"]);
|
|
4
|
+
export type TerminalState = z.infer<typeof terminalStateSchema>;
|
|
5
|
+
export const terminalConfigSchema = z.strictObject({ cwd: z.string(), profile: z.string() });
|
|
6
|
+
export type TerminalConfig = z.infer<typeof terminalConfigSchema>;
|
|
7
|
+
|
|
8
|
+
export const terminalStatusSchema = z.strictObject({
|
|
9
|
+
state: terminalStateSchema, shell: z.string().nullable(), integration: z.boolean(),
|
|
10
|
+
exitCode: z.number().nullable(), error: z.string().nullable(),
|
|
11
|
+
/** Current directory reported by shell integration; null without integration. */
|
|
12
|
+
cwd: z.string().nullable(),
|
|
13
|
+
/** Exit code of the latest foreground command; null without integration or before any command has run. */
|
|
14
|
+
lastExitCode: z.number().nullable(),
|
|
15
|
+
});
|
|
16
|
+
export type TerminalStatus = z.infer<typeof terminalStatusSchema>;
|
|
17
|
+
export const inactiveStatus: TerminalStatus = { state: "inactive", shell: null, integration: false, exitCode: null, error: null, cwd: null, lastExitCode: null };
|
|
18
|
+
|
|
19
|
+
/** Terminal channel: the frontend sends input, dimensions, and rendered-character acknowledgments; the backend sends screen resets, output, dimensions, and status, all delivered in order. */
|
|
20
|
+
export type TerminalUp =
|
|
21
|
+
| { type: "input"; data: string }
|
|
22
|
+
/** xterm binary input (such as X10 mouse reports), with each character representing one byte. */
|
|
23
|
+
| { type: "binary"; data: string }
|
|
24
|
+
| { type: "resize"; cols: number; rows: number }
|
|
25
|
+
| { type: "ack"; chars: number }
|
|
26
|
+
/** The frontend screen has remounted and requests a current screen snapshot. */
|
|
27
|
+
| { type: "refresh" };
|
|
28
|
+
export type TerminalDown =
|
|
29
|
+
| { type: "reset"; cols: number; rows: number; data: string }
|
|
30
|
+
| { type: "output"; data: string }
|
|
31
|
+
| { type: "resize"; cols: number; rows: number }
|
|
32
|
+
| { type: "status"; status: TerminalStatus };
|
|
33
|
+
|
|
34
|
+
const cellSchema = z.strictObject({ text: z.string(), width: z.number(), fg: z.string(), bg: z.string(), bold: z.boolean(), italic: z.boolean(), underline: z.boolean(), strike: z.boolean() });
|
|
35
|
+
export const terminalImageSchema = z.strictObject({ cols: z.number(), rows: z.number(), cursor: z.strictObject({ x: z.number(), y: z.number(), visible: z.boolean() }), cells: z.array(z.array(cellSchema)) });
|
|
36
|
+
export type TerminalImage = z.infer<typeof terminalImageSchema>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineCall } from "./calls.ts";
|
|
3
|
+
import { defineEvent } from "./events.ts";
|
|
4
|
+
import { providerUsageIdSchema, usageItemsSchema, providerUsageErrorSchema } from "./provider-usage.ts";
|
|
5
|
+
|
|
6
|
+
/** Shared provider usage state; response consumption remains explicit. */
|
|
7
|
+
export const providerUsageStateSchema = z.strictObject({
|
|
8
|
+
providerId: z.string(), usage: providerUsageIdSchema.nullable(),
|
|
9
|
+
source: z.enum(["query", "headers"]).optional(), model: z.string().optional(),
|
|
10
|
+
state: z.enum(["disabled", "waiting", "loading", "ready", "error"]),
|
|
11
|
+
items: usageItemsSchema, updatedAt: z.number().nonnegative().nullable(), error: providerUsageErrorSchema.nullable(),
|
|
12
|
+
});
|
|
13
|
+
export type ProviderUsageState = z.infer<typeof providerUsageStateSchema>;
|
|
14
|
+
export const ReadUsage = defineCall("core.usage.read", {
|
|
15
|
+
scope: "plugin", input: z.strictObject({ providerId: z.string(), refresh: z.boolean() }), output: providerUsageStateSchema,
|
|
16
|
+
});
|
|
17
|
+
export const UsageChanged = defineEvent("core.usage.changed", {
|
|
18
|
+
schema: z.strictObject({ providerId: z.string().nullable() }),
|
|
19
|
+
source: "host", semantics: { kind: "snapshot", key: ["providerId"] },
|
|
20
|
+
});
|