@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
@@ -0,0 +1,174 @@
1
+ /** SDK renderer entry point: cards, tool views, and frontend capabilities bound to objects; React is shared by the host. */
2
+ import { createContext, createElement, useContext, type ComponentType, type ReactNode } from "react";
3
+ import type { UiText } from "./i18n.ts";
4
+ import type { CardAppearance } from "./card.ts";
5
+ export type { CardAppearance } from "./card.ts";
6
+ export * from "./renderer-i18n.ts";
7
+ import type { ActionContract, CreationContract } from "./commands.ts";
8
+ import type { Json } from "./json.ts";
9
+ import { reference, type ContractReference, type DataContract } from "./presentation.ts";
10
+ import type { ResourceRef, SessionSnapshot } from "./workspace.ts";
11
+ export * from "./renderer-services.ts";
12
+ export * from "./renderer-calls.ts";
13
+ export * from "./renderer-creation.ts";
14
+ export * from "./renderer-tools.ts";
15
+ export * from "./renderer-records.ts";
16
+ export * from "./renderer-card.ts";
17
+ export * from "./renderer-island.ts";
18
+ export * from "./renderer-view-state.ts";
19
+ import type { CardPanelDefinition, CardPanelView } from "./renderer-card.ts";
20
+ import type { RecordRenderer } from "./renderer-records.ts";
21
+ export type * from "./renderer-content.ts";
22
+ import type { RendererContentInput, RendererInputOptions } from "./renderer-content.ts";
23
+ import type { ContentReader } from "./content-runtime.ts";
24
+ export type { ContentReader } from "./content-runtime.ts";
25
+ import type { CreationEntry } from "./renderer-creation.ts";
26
+ import type { ToolRenderer } from "./renderer-tools.ts";
27
+ import type { EventSubscriber } from "./events.ts";
28
+ import type { PluginCalls } from "./calls.ts";
29
+ import type { RendererCalls } from "./renderer-calls.ts";
30
+ import type { ChannelContract, ChannelEnd, ChannelValue } from "./channels.ts";
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. */
33
+ export interface RendererChannel<Up extends ChannelValue = ChannelValue, Down extends ChannelValue = ChannelValue> {
34
+ send(data: Up): void;
35
+ onMessage(listener: (data: Down) => void): () => void;
36
+ close(): void;
37
+ readonly closed: Promise<ChannelEnd>;
38
+ }
39
+
40
+ export type RendererReceipt = { ok: true } | { ok: false; error: { code: string; message: string; display?: UiText } };
41
+ /**
42
+ * Frontend state retained per object, such as input drafts: survives card remounts, switches between focus containers, and session view disposal;
43
+ * release is called when the object leaves the current session projection through removal or a cursor jump, or is explicitly discarded; state is not restored after application restart.
44
+ */
45
+ export interface RendererRetained {
46
+ /** Obtain the value for key, creating it with create on first access; the same object and key return the same instance. Prefix key with the plugin ID to avoid conflicts with other contributors. */
47
+ get<T>(key: string, create: () => T, release?: (value: T) => void | Promise<void>): T;
48
+ /** End retention immediately and call release; no effect if absent. */
49
+ discard(key: string): void;
50
+ }
51
+ /** Frontend capabilities bound to a card object; the host handles subscriptions, command gating, and current-view consistency. */
52
+ export interface RendererObjectContext {
53
+ readonly ref: ResourceRef;
54
+ readonly content: ContentReader;
55
+ input<I, O extends Json>(contract: ActionContract<I, O> | CreationContract<I, Json, O>): RendererContentInput;
56
+ readonly view: {
57
+ /** The current object view is operable; ordinary background refreshes and local commits on other objects do not change this value. */
58
+ ready: boolean;
59
+ /** View read error; object action and creation failures are returned through their respective call receipts. */
60
+ error: UiText | null; missing: boolean;
61
+ };
62
+ readonly session: {
63
+ snapshot(): SessionSnapshot | null;
64
+ subscribe(listener: () => void): () => void;
65
+ create<I, O extends Json>(contract: CreationContract<I, Json, O>, input: NoInfer<I> & Json, position: { x: number; y: number }, options?: RendererInputOptions): Promise<RendererReceipt>;
66
+ refresh(): Promise<void>;
67
+ };
68
+ invoke<I, O extends Json>(contract: ActionContract<I, O>, input: NoInfer<I> & Json, options?: RendererInputOptions): Promise<RendererReceipt>;
69
+ /** Open a duplex channel for this object; the host validates that it is on the current path and routes to its owning plugin. Ends when the object leaves the current set or the connection disconnects. */
70
+ openChannel<Up extends ChannelValue, Down extends ChannelValue>(contract: ChannelContract<Up, Down>): RendererChannel<Up, Down>;
71
+ /** Object-level retained state with a lifecycle managed uniformly by the host. */
72
+ readonly retained: RendererRetained;
73
+ }
74
+ const ObjectContext = createContext<RendererObjectContext | null>(null);
75
+ export function useRendererObject(): RendererObjectContext {
76
+ const object = useContext(ObjectContext);
77
+ if (!object) throw new Error("The renderer must be bound to an object.");
78
+ return object;
79
+ }
80
+
81
+ /** Static identification information at the type level; does not parse card data or create body components. */
82
+ export interface ResourceDescription {
83
+ kind: string;
84
+ label: UiText;
85
+ icon: ReactNode;
86
+ /** Default card dimensions available before creation, sharing a declaration with the card renderer. */
87
+ card?: CardAppearance;
88
+ }
89
+ export interface CardView extends CardAppearance {
90
+ body: ReactNode;
91
+ actions?: ReactNode;
92
+ panels?: readonly CardPanelView[];
93
+ }
94
+ export interface CardRenderer {
95
+ contract: ContractReference;
96
+ resolve(data: Json, context?: RendererObjectContext): CardView | null;
97
+ }
98
+ export interface RendererModule {
99
+ resources: ResourceDescription[];
100
+ cards: CardRenderer[];
101
+ tools: ToolRenderer[];
102
+ /** History record views: declare summaries, icons, and optional row content by record contract. */
103
+ records?: RecordRenderer[];
104
+ /** Creation entry points: the host arbitrates menus and drag-and-drop uniformly, with all paths ultimately using the bound creation contract. */
105
+ creations?: CreationEntry[];
106
+ /** Startup preparation: the host invokes modules in parallel before the first screen, and modules initialize their own internal resources; timeouts or failures are logged without blocking startup. */
107
+ prepare?(context: RendererPrepareContext): Promise<void>;
108
+ }
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" }>;
116
+ /** A directly addressable location within a page; an empty location means the root page. */
117
+ locations?: readonly string[];
118
+ Body: ComponentType<{ location?: string }>;
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
+ }
130
+ /** Frontend plugins own settings pages and shared service lifecycles; feature views are composed from modules. */
131
+ export interface RendererPlugin {
132
+ modules?: readonly RendererModule[];
133
+ settings?: readonly PluginSettingsPage[];
134
+ sessionEntries?: readonly PluginSessionEntry[];
135
+ prepare?(context: RendererPrepareContext): Promise<void>;
136
+ closing?(): void | Promise<void>;
137
+ }
138
+ export function defineRendererPlugin(plugin: RendererPlugin): RendererPlugin { return plugin; }
139
+ export interface RendererPrepareContext {
140
+ /** Typed calls for frontend module-level services; cancellation propagates with the preparation scope. */
141
+ readonly calls: RendererCalls;
142
+ /** Global subscriptions bound to the module lifecycle, cleaned up on failure, timeout, or application shutdown. */
143
+ readonly events: EventSubscriber;
144
+ /** Triggered when the host stops waiting due to timeout or shutdown; the module should stop promptly. */
145
+ readonly signal: AbortSignal;
146
+ }
147
+
148
+ /** Type erasure occurs only at the registration boundary; components receive data that has passed contract validation. */
149
+ export function cardRenderer<T extends Json>(
150
+ contract: DataContract<T>,
151
+ definition: CardAppearance & {
152
+ Body: ComponentType<{ data: T }>;
153
+ Actions?: ComponentType<{ data: T }>;
154
+ panels?: readonly CardPanelDefinition<T>[];
155
+ },
156
+ ): CardRenderer {
157
+ const { Body, Actions, panels, ...appearance } = definition;
158
+ const ids = new Set<string>();
159
+ for (const panel of panels ?? []) {
160
+ if (!panel.id.trim() || panel.id !== panel.id.trim()) throw new Error("Card panel IDs must be non-empty and trimmed.");
161
+ if (ids.has(panel.id)) throw new Error(`Duplicate card panel ID: ${panel.id}`);
162
+ ids.add(panel.id);
163
+ }
164
+ return { contract: reference(contract), resolve(raw, context) {
165
+ const parsed = contract.schema.safeParse(raw);
166
+ if (!parsed.success) return null;
167
+ const bind = (node: ReactNode) => context ? createElement(ObjectContext.Provider, { value: context }, node) : node;
168
+ return { ...appearance, body: bind(createElement(Body, { data: parsed.data })),
169
+ actions: Actions ? bind(createElement(Actions, { data: parsed.data })) : undefined,
170
+ panels: panels?.map(({ Body: PanelBody, ...panel }) => ({
171
+ ...panel, body: bind(createElement(PanelBody, { data: parsed.data })),
172
+ })) };
173
+ } };
174
+ }
@@ -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 }
@@ -0,0 +1,22 @@
1
+ import type { CallContract, CallScope } from "./calls.ts";
2
+ import type { z } from "zod";
3
+ import type { Json } from "./json.ts";
4
+ import type { ObjectReader } from "./objects.ts";
5
+ import type { Source } from "./workspace.ts";
6
+ import type { ContentReader } from "./content-runtime.ts";
7
+
8
+ export type CallContext<S extends CallScope = CallScope> = {
9
+ /** The host binds the source for backend plugin calls; null for desktop user calls. */
10
+ readonly source: Source;
11
+ readonly signal: AbortSignal;
12
+ readonly content: ContentReader;
13
+ } & (S extends "object" ? { readonly object: ObjectReader } : { readonly object?: never });
14
+ export interface CallContribution {
15
+ readonly contract: CallContract;
16
+ execute(input: Json, context: CallContext): Promise<Json>;
17
+ }
18
+ /** Implementations receive parsed input and return JSON accepted by the output schema before caller-side decoding. */
19
+ export function implementCall<Input extends z.ZodType<Json>, Output extends z.ZodType<Json>, S extends CallScope>(contract: CallContract<Input, Output, S>,
20
+ execute: (input: NoInfer<z.output<Input>>, context: CallContext<NoInfer<S>>) => Promise<NoInfer<z.input<Output>> & Json>): CallContribution {
21
+ return { contract, execute: (input, context) => execute(input as z.output<Input>, context as CallContext<S>) };
22
+ }
@@ -0,0 +1,24 @@
1
+ import type { ChannelContract, ChannelEnd, ChannelValue } from "./channels.ts";
2
+ import type { ObjectReader } from "./objects.ts";
3
+
4
+ /**
5
+ * The backend end of a channel. Messages are delivered in send order; messages arriving before listener registration are buffered and delivered in order when the first listener registers.
6
+ * signal aborts when either end closes, the object leaves the current set, the plugin shuts down, or the connection disconnects; reason is ChannelEnd.
7
+ */
8
+ export interface ObjectChannel<Up extends ChannelValue = ChannelValue, Down extends ChannelValue = ChannelValue> {
9
+ readonly object: ObjectReader;
10
+ readonly signal: AbortSignal;
11
+ send(data: Down): void;
12
+ onMessage(listener: (data: Up) => void): () => void;
13
+ close(): void;
14
+ }
15
+ export interface ChannelContribution {
16
+ readonly contract: ChannelContract;
17
+ /** Called when the frontend opens a channel; throwing PluginApiError ends the channel with unavailable. */
18
+ open(channel: ObjectChannel): void | Promise<void>;
19
+ }
20
+ export function implementChannel<Up extends ChannelValue, Down extends ChannelValue>(contract: ChannelContract<Up, Down>,
21
+ open: (channel: ObjectChannel<Up, Down>) => void | Promise<void>): ChannelContribution {
22
+ return { contract, open: channel => open(channel as unknown as ObjectChannel<Up, Down>) };
23
+ }
24
+ export type { ChannelEnd };
@@ -0,0 +1,207 @@
1
+ /** Backend interfaces shared by plugins and the host; the host implements storage, transactions, and authorization. */
2
+ import type { UiText } from "./i18n.ts";
3
+ import type { CardAppearance } from "./card.ts";
4
+ import type { ConfigContract } from "./config.ts";
5
+ import type { DatabaseSync } from "node:sqlite";
6
+ import type { Json } from "./json.ts";
7
+ import type { PluginNamespace } from "./plugin.ts";
8
+ import type { ResourceContract, ImplementationContract } from "./module-contract.ts";
9
+ import type { DataContract, Presentation } from "./presentation.ts";
10
+ import type { RecordContract, SessionRecord } from "./records.ts";
11
+ import type { EventContract, PluginEvents } from "./events.ts";
12
+ import type { ToolCompletion, ToolInvocationInfo, ToolModelOutput } from "./tool-result.ts";
13
+ import type { PluginConfig, PluginLogger, PluginSecrets, PluginTasks } from "./runtime.ts";
14
+ import type { ModelServices } from "./models.ts";
15
+ import type { ResourceSnapshot, SessionSnapshot, SessionDirectory } from "./workspace.ts";
16
+ import type { ActionContract, CreationContract } from "./commands.ts";
17
+ import type { ObjectReader, ObjectExecutor, SessionExecutor } from "./objects.ts";
18
+ import type { ContentReader, ContentScope } from "./content-runtime.ts";
19
+ import type { PluginCalls } from "./calls.ts";
20
+ import type { CallContribution } from "./runtime-calls.ts";
21
+ import type { ChannelContribution } from "./runtime-channels.ts";
22
+ import type { TopLevelTool } from "./runtime-tools.ts";
23
+ import type { ModelProviderServices } from "./runtime-model-providers.ts";
24
+ import type { UsageProviderServices, UsageProvider, UsageServices, ProviderUsage } from "./runtime-usage.ts";
25
+ import type { WebServices, WebProviderServices, WebProviderDefinition } from "./runtime-web.ts";
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";
29
+ export type * from "./objects.ts";
30
+ export type { ResourceRef, ResourceCreationResult, ResourceSnapshot, SessionSnapshot } from "./workspace.ts";
31
+
32
+ /**
33
+ * Lifecycle scope: on termination, abort signal first, then await all cleanup in reverse registration order.
34
+ * The host maintains application, session, and object scopes; child scopes end before their parent. Exit, archival, and object removal share the same cleanup path.
35
+ */
36
+ export interface Lifetime {
37
+ readonly signal: AbortSignal;
38
+ /** Register cleanup; throws unavailable if the scope has ended. Cleanup failures are only logged and do not block remaining cleanup. */
39
+ defer(cleanup: () => void | Promise<void>): void;
40
+ }
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;
46
+ readonly id: string;
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. */
48
+ readonly lifetime: Lifetime;
49
+ readonly sessions: SessionDirectory;
50
+ readonly createSession: SessionCreation;
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;
55
+ readonly log: PluginLogger;
56
+ readonly ipc: { readonly events: PluginEvents; readonly calls: PluginCalls };
57
+ config<T extends Json>(contract: ConfigContract<T>): PluginConfig<T>;
58
+ readonly secrets: PluginSecrets;
59
+ db(): DatabaseSync;
60
+ readonly tasks: PluginTasks;
61
+ readonly models: ModelServices;
62
+ readonly modelProviders: ModelProviderServices;
63
+ readonly usageProviders: UsageProviderServices;
64
+ readonly usages: UsageServices;
65
+ readonly providerUsage: ProviderUsage;
66
+ readonly webProviders: WebProviderServices;
67
+ readonly web: WebServices;
68
+ session(id: string): SessionExecutor;
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. */
70
+ object<S extends Json>(sessionId: string, contract: ResourceContract<S>, id: string): ObjectExecutor;
71
+ }
72
+ export interface SessionCommitted {
73
+ readonly snapshot: SessionSnapshot;
74
+ readonly records: readonly SessionRecord[];
75
+ readonly session: SessionExecutor;
76
+ }
77
+ /** Fixed snapshot after the execution cursor changes; ordinary appends and explicit cursor selection use the same shape, while panel candidate selection does not trigger it. */
78
+ export interface SessionCursorChanged {
79
+ readonly snapshot: SessionSnapshot;
80
+ readonly session: SessionExecutor;
81
+ }
82
+ export interface SessionObserver {
83
+ readonly id: string;
84
+ committed?(event: SessionCommitted): undefined;
85
+ cursorChanged?(event: SessionCursorChanged): undefined;
86
+ }
87
+ export type Preparation<C extends Json = Json> =
88
+ | { action: "accept" }
89
+ | { action: "replace"; input: C }
90
+ | { action: "reject"; code: string; message: string; display?: UiText };
91
+ /** Preparation context: the resource's own preparation can read and write content in this scope, while external preparers can only read; neither modifies session data. */
92
+ export interface CreationPreparation { readonly signal: AbortSignal; readonly content: ContentScope }
93
+ export interface ActionPreparation extends CreationPreparation { readonly object: ObjectReader }
94
+ export interface ExternalPreparation { readonly signal: AbortSignal; readonly object: ObjectReader; readonly content: ContentReader }
95
+ export interface CommandPreparer<O extends Json = Json, I = unknown> {
96
+ readonly id: string;
97
+ readonly contract: ActionContract<I, O>;
98
+ prepare(input: O, context: ExternalPreparation): Preparation<I & Json> | Promise<Preparation<I & Json>>;
99
+ }
100
+ export interface ContextFile { readonly file: string; readonly text: string }
101
+ export interface CreationContribution {
102
+ readonly contract: CreationContract<unknown>;
103
+ prepare?(input: Json, context: CreationPreparation): Preparation | Promise<Preparation>;
104
+ create(input: Json, object: ObjectExecutor): { title: string; data: Json };
105
+ }
106
+ export interface ActionContribution {
107
+ readonly contract: ActionContract<unknown>;
108
+ prepare?(input: Json, context: ActionPreparation): Preparation | Promise<Preparation>;
109
+ execute(input: Json, object: ObjectExecutor, signal: AbortSignal): Promise<void>;
110
+ }
111
+ export interface ResourceContribution {
112
+ readonly resource: ResourceContract;
113
+ /** Geometry declarations for backend placement, shared with ResourceDescription and cardRenderer. */
114
+ readonly card?: CardAppearance;
115
+ readonly records: readonly RecordContract[];
116
+ /** Opt into templates that copy encoded resource state and only these current-data contracts, preserving their values under new host identities. */
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
+ };
122
+ /** Declare that this resource can receive links and provide the recipient's tool policy; independent of Agent domain types. */
123
+ readonly links?: { receive: boolean; tools?(object: ObjectReader): { allow?: readonly string[]; exclude?: readonly string[] } };
124
+ readonly creation?: CreationContribution;
125
+ readonly actions?: readonly ActionContribution[];
126
+ project(resource: ResourceSnapshot, object: ObjectReader): Presentation;
127
+ inspect(resource: ResourceSnapshot, object: ObjectReader): Promise<Json>;
128
+ context?(resource: ResourceSnapshot, object: ObjectReader): Promise<ContextFile[]>;
129
+ /** Explanation presented to users before ending the current instance; does not block backend removal or wait for external cleanup. */
130
+ disposalWarning?(resource: ResourceSnapshot, object: ObjectReader): UiText | null;
131
+ /**
132
+ * Called synchronously when an object enters the current set through startup, creation, restoration, or rewinding that makes it reappear; lifetime ends when the object leaves the current set,
133
+ * its session is archived, or the application exits. Register cleanup here for external instances owned exclusively by the object.
134
+ */
135
+ attach?(resource: ResourceSnapshot, object: ObjectReader, lifetime: Lifetime): void;
136
+ }
137
+ type OptionalData<T, D> = T extends unknown ? Omit<T, "data"> & { data?: D | null } : never;
138
+ export type CallResult<D extends Json = Json> =
139
+ | { kind: "succeeded"; data: D }
140
+ | { kind: "partial"; data: D; message: string }
141
+ | OptionalData<Exclude<ToolCompletion, { kind: "succeeded" | "partial" }>, D>;
142
+ export type PreparedResult = CallResult & ToolInvocationInfo;
143
+ /** Tool caller identity captured at the request boundary; carries neither private resource state nor operation permissions. */
144
+ export type ToolCaller = Readonly<Pick<ResourceSnapshot, "ref" | "title">>;
145
+ export interface ToolInvocation extends ToolInvocationInfo {
146
+ effect: "read" | "write";
147
+ execution: "parallel" | "sequential";
148
+ execute(signal: AbortSignal, publish?: (data: Json) => void): Promise<CallResult>;
149
+ toModel?(data: Json): ToolModelOutput;
150
+ }
151
+ export interface ToolContribution {
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;
155
+ prepare(resource: ResourceSnapshot, object: ObjectExecutor, target: Json, args: Json, caller: ToolCaller): ToolInvocation | PreparedResult;
156
+ }
157
+ export interface ModuleContribution {
158
+ agentSkills?: readonly AgentSkillDeclaration[];
159
+ agentInstructions?: readonly AgentInstructionDeclaration[];
160
+ webProviders?: readonly WebProviderDefinition[];
161
+ usageProviders?: readonly UsageProvider[];
162
+ modelProviderPresets?: readonly ModelProviderPresetContribution[];
163
+ calls?: readonly CallContribution[];
164
+ /** Object-level duplex channel; the contract belongs to this plugin, and routing follows the plugin owning the object type. */
165
+ channels?: readonly ChannelContribution[];
166
+ topLevelTools?: readonly TopLevelTool[];
167
+ events?: readonly EventContract[];
168
+ records?: readonly RecordContract[];
169
+ resources?: readonly ResourceContribution[];
170
+ tools?: readonly ToolContribution[];
171
+ observers?: readonly SessionObserver[];
172
+ preparers?: readonly CommandPreparer[];
173
+ }
174
+ /** Named module entry point: plugins provide contributions, and the host handles assembly and invocation. */
175
+ export interface PluginModule { setup(plugin: PluginContext): ModuleContribution }
176
+ /** Composition of shared plugin services and feature modules; shared services are established first and released after the modules. */
177
+ export interface PluginContribution extends ModuleContribution {
178
+ modules?: readonly PluginModule[];
179
+ }
180
+ /**
181
+ * Format declarations for persistent plugin files; paths are relative to extensions/<pluginId>, and migration key n means n → n+1.
182
+ * Versions are stamped by the writer: JSON uses a `{ version, data }` envelope, and SQLite uses `PRAGMA user_version`.
183
+ */
184
+ export type PluginStorageDeclaration = {
185
+ kind: "json"; path: string; version: number;
186
+ migrations: Readonly<Record<number, (data: unknown) => Json>>;
187
+ } | {
188
+ kind: "sqlite"; path: string; version: number;
189
+ migrations: Readonly<Record<number, (db: DatabaseSync) => void>>;
190
+ };
191
+ /** Default export of the storage entry point (manifest `asterism.storage`); the host imports it separately before the plugin runtime. */
192
+ export function defineStorage(declarations: readonly PluginStorageDeclaration[]): readonly PluginStorageDeclaration[] {
193
+ for (const item of declarations) if (!Number.isSafeInteger(item.version) || item.version < 1)
194
+ throw new Error(`Storage ${item.path} must declare a positive integer version.`);
195
+ return Object.freeze([...declarations]);
196
+ }
197
+ export interface PluginDefinition {
198
+ namespace: PluginNamespace; modules?: readonly PluginModule[];
199
+ setup?(plugin: PluginContext): PluginContribution;
200
+ }
201
+ export function defineModule(setup: PluginModule["setup"]): PluginModule {
202
+ if (typeof setup !== "function") throw new Error("A plugin module must declare a setup function.");
203
+ return { setup };
204
+ }
205
+ export function definePlugin(definition: PluginDefinition): PluginDefinition & { modules: readonly PluginModule[] } {
206
+ return { ...definition, modules: [...definition.modules ?? []] };
207
+ }
@@ -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,117 @@
1
+ import type { ActionContract, CreationContract } from "./commands.ts";
2
+ import type { z } from "zod";
3
+ import { jsonSchema, type Json } from "./json.ts";
4
+ import { defaultToolModel, type ToolModelOutput } from "./tool-result.ts";
5
+ import type { ResourceContract, ImplementationContract } from "./module-contract.ts";
6
+ import type { RecordContract } from "./records.ts";
7
+ import { reference, present, type DataContract } from "./presentation.ts";
8
+ import { PluginApiError } from "./runtime.ts";
9
+ import type { ContextFile, ObjectReader, ObjectExecutor, CreationContribution, ActionContribution, CommandPreparer, CreationPreparation, ActionPreparation, ExternalPreparation, ResourceContribution, ToolContribution, Preparation, CallResult, Lifetime } from "./runtime-contracts.ts";
10
+ import { validationMessage, jsonArgumentHint } from "./tool-validation.ts";
11
+ import type { ToolCaller } from "./runtime-contracts.ts";
12
+ import { encodeJson } from "./data-encoding.ts";
13
+
14
+ function parse<T>(schema: z.ZodType<T>, input: unknown, subject: string): T {
15
+ const result = schema.safeParse(input);
16
+ if (!result.success) throw new PluginApiError("invalid_argument", validationMessage(result.error, subject));
17
+ return result.data;
18
+ }
19
+ /** Creation and actions are invoked after host validation; types are erased only during contribution assembly. */
20
+ export function implementCreation<I, S extends Json, O extends Json>(contract: CreationContract<I, S, O>, definition: {
21
+ prepare?(input: NoInfer<O>, context: CreationPreparation): Preparation<NoInfer<I> & Json> | Promise<Preparation<NoInfer<I> & Json>>;
22
+ create(input: NoInfer<O>, object: ObjectExecutor): { title: string; data: NoInfer<S> };
23
+ }): CreationContribution {
24
+ return { contract, ...definition } as CreationContribution;
25
+ }
26
+ export function implementAction<I, O extends Json>(contract: ActionContract<I, O>, definition: {
27
+ prepare?(input: NoInfer<O>, context: ActionPreparation): Preparation<NoInfer<I> & Json> | Promise<Preparation<NoInfer<I> & Json>>;
28
+ execute(input: NoInfer<O>, object: ObjectExecutor, signal: AbortSignal): Promise<void>;
29
+ }): ActionContribution {
30
+ return { contract, ...definition } as ActionContribution;
31
+ }
32
+ export function implementPreparer<I, O extends Json>(contract: ActionContract<I, O>, definition: {
33
+ id: string;
34
+ prepare(input: NoInfer<O>, context: ExternalPreparation): Preparation<NoInfer<I> & Json> | Promise<Preparation<NoInfer<I> & Json>>;
35
+ }): CommandPreparer {
36
+ return { contract, ...definition } as CommandPreparer;
37
+ }
38
+ export function implementResource<S extends Json, V extends Json>(
39
+ resource: ResourceContract<S>, view: DataContract<V>, definition: {
40
+ card?: ResourceContribution["card"];
41
+ records?: readonly RecordContract[];
42
+ template?: ResourceContribution["template"];
43
+ links?: ResourceContribution["links"];
44
+ creation?: CreationContribution;
45
+ actions?: readonly ActionContribution[];
46
+ project(data: NoInfer<S>, object: ObjectReader): NoInfer<V>;
47
+ inspect(data: NoInfer<S>, object: ObjectReader): Promise<Json>;
48
+ context?(data: NoInfer<S>, object: ObjectReader): Promise<ContextFile[]>;
49
+ disposalWarning?(data: NoInfer<S>, object: ObjectReader): import("./i18n.ts").UiText | null;
50
+ /** Called when an object enters the current set; external instances owned exclusively by the object register cleanup with lifetime. */
51
+ attach?(data: NoInfer<S>, object: ObjectReader, lifetime: Lifetime): void;
52
+ },
53
+ ): ResourceContribution {
54
+ const attach = definition.attach;
55
+ return {
56
+ resource, card: definition.card, records: [...definition.records ?? []], links: definition.links, template: definition.template,
57
+ creation: definition.creation, actions: definition.actions,
58
+ project: (snapshot, object) => present(view, definition.project(parse(resource.stateSchema, snapshot.data, "resource"), object)),
59
+ inspect: async (snapshot, object) => jsonSchema.parse(await definition.inspect(parse(resource.stateSchema, snapshot.data, "resource"), object)),
60
+ context: async (snapshot, object) => definition.context?.(parse(resource.stateSchema, snapshot.data, "resource"), object) ?? [],
61
+ disposalWarning: (snapshot, object) => definition.disposalWarning?.(parse(resource.stateSchema, snapshot.data, "resource"), object) ?? null,
62
+ ...(attach ? { attach: (snapshot: Parameters<NonNullable<ResourceContribution["attach"]>>[0], object: ObjectReader, lifetime: Lifetime) =>
63
+ attach(parse(resource.stateSchema, snapshot.data, "resource"), object, lifetime) } : {}),
64
+ };
65
+ }
66
+ export function implementTool<S extends Json, T extends Json, A extends Json, D extends Json>(
67
+ contract: ImplementationContract<S, T, A, D>, definition: {
68
+ call(data: S, target: T, args: A, context: {
69
+ object: ObjectExecutor; caller: ToolCaller; signal: AbortSignal; publish(data: NoInfer<D>): void;
70
+ }): Promise<CallResult<NoInfer<D>>>;
71
+ toModel?(data: D): ToolModelOutput;
72
+ },
73
+ ): ToolContribution {
74
+ const identity = reference(contract);
75
+ return {
76
+ contract: contract as unknown as ToolContribution["contract"],
77
+ prepare(resource, object, target, args, caller) {
78
+ const state = contract.resource.stateSchema.safeParse(resource.data);
79
+ if (!state.success) return { kind: "failed", implementation: identity,
80
+ error: { code: "invalid_resource", message: "The resource state does not match its backend contract." } };
81
+ const parsedTarget = contract.targetSchema.safeParse(target);
82
+ const parsedArgs = contract.tool.argsSchema.safeParse(args);
83
+ if (!parsedTarget.success || !parsedArgs.success) return { kind: "rejected", implementation: identity,
84
+ error: { code: "invalid_arguments", message: [
85
+ `Invalid arguments for ${contract.resource.kind}/${contract.tool.name}.`,
86
+ ...(!parsedTarget.success ? [validationMessage(parsedTarget.error, "target")] : []),
87
+ ...(!parsedArgs.success ? [validationMessage(parsedArgs.error, "args")] : []),
88
+ jsonArgumentHint,
89
+ ].join("\n") } };
90
+ return {
91
+ effect: contract.effect, execution: contract.execution, implementation: identity,
92
+ // Model projections consume decoded values.
93
+ toModel: data => {
94
+ const decoded = contract.schema.parse(data);
95
+ return definition.toModel ? definition.toModel(decoded) : defaultToolModel(decoded);
96
+ },
97
+ async execute(signal, publish) {
98
+ let active = true;
99
+ const snapshot = (data: Json) => {
100
+ const encoded = encodeJson<Json>(contract.schema, data, contract.id);
101
+ if (encoded === null && data !== null)
102
+ throw new PluginApiError("invalid_argument", `${contract.id}: non-null tool results require a non-null encoding; null denotes absent output.`);
103
+ return encoded;
104
+ };
105
+ try {
106
+ const result = await definition.call(state.data, parsedTarget.data, parsedArgs.data, { object, caller, signal,
107
+ publish(data) { if (active) publish?.(snapshot(data)); } });
108
+ if (result.kind === "succeeded" || result.kind === "partial") {
109
+ if (result.data === undefined) throw new PluginApiError("invalid_argument", "The tool did not provide final data.");
110
+ } else if (result.data === undefined || result.data === null) return result;
111
+ return { ...result, data: snapshot(result.data) };
112
+ } finally { active = false; }
113
+ },
114
+ };
115
+ },
116
+ };
117
+ }