@asterism-app/sdk 0.0.0-stage → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -0
- package/README.md +20 -2
- package/docs/data-and-lifecycle.md +77 -0
- package/docs/getting-started.md +37 -0
- package/docs/manual.md +34 -0
- package/docs/minimal-plugin.md +33 -0
- package/docs/project-structure.md +27 -0
- package/docs/resources-and-tools.md +29 -0
- package/docs/views-and-settings.md +35 -0
- package/examples/author.ts +158 -0
- package/examples/consume-usage.ts +29 -0
- package/examples/content-author.ts +29 -0
- package/examples/content-contract.ts +10 -0
- package/examples/content-renderer.ts +26 -0
- package/examples/counter/docs/usage.md +9 -0
- package/examples/counter/package.json +27 -0
- package/examples/counter/src/contracts.ts +31 -0
- package/examples/counter/src/panel.module.css +2 -0
- package/examples/counter/src/renderer.tsx +69 -0
- package/examples/counter/src/runtime.ts +43 -0
- package/examples/counter/src/storage.ts +4 -0
- package/examples/counter/src/styles.d.ts +4 -0
- package/examples/counter/tsconfig.json +16 -0
- package/examples/counter-contract.ts +18 -0
- package/examples/data-codecs.ts +18 -0
- package/examples/events.ts +37 -0
- package/examples/i18n.ts +18 -0
- package/examples/model-completion.ts +15 -0
- package/examples/model-input.ts +16 -0
- package/examples/plugin-calls.ts +22 -0
- package/examples/provider-usage-headers.ts +20 -0
- package/examples/provider-usage.ts +34 -0
- package/examples/renderer.ts +47 -0
- package/examples/session-resources.ts +29 -0
- package/examples/settings-contract.ts +8 -0
- package/examples/settings-renderer.ts +30 -0
- package/examples/settings-runtime.ts +11 -0
- package/examples/storage.ts +26 -0
- package/examples/tool-renderer.ts +47 -0
- package/examples/web-provider.ts +27 -0
- package/package.json +39 -4
- package/src/calls.ts +27 -0
- package/src/card.ts +7 -0
- package/src/channels.ts +20 -0
- package/src/commands.ts +28 -0
- package/src/config.ts +25 -0
- package/src/content-runtime.ts +11 -0
- package/src/content.ts +6 -0
- package/src/contracts.ts +29 -0
- package/src/data-encoding.ts +14 -0
- package/src/error-details.ts +78 -0
- package/src/events.ts +91 -0
- package/src/i18n.ts +58 -0
- package/src/json.ts +4 -0
- package/src/model-cost-estimate.ts +49 -0
- package/src/model-cost.ts +44 -0
- package/src/model-input.ts +19 -0
- package/src/model-media.ts +11 -0
- package/src/model-records.ts +56 -0
- package/src/model-selection.ts +34 -0
- package/src/models.ts +137 -0
- package/src/module-contract.ts +44 -0
- package/src/objects.ts +89 -0
- package/src/plugin.ts +58 -0
- package/src/presentation.ts +21 -0
- package/src/provider-usage.ts +87 -0
- package/src/records.ts +60 -0
- package/src/renderer-calls.ts +11 -0
- package/src/renderer-card.ts +29 -0
- package/src/renderer-content.ts +9 -0
- package/src/renderer-creation.ts +83 -0
- package/src/renderer-i18n.ts +49 -0
- package/src/renderer-island.ts +66 -0
- package/src/renderer-records.ts +49 -0
- package/src/renderer-services.ts +75 -0
- package/src/renderer-tools.ts +103 -0
- package/src/renderer-view-state.ts +33 -0
- package/src/renderer.ts +158 -0
- package/src/runtime-calls.ts +20 -0
- package/src/runtime-channels.ts +24 -0
- package/src/runtime-contracts.ts +188 -0
- package/src/runtime-providers.ts +60 -0
- package/src/runtime-resources.ts +117 -0
- package/src/runtime-tools.ts +31 -0
- package/src/runtime-web.ts +36 -0
- package/src/runtime.ts +44 -0
- package/src/tool-result.ts +45 -0
- package/src/tool-validation.ts +14 -0
- package/src/web.ts +75 -0
- package/src/workspace.ts +43 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Shared payloads for model execution; consuming plugins declare RecordContract identities, and the SDK does not create a separate history. */
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { jsonSchema, type Json } from "./json.ts";
|
|
4
|
+
import { toolOutcomeSchema } from "./tool-result.ts";
|
|
5
|
+
import { presentationSchema, type DataContract } from "./presentation.ts";
|
|
6
|
+
import type { ModelCompletion } from "./models.ts";
|
|
7
|
+
import { errorDetailsSchema } from "./error-details.ts";
|
|
8
|
+
|
|
9
|
+
const id = z.string().min(1).max(160);
|
|
10
|
+
export const modelUsageSchema = z.strictObject({
|
|
11
|
+
input: z.number().nonnegative().nullable(), output: z.number().nonnegative().nullable(),
|
|
12
|
+
total: z.number().nonnegative().nullable(), reasoning: z.number().nonnegative().nullable(),
|
|
13
|
+
cacheRead: z.number().nonnegative().nullable(), cacheWrite: z.number().nonnegative().nullable(),
|
|
14
|
+
});
|
|
15
|
+
/** Dates and times locate events; millisecond offsets come from the same monotonic clock, not from subtracting wall-clock timestamps. */
|
|
16
|
+
export const requestTimingSchema = z.strictObject({
|
|
17
|
+
startedAt: z.iso.datetime(),
|
|
18
|
+
endedAt: z.iso.datetime(),
|
|
19
|
+
durationMs: z.number().nonnegative(),
|
|
20
|
+
firstOutputMs: z.number().nonnegative().nullable(),
|
|
21
|
+
firstTextMs: z.number().nonnegative().nullable(),
|
|
22
|
+
firstReasoningMs: z.number().nonnegative().nullable(),
|
|
23
|
+
lastOutputMs: z.number().nonnegative().nullable(),
|
|
24
|
+
outputChunks: z.number().int().nonnegative(),
|
|
25
|
+
outcome: z.enum(["completed", "failed", "cancelled"]),
|
|
26
|
+
});
|
|
27
|
+
export type RequestTiming = z.infer<typeof requestTimingSchema>;
|
|
28
|
+
|
|
29
|
+
export const modelSourceSchema = z.strictObject({ model: z.string(), adapter: z.string(), scope: z.string() });
|
|
30
|
+
export const modelCompletionSchema = z.strictObject({ usage: modelUsageSchema.nullable(), finishReason: z.string(),
|
|
31
|
+
rawFinishReason: z.string().nullable(), providerResponseId: z.string().nullable(), timing: requestTimingSchema.optional(),
|
|
32
|
+
extensions: z.array(presentationSchema).optional() });
|
|
33
|
+
/** Decode only domain extensions known to the caller; unknown or undecodable extensions do not affect the public terminal state. */
|
|
34
|
+
export function completionData<T extends Json>(completion: ModelCompletion | null | undefined, contract: DataContract<T>): T | null {
|
|
35
|
+
const extension = completion?.extensions?.find(value => value.contract.id === contract.id);
|
|
36
|
+
if (!extension) return null;
|
|
37
|
+
const result = contract.schema.safeParse(structuredClone(extension.data));
|
|
38
|
+
return result.success ? result.data : null;
|
|
39
|
+
}
|
|
40
|
+
const block = { responseId: id, requestParentId: id.nullable(), ordinal: z.number().int().nonnegative(),
|
|
41
|
+
complete: z.boolean(), continuation: jsonSchema, source: modelSourceSchema, completion: modelCompletionSchema.nullable().optional(),
|
|
42
|
+
/** Block generation start and end times (millisecond timestamps); blocks finalized before generation ends have a null end time, and unrecorded times are omitted. See outcome for tool execution times. */
|
|
43
|
+
startedAt: z.number().nonnegative().optional(), endedAt: z.number().nonnegative().nullable().optional() };
|
|
44
|
+
export const modelBlockSchema = z.discriminatedUnion("kind", [
|
|
45
|
+
z.strictObject({ ...block, kind: z.literal("thinking"), text: z.string(), summary: z.string() }),
|
|
46
|
+
z.strictObject({ ...block, kind: z.literal("text"), text: z.string() }),
|
|
47
|
+
z.strictObject({ ...block, kind: z.literal("toolcall"), callId: id, name: id,
|
|
48
|
+
/** Original model text; retained only when parsed arguments are unavailable (the call is incomplete), otherwise arguments is authoritative. */
|
|
49
|
+
argumentsText: z.string().nullable(), arguments: jsonSchema, outcome: toolOutcomeSchema.nullable() })
|
|
50
|
+
.refine(data => data.complete === (data.outcome !== null), "Complete tool calls must have an outcome."),
|
|
51
|
+
]);
|
|
52
|
+
/** Terminal state is embedded in finalized blocks; execution notifications carry only feedback, not response identity or measurements. */
|
|
53
|
+
export const modelRunRecordSchema = z.discriminatedUnion("type", [
|
|
54
|
+
z.strictObject({ type: z.literal("block"), block: modelBlockSchema }),
|
|
55
|
+
z.strictObject({ type: z.literal("diagnostic"), origin: z.enum(["model", "software"]), code: z.string(), message: z.string(), level: z.enum(["error", "warning"]), details: errorDetailsSchema.optional() }),
|
|
56
|
+
]);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { thinkingLevels, type ThinkingLevel } from "./models.ts";
|
|
3
|
+
|
|
4
|
+
/** Model expression `<providerId>/<modelId>`: provider and model always appear as a pair, split at the first `/`. */
|
|
5
|
+
export interface ModelExpression { provider: string; model: string }
|
|
6
|
+
export function parseModelExpression(value: string): ModelExpression | null {
|
|
7
|
+
const index = value.indexOf("/");
|
|
8
|
+
if (index <= 0 || index === value.length - 1) return null;
|
|
9
|
+
return { provider: value.slice(0, index), model: value.slice(index + 1) };
|
|
10
|
+
}
|
|
11
|
+
export function formatModelExpression(expression: ModelExpression): string { return `${expression.provider}/${expression.model}`; }
|
|
12
|
+
export const modelExpressionSchema = z.string().max(321).refine(value => parseModelExpression(value) !== null, "Use a model expression such as provider/model.");
|
|
13
|
+
/** A reference string for a specific model; corresponds one-to-one with model records without using random record IDs. */
|
|
14
|
+
export function modelReference(model: { providerId: string; modelId: string }): string {
|
|
15
|
+
return formatModelExpression({ provider: model.providerId, model: model.modelId });
|
|
16
|
+
}
|
|
17
|
+
export const thinkingSchema = z.enum(thinkingLevels);
|
|
18
|
+
|
|
19
|
+
/** Display and requests share override calculation; empty values inherit defaults, without checking availability or rewriting selections. */
|
|
20
|
+
export function resolveModelSelection(
|
|
21
|
+
defaults: { model: string | null; thinking: ThinkingLevel | null } | undefined,
|
|
22
|
+
overrides: { model?: string | null; thinking?: ThinkingLevel | null },
|
|
23
|
+
) {
|
|
24
|
+
return {
|
|
25
|
+
model: overrides.model ?? defaults?.model ?? null,
|
|
26
|
+
thinking: overrides.thinking ?? defaults?.thinking ?? null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Generates menu options only; does not resolve existing selections or validate requests. */
|
|
31
|
+
export function availableThinkingLevels(model: { capabilities: { reasoning: boolean; thinkingLevels: readonly typeof thinkingLevels[number][] } } | null) {
|
|
32
|
+
if (!model) return thinkingLevels;
|
|
33
|
+
return model.capabilities.reasoning ? thinkingLevels.filter(level => model.capabilities.thinkingLevels.includes(level)) : [];
|
|
34
|
+
}
|
package/src/models.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** Shared model protocol: independent of resource implementations, agent configuration, and host adapters. */
|
|
2
|
+
import type { Json } from "./json.ts";
|
|
3
|
+
import type { ModelInputPart } from "./model-input.ts";
|
|
4
|
+
import type { ContentReader } from "./content-runtime.ts";
|
|
5
|
+
import type { ToolOutcome, ToolInvocationInfo, ToolResult } from "./tool-result.ts";
|
|
6
|
+
import type { ResourceRef } from "./workspace.ts";
|
|
7
|
+
import type { RequestTiming } from "./model-records.ts";
|
|
8
|
+
import type { Presentation } from "./presentation.ts";
|
|
9
|
+
import type { ErrorDetails } from "./error-details.ts";
|
|
10
|
+
|
|
11
|
+
/** Standard levels shared across the application; protocol encoding does not change public values. */
|
|
12
|
+
export const thinkingLevels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
13
|
+
export type ThinkingLevel = typeof thinkingLevels[number];
|
|
14
|
+
export interface ModelTarget { role: string; model?: string; thinking?: ThinkingLevel }
|
|
15
|
+
export type PromptBlock =
|
|
16
|
+
| { id: string; kind: "system"; from: string; text: string }
|
|
17
|
+
| { id: string; kind: "agent"; role: string; text: string }
|
|
18
|
+
| { id: string; kind: "context"; file: string; text: string };
|
|
19
|
+
export interface SystemPrompt { blocks: readonly PromptBlock[] }
|
|
20
|
+
/** 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
|
+
* continuation carries only native data such as signatures/ciphertext; target projection determines whether it can be reused.
|
|
22
|
+
*/
|
|
23
|
+
export type ModelPart =
|
|
24
|
+
| { kind: "thinking"; text: string; summary: string; continuation: Json }
|
|
25
|
+
| { kind: "text"; text: string; continuation: Json }
|
|
26
|
+
| { kind: "toolcall"; callId: string; name: string; arguments: Json; continuation: Json };
|
|
27
|
+
export type ModelItem =
|
|
28
|
+
| { kind: "input" | "context"; parts: readonly ModelInputPart[] }
|
|
29
|
+
// scope/adapter constrain native data; they do not gate public content in cross-model requests.
|
|
30
|
+
| { kind: "output"; parts: ModelPart[]; scope: string; adapter: string }
|
|
31
|
+
| { kind: "result"; callId: string; name: string; outcome: ToolOutcome };
|
|
32
|
+
export interface Usage {
|
|
33
|
+
input: number | null; output: number | null; total: number | null;
|
|
34
|
+
reasoning: number | null; cacheRead: number | null; cacheWrite: number | null;
|
|
35
|
+
}
|
|
36
|
+
export type StreamPart =
|
|
37
|
+
| { id: string; kind: "text" | "thinking" }
|
|
38
|
+
| { id: string; kind: "toolcall"; callId: string; name: string };
|
|
39
|
+
export type ModelEvent =
|
|
40
|
+
| { type: "part.start"; part: StreamPart }
|
|
41
|
+
| { type: "part.delta"; partId: string; delta: string; channel?: "summary" }
|
|
42
|
+
| { type: "part.end"; partId: string; arguments?: Json; continuation?: Json }
|
|
43
|
+
| { type: "finish"; reason: string; usage: Usage | null; responseId?: string; rawFinishReason?: string; source?: ModelSource; timing?: RequestTiming; response?: ModelResponse | null };
|
|
44
|
+
export type ModelSource = { model: string; adapter: string; scope: string };
|
|
45
|
+
export type ModelCompletion = {
|
|
46
|
+
usage: Usage | null; finishReason: string; rawFinishReason: string | null; providerResponseId: string | null;
|
|
47
|
+
timing?: RequestTiming;
|
|
48
|
+
/** Typed data declared and consumed by the domain; the public layer preserves unknown contracts. */
|
|
49
|
+
extensions?: Presentation[];
|
|
50
|
+
};
|
|
51
|
+
/** Finalized response blocks; these are not session nodes, and the consumer provides timeline identity and persistence. */
|
|
52
|
+
export type ModelBlock = {
|
|
53
|
+
responseId: string; requestParentId: string | null; ordinal: number; complete: boolean; continuation: Json; source: ModelSource;
|
|
54
|
+
/** Terminal state is delivered once with the actual response block having the highest ordinal; null for all other blocks. */
|
|
55
|
+
completion?: ModelCompletion | null;
|
|
56
|
+
/** Block generation start and end times (millisecond timestamps); blocks finalized before generation ends have a null end time, and unrecorded times are omitted. See outcome for tool execution times. */
|
|
57
|
+
startedAt?: number; endedAt?: number | null;
|
|
58
|
+
} & (
|
|
59
|
+
| { kind: "thinking"; text: string; summary: string }
|
|
60
|
+
| { kind: "text"; text: string }
|
|
61
|
+
| { kind: "toolcall"; callId: string; name: string; argumentsText: string | null; arguments: Json; outcome: ToolOutcome | null }
|
|
62
|
+
);
|
|
63
|
+
export type ModelLiveBlock = { id: string; responseId: string; ordinal: number } & (
|
|
64
|
+
| { kind: "text"; text: string; endedAt: number | null }
|
|
65
|
+
| { kind: "thinking"; text: string; startedAt: number; endedAt: number | null }
|
|
66
|
+
/** During the input phase, arguments is a display preview parsed tolerantly and cannot be used for execution; other phases contain complete arguments. */
|
|
67
|
+
| { kind: "toolcall"; name: string; argumentsText: string; arguments: Json; state: "input" | "ready" | "executing";
|
|
68
|
+
invocation: ToolInvocationInfo | null; result: Extract<ToolResult, { kind: "running" }> | null }
|
|
69
|
+
);
|
|
70
|
+
export type ModelRunEvent =
|
|
71
|
+
| { type: "block"; block: ModelBlock }
|
|
72
|
+
| { type: "progress"; blocks: readonly ModelLiveBlock[] }
|
|
73
|
+
| { type: "diagnostic"; origin: "model" | "software"; code: string; message: string; level: "error" | "warning"; details?: ErrorDetails };
|
|
74
|
+
/** Deliver execution events synchronously; consumers save finalized records, exceptions stop execution, and no further events are delivered after returning. */
|
|
75
|
+
export interface ModelRunObserver { event(event: ModelRunEvent): undefined }
|
|
76
|
+
/** Used only for HTTP request identity; does not enter model messages, history, or resource authorization. */
|
|
77
|
+
export interface ModelRequestIdentity {
|
|
78
|
+
userAgent?: string;
|
|
79
|
+
/** Stable within a session; may be omitted for a one-off call without a session. */
|
|
80
|
+
sessionId?: string;
|
|
81
|
+
}
|
|
82
|
+
/** Transient metadata from the actual HTTP response; contains no request credentials or body and is not automatically written to history. */
|
|
83
|
+
export interface ModelResponse {
|
|
84
|
+
connection: { id: string };
|
|
85
|
+
endpoint: string;
|
|
86
|
+
model: string;
|
|
87
|
+
status: number;
|
|
88
|
+
receivedAt: number;
|
|
89
|
+
headers: Readonly<Record<string, string>>;
|
|
90
|
+
}
|
|
91
|
+
export interface ModelRunRequest {
|
|
92
|
+
object: ResourceRef;
|
|
93
|
+
/** Session identity comes from object; callers may declare their own client identity. */
|
|
94
|
+
userAgent?: string;
|
|
95
|
+
requestParentId: string | null;
|
|
96
|
+
target: ModelTarget;
|
|
97
|
+
system: SystemPrompt;
|
|
98
|
+
messages: ModelItem[];
|
|
99
|
+
/** Filters plugin top-level tools and resource tools selected through actual links by name; does not affect fixed host entry points. */
|
|
100
|
+
tools?: { allow?: readonly string[]; exclude?: readonly string[] };
|
|
101
|
+
cache?: { expectHit: boolean };
|
|
102
|
+
/** Synchronously package domain data after final request preparation; delivered with the actual response's completion. */
|
|
103
|
+
completionExtensions?: (context: ModelRequestContext) => readonly Presentation[];
|
|
104
|
+
signal: AbortSignal;
|
|
105
|
+
observer: ModelRunObserver;
|
|
106
|
+
}
|
|
107
|
+
export type ModelRunResult = {
|
|
108
|
+
responseId: string | null; source: ModelSource | null; completion: ModelCompletion | null; tools: readonly ToolOutcome[];
|
|
109
|
+
/** Metadata from the last HTTP response received during this call; null if none was received, and excluded from response blocks and history. */
|
|
110
|
+
response: ModelResponse | null;
|
|
111
|
+
/** Measured timing for this call; still available for transient feedback when no terminal state was obtained. */
|
|
112
|
+
timing: RequestTiming | null;
|
|
113
|
+
} & ({ reason: "completed" } | { reason: "cancelled" } | { reason: "failed"; code: string; details?: ErrorDetails });
|
|
114
|
+
/** The final request's input context; an independent copy for domain extension producers to read. */
|
|
115
|
+
export interface ModelRequestContext {
|
|
116
|
+
system: SystemPrompt;
|
|
117
|
+
tools: readonly { name: string; description: string; parameters: Json }[];
|
|
118
|
+
}
|
|
119
|
+
export interface StreamRequest extends ModelRequestIdentity {
|
|
120
|
+
target: ModelTarget;
|
|
121
|
+
system: SystemPrompt;
|
|
122
|
+
messages: ModelItem[];
|
|
123
|
+
/** The output limit for this completion; uses the smaller of this value and the model's configured limit. */
|
|
124
|
+
maxOutputTokens?: number;
|
|
125
|
+
tools?: { name: string; description: string; parameters: Json }[];
|
|
126
|
+
signal: AbortSignal;
|
|
127
|
+
}
|
|
128
|
+
export interface ModelInputOptions { content?: ContentReader }
|
|
129
|
+
export interface ModelServices {
|
|
130
|
+
/** Check explicit request input and target without sending a model request or saving state. */
|
|
131
|
+
validate(request: Pick<StreamRequest, "target" | "system" | "messages" | "signal" | "userAgent" | "sessionId">, options?: ModelInputOptions): Promise<void>;
|
|
132
|
+
stream(request: StreamRequest, options?: ModelInputOptions): AsyncIterable<ModelEvent>;
|
|
133
|
+
/** One request and its tool batch; neither saves history nor drives the next request. */
|
|
134
|
+
run(request: ModelRunRequest): Promise<ModelRunResult>;
|
|
135
|
+
roles(): readonly { id: string; description: string; hidden: boolean; model: string | null; thinking: ThinkingLevel | null }[];
|
|
136
|
+
list(): readonly { model: string; provider: string; name: string; contextWindow: number | null; capabilities: { reasoning: boolean; tools: boolean; thinkingLevels: readonly ThinkingLevel[] } }[];
|
|
137
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Json } from "./json.ts";
|
|
3
|
+
import { contractReferenceSchema, type ContractReference, type DataContract } from "./presentation.ts";
|
|
4
|
+
|
|
5
|
+
export interface ResourceContract<S extends Json = Json> {
|
|
6
|
+
kind: string;
|
|
7
|
+
/** Decodes saved state; creation callbacks return S for encoding before persistence. State is arbitrary JSON, stored separately from host identity and title. */
|
|
8
|
+
stateSchema: z.ZodType<S>;
|
|
9
|
+
}
|
|
10
|
+
export interface ToolContract<A extends Json = Json> extends ContractReference {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
guidelines: readonly string[];
|
|
14
|
+
argsSchema: z.ZodType<A>;
|
|
15
|
+
}
|
|
16
|
+
/** Tool definitions fix selection descriptions, usage guidance, and args; implementations add only target/result and execution information. */
|
|
17
|
+
export interface ImplementationContract<S extends Json, T extends Json, A extends Json, O extends Json> extends DataContract<O> {
|
|
18
|
+
resource: ResourceContract<S>;
|
|
19
|
+
tool: ToolContract<A>;
|
|
20
|
+
targetSchema: z.ZodType<T>;
|
|
21
|
+
effect: "read" | "write";
|
|
22
|
+
execution: "parallel" | "sequential";
|
|
23
|
+
}
|
|
24
|
+
export function defineResource<S extends Json>(kind: string, stateSchema: z.ZodType<S>): ResourceContract<S> {
|
|
25
|
+
return { kind, stateSchema };
|
|
26
|
+
}
|
|
27
|
+
/** Tool args/target/result are projected as JSON Schema for the model; representability is validated at declaration time to avoid failures only when invoked. */
|
|
28
|
+
function requireDescription(schema: z.ZodType, subject: string): void {
|
|
29
|
+
if (!schema.description?.trim()) throw new Error(`Missing schema description: ${subject}`);
|
|
30
|
+
try { z.toJSONSchema(schema); } catch (error) { throw new Error(`Schema is not representable as JSON Schema: ${subject}`, { cause: error }); }
|
|
31
|
+
}
|
|
32
|
+
export function defineTool<A extends Json>(definition: ToolContract<A>): ToolContract<A> {
|
|
33
|
+
if (!definition.name.trim() || !definition.description.trim()) throw new Error("Tool name and description must not be empty.");
|
|
34
|
+
if (definition.guidelines.some(guideline => !guideline.trim())) throw new Error(`Empty tool guideline: ${definition.name}`);
|
|
35
|
+
requireDescription(definition.argsSchema, `${definition.name}.args`);
|
|
36
|
+
return { ...definition, ...contractReferenceSchema.parse({ id: definition.id }), guidelines: [...definition.guidelines] };
|
|
37
|
+
}
|
|
38
|
+
export function defineImplementation<S extends Json, T extends Json, A extends Json, O extends Json>(
|
|
39
|
+
definition: ImplementationContract<S, T, A, O>,
|
|
40
|
+
): ImplementationContract<S, T, A, O> {
|
|
41
|
+
requireDescription(definition.targetSchema, `${definition.id}.target`);
|
|
42
|
+
requireDescription(definition.schema, `${definition.id}.result`);
|
|
43
|
+
return definition;
|
|
44
|
+
}
|
package/src/objects.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Json } from "./json.ts";
|
|
2
|
+
import type { DataContract } from "./presentation.ts";
|
|
3
|
+
import type { RecordContract } from "./records.ts";
|
|
4
|
+
import type { ResourceRef, ResourceCreationResult, ResourceSnapshot, SessionSnapshot } from "./workspace.ts";
|
|
5
|
+
import type { ContentReader, ContentScope } from "./content-runtime.ts";
|
|
6
|
+
import type { EventPublisher } from "./events.ts";
|
|
7
|
+
import type { ActionContract, CreationContract } from "./commands.ts";
|
|
8
|
+
|
|
9
|
+
export interface ResourceCreationOptions {
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
/** Read eligibility for content referenced by the input; defaults to the current session content facade when omitted. */
|
|
12
|
+
content?: ContentReader;
|
|
13
|
+
/** Link the new resource to this recipient, committed atomically with creation. */
|
|
14
|
+
linkTo?: ResourceRef;
|
|
15
|
+
/** Initial resource links received by the new resource, committed atomically with creation. */
|
|
16
|
+
resources?: readonly ResourceRef[];
|
|
17
|
+
}
|
|
18
|
+
export interface ResourceActionOptions {
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
/** The valid content source for this action's input; defaults to the caller's bound session/object content facade when omitted. */
|
|
21
|
+
content?: ContentReader;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Data and event writes within a synchronous transaction; the host limits the validity of writer calls. */
|
|
25
|
+
export interface SessionWriter {
|
|
26
|
+
/** Append to the current timeline tip at commit time and advance the cursor; previously obtained read-only snapshots do not determine the append position. */
|
|
27
|
+
append<T extends Json>(contract: RecordContract<T>, data: NoInfer<T>): string;
|
|
28
|
+
set<T extends Json>(contract: DataContract<T>, data: NoInfer<T>): void;
|
|
29
|
+
}
|
|
30
|
+
export type ObjectWriter = SessionWriter;
|
|
31
|
+
export interface SessionReader {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly content: ContentReader;
|
|
34
|
+
snapshot(): SessionSnapshot;
|
|
35
|
+
get<T extends Json>(contract: DataContract<T>): T | null;
|
|
36
|
+
readonly title: { get(): string };
|
|
37
|
+
}
|
|
38
|
+
/** Generic execution facade at the session level; resource operations use explicit references, and the specific call entry point binds the caller's perspective. */
|
|
39
|
+
export interface SessionExecutor extends SessionReader {
|
|
40
|
+
create<I, S extends Json, O extends Json>(contract: CreationContract<I, S, O>, input: NoInfer<I> & Json, options?: ResourceCreationOptions): Promise<ResourceCreationResult>;
|
|
41
|
+
/** Invoke a resource's public action; returning means action handling is complete, while the resource manages any independent managed work itself. */
|
|
42
|
+
invoke<I, O extends Json>(contract: ActionContract<I, O>, target: ResourceRef, input: NoInfer<I> & Json, options?: ResourceActionOptions): Promise<void>;
|
|
43
|
+
/** Link a resource to a recipient; both ends are resources, and the recipient's resource type declares support for receiving links. */
|
|
44
|
+
link(receiver: ResourceRef, resource: ResourceRef): void;
|
|
45
|
+
/** Unlink the specified direction; the reverse link remains independently. */
|
|
46
|
+
unlink(receiver: ResourceRef, resource: ResourceRef): void;
|
|
47
|
+
/** Remove a settled object and all its links; preserve history and external entities. */
|
|
48
|
+
remove(resource: ResourceRef): void;
|
|
49
|
+
/** Append a resource rename event when the title changes; return the title with leading and trailing whitespace removed. */
|
|
50
|
+
rename(resource: ResourceRef, title: string): string;
|
|
51
|
+
commit(work: (writer: SessionWriter) => undefined): void;
|
|
52
|
+
/** set directly overwrites the session title; it can be called within a synchronous commit and commits or rolls back with it. */
|
|
53
|
+
readonly title: SessionReader["title"] & { set(text: string): void };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* An operation facade for the same session from the perspective of the tool caller and this request.
|
|
57
|
+
* The host binds the originating resource, the effective link scope at request start, and the tool execution scope, handling identity and execution validation consistently.
|
|
58
|
+
* Operations originate from the initiating resource: newly created resources link to it, while removal and rename targets resolve within this request's effective link scope.
|
|
59
|
+
* New operations use this perspective, explicitly defining implicitly bound endpoints, the operable resource scope, and the execution scope.
|
|
60
|
+
*/
|
|
61
|
+
export interface ToolSession extends SessionReader {
|
|
62
|
+
create<I, S extends Json, O extends Json>(contract: CreationContract<I, S, O>, input: NoInfer<I> & Json, options?: {
|
|
63
|
+
resources?: readonly string[];
|
|
64
|
+
/** Link the bound initiator to the new resource, committed atomically with creation; defaults to false when omitted. */
|
|
65
|
+
exposeSelf?: boolean;
|
|
66
|
+
}): Promise<ResourceCreationResult>;
|
|
67
|
+
remove(resourceId: string): void;
|
|
68
|
+
rename(resourceId: string, title: string): string;
|
|
69
|
+
}
|
|
70
|
+
export interface ObjectReader {
|
|
71
|
+
readonly ref: ResourceRef;
|
|
72
|
+
readonly content: ContentReader;
|
|
73
|
+
readonly session: SessionReader;
|
|
74
|
+
/** The resource snapshot bound to the call; null when identity has not yet been established within the creation transaction. */
|
|
75
|
+
snapshot(): ResourceSnapshot | null;
|
|
76
|
+
/** Current data is independent of the fixed snapshot and is not restored by cursor rewinding or branching. */
|
|
77
|
+
get<T extends Json>(contract: DataContract<T>): T | null;
|
|
78
|
+
}
|
|
79
|
+
export interface ObjectExecutor extends ObjectReader {
|
|
80
|
+
readonly events: EventPublisher;
|
|
81
|
+
/** Staging is valid only within the host execution scope; reads are validated against content already owned by the object or supplied in this input. */
|
|
82
|
+
readonly content: ContentScope;
|
|
83
|
+
readonly session: SessionExecutor;
|
|
84
|
+
commit(work: (writer: ObjectWriter) => undefined): void;
|
|
85
|
+
/** Mark asynchronous work that depends on the current timeline path; nested holds release independently, and cancellation waits for work to actually end. */
|
|
86
|
+
withBusy<T>(work: (signal: AbortSignal) => Promise<T>, options?: { signal?: AbortSignal }): Promise<T>;
|
|
87
|
+
/** Notify the host to query the projection again after in-memory display data changes; does not append history or save resource data. */
|
|
88
|
+
invalidate(): void;
|
|
89
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { uiTextSchema } from "./i18n.ts";
|
|
3
|
+
import type { Json } from "./json.ts";
|
|
4
|
+
import { bindMessages, type SourceMessage, type UiMessage } from "./i18n.ts";
|
|
5
|
+
import { defineData, type DataContract } from "./presentation.ts";
|
|
6
|
+
import { defineResource, type ResourceContract } from "./module-contract.ts";
|
|
7
|
+
import { defineConfig, type ConfigContract } from "./config.ts";
|
|
8
|
+
import { defineEvent, type EventContract, type EventDefinition } from "./events.ts";
|
|
9
|
+
|
|
10
|
+
const pluginIdPattern = /^[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*)+$/u;
|
|
11
|
+
const shortNamePattern = /^[a-z0-9][a-z0-9._-]*$/u;
|
|
12
|
+
|
|
13
|
+
/** Static information can be read without executing plugin code; the host resolves icon to an image within the package. */
|
|
14
|
+
export const pluginMetadataSchema = z.strictObject({
|
|
15
|
+
name: uiTextSchema.optional(), description: uiTextSchema.optional(),
|
|
16
|
+
version: z.string().optional(), author: z.string().optional(), icon: z.string().optional(),
|
|
17
|
+
});
|
|
18
|
+
export type PluginMetadata = z.infer<typeof pluginMetadataSchema>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Plugin namespace: declare plugin identity only once here; contract files declare resource types and data contracts using short names,
|
|
22
|
+
* which are expanded here to `<pluginId>/<name>`. Pure contracts shared by frontend and backend.
|
|
23
|
+
*/
|
|
24
|
+
export interface PluginNamespace {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
messages<T extends Record<string, SourceMessage>>(sourceLocale: string, definitions: T): { readonly [K in keyof T]: UiMessage };
|
|
27
|
+
name(short: string): string;
|
|
28
|
+
resource<S extends Json>(name: string, stateSchema: z.ZodType<S>): ResourceContract<S>;
|
|
29
|
+
data<T extends Json>(name: string, schema: z.ZodType<T>): DataContract<T>;
|
|
30
|
+
event<I, D extends Json>(name: string, definition: EventDefinition<I, D>): EventContract<I, D>;
|
|
31
|
+
/** The plugin's sole user configuration contract. */
|
|
32
|
+
config<T extends Json>(schema: z.ZodType<T>, defaults: NoInfer<T>): ConfigContract<T>;
|
|
33
|
+
}
|
|
34
|
+
export function pluginNamespace(id: string): PluginNamespace {
|
|
35
|
+
if (!pluginIdPattern.test(id)) throw new Error(`Invalid plugin ID: ${id}`);
|
|
36
|
+
const name = (short: string) => {
|
|
37
|
+
if (!shortNamePattern.test(short)) throw new Error(`Invalid short name in plugin ${id}: ${short}`);
|
|
38
|
+
return `${id}/${short}`;
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
id, name,
|
|
42
|
+
messages: (sourceLocale, definitions) => bindMessages(id, sourceLocale, definitions),
|
|
43
|
+
resource: (short, stateSchema) => defineResource(name(short), stateSchema),
|
|
44
|
+
data: (short, schema) => defineData(name(short), schema),
|
|
45
|
+
event: (short, definition) => defineEvent(name(short), definition),
|
|
46
|
+
config: (schema, defaults) => defineConfig(id, schema, defaults),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function belongsTo(pluginId: string, qualified: string): boolean {
|
|
50
|
+
return qualified.startsWith(`${pluginId}/`) && qualified.length > pluginId.length + 1;
|
|
51
|
+
}
|
|
52
|
+
/** Extract the source plugin from a namespaced ID; return null when there is no namespace. */
|
|
53
|
+
export function pluginOf(qualified: string): string | null {
|
|
54
|
+
const index = qualified.indexOf("/");
|
|
55
|
+
return index > 0 ? qualified.slice(0, index) : null;
|
|
56
|
+
}
|
|
57
|
+
/** Resource projection for a missing implementation: preserves raw data and the source plugin for fallback UI display. */
|
|
58
|
+
export const missingResourceContract = { id: "asterism.builtin/missing-resource" } as const;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { jsonSchema, type Json } from "./json.ts";
|
|
3
|
+
import { encodeJson } from "./data-encoding.ts";
|
|
4
|
+
|
|
5
|
+
export const contractReferenceSchema = z.strictObject({ id: z.string().min(1).max(160) });
|
|
6
|
+
export type ContractReference = z.infer<typeof contractReferenceSchema>;
|
|
7
|
+
/** The schema decodes stored or transported JSON into T; producers encode T through the same schema. */
|
|
8
|
+
export interface DataContract<T extends Json = Json> extends ContractReference { schema: z.ZodType<T> }
|
|
9
|
+
export const presentationSchema = z.strictObject({ contract: contractReferenceSchema, data: jsonSchema });
|
|
10
|
+
export type Presentation = z.infer<typeof presentationSchema>;
|
|
11
|
+
|
|
12
|
+
/** Shared declarations depend on no process or UI; each end registers its own implementation. */
|
|
13
|
+
export function defineData<T extends Json>(id: string, schema: z.ZodType<T>): DataContract<T> {
|
|
14
|
+
return { ...contractReferenceSchema.parse({ id }), schema };
|
|
15
|
+
}
|
|
16
|
+
export function reference(contract: ContractReference): ContractReference {
|
|
17
|
+
return { id: contract.id };
|
|
18
|
+
}
|
|
19
|
+
export function present<T extends Json>(contract: DataContract<T>, value: NoInfer<T>): Presentation {
|
|
20
|
+
return { contract: reference(contract), data: encodeJson(contract.schema, value, contract.id) };
|
|
21
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { uiTextSchema, type UiText } from "./i18n.ts";
|
|
3
|
+
import { jsonSchema, type Json } from "./json.ts";
|
|
4
|
+
|
|
5
|
+
/** Usage queries and presets use global catalog IDs; connection identity is independent, and duplicate catalog identities must fail explicitly. */
|
|
6
|
+
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
|
+
|
|
47
|
+
const identity = {
|
|
48
|
+
id: z.string().trim().min(1), label: uiTextSchema,
|
|
49
|
+
scope: z.enum(["account", "plan", "key", "connection"]),
|
|
50
|
+
};
|
|
51
|
+
export const quotaValueSchema = z.discriminatedUnion("type", [
|
|
52
|
+
z.strictObject({ type: z.literal("ratio"), remaining: z.number().min(0).max(1) }),
|
|
53
|
+
z.strictObject({ type: z.literal("quantity"), remaining: z.number().nonnegative(), limit: z.number().positive(), unit: uiTextSchema })
|
|
54
|
+
.refine(value => value.remaining <= value.limit, "Remaining quota cannot exceed its limit."),
|
|
55
|
+
]);
|
|
56
|
+
export const usageItemSchema = z.discriminatedUnion("type", [
|
|
57
|
+
z.strictObject({ ...identity, type: z.literal("balance"), remaining: z.number(), currency: z.string().regex(/^[A-Z]{3}$/u) }),
|
|
58
|
+
z.strictObject({ ...identity, type: z.literal("quota"), value: quotaValueSchema,
|
|
59
|
+
category: z.enum(["allowance", "rate_limit"]).optional(),
|
|
60
|
+
window: z.strictObject({
|
|
61
|
+
mode: z.enum(["rolling", "fixed", "unknown"]),
|
|
62
|
+
/** Supply an actual fixed duration only when known; unknown periods must not be fabricated as weeks or months. */
|
|
63
|
+
durationMs: z.number().positive().nullable(),
|
|
64
|
+
resetAt: z.number().nonnegative().nullable(),
|
|
65
|
+
}),
|
|
66
|
+
}),
|
|
67
|
+
]);
|
|
68
|
+
export const usageItemsSchema = z.array(usageItemSchema).refine(items => new Set(items.map(item => item.id)).size === items.length,
|
|
69
|
+
"Usage item IDs must be unique.");
|
|
70
|
+
export type UsageItem = z.infer<typeof usageItemSchema>;
|
|
71
|
+
export type QuotaItem = Extract<UsageItem, { type: "quota" }>;
|
|
72
|
+
export const providerUsageErrorSchema = z.enum(["unavailable", "credential", "authentication", "unsupported", "rate_limit", "network", "timeout", "protocol", "server"]);
|
|
73
|
+
export type ProviderUsageError = z.infer<typeof providerUsageErrorSchema>;
|
|
74
|
+
export const usageProviderInfoSchema = z.strictObject({ id: providerUsageIdSchema, label: uiTextSchema, owner: z.string(),
|
|
75
|
+
source: z.enum(["query", "headers"]).default("query") });
|
|
76
|
+
export type UsageProviderInfo = z.infer<typeof usageProviderInfoSchema>;
|
|
77
|
+
|
|
78
|
+
export function quotaRemaining(item: QuotaItem): number {
|
|
79
|
+
return item.value.type === "ratio" ? item.value.remaining : item.value.remaining / item.value.limit;
|
|
80
|
+
}
|
|
81
|
+
/** Select the window with the shortest period, independently of the reset countdown; unknown periods follow known ones, and equal lengths retain source order. */
|
|
82
|
+
export function primaryQuota(items: readonly UsageItem[]): QuotaItem | null {
|
|
83
|
+
let selected: QuotaItem | null = null;
|
|
84
|
+
for (const item of items) if (item.type === "quota" && (!selected ||
|
|
85
|
+
(item.window.durationMs ?? Infinity) < (selected.window.durationMs ?? Infinity))) selected = item;
|
|
86
|
+
return selected;
|
|
87
|
+
}
|
package/src/records.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { Json } from "./json.ts";
|
|
3
|
+
import type { Source } from "./workspace.ts";
|
|
4
|
+
|
|
5
|
+
/** Public envelope for session records: the host maps its storage format to this envelope, and plugins decode by contract without depending on internal node structure. */
|
|
6
|
+
export interface RecordRef { readonly id: string }
|
|
7
|
+
export interface SessionRecord {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly sessionId: string;
|
|
10
|
+
readonly parentId: string | null;
|
|
11
|
+
/** Append order within the session, starting at 1. */
|
|
12
|
+
readonly sequence: number;
|
|
13
|
+
readonly createdAt: string;
|
|
14
|
+
readonly contract: RecordRef;
|
|
15
|
+
/** Writer; stamped by the host according to the write channel, while the associated object belongs in data. */
|
|
16
|
+
readonly source: Source;
|
|
17
|
+
/** Model-visible text written by the platform, consumed when executors project context; recipient being null means all executors. */
|
|
18
|
+
readonly context: { readonly text: string; readonly recipient: string | null } | null;
|
|
19
|
+
readonly data: Json;
|
|
20
|
+
}
|
|
21
|
+
export type Decoded<D> =
|
|
22
|
+
| { readonly status: "known"; readonly record: SessionRecord; readonly data: D }
|
|
23
|
+
| { readonly status: "unknown"; readonly record: SessionRecord; readonly reason: "invalid_data" };
|
|
24
|
+
export interface RecordContract<D extends Json = Json> extends RecordRef {
|
|
25
|
+
/** Decodes persisted JSON into D; record writers encode D through the same schema. */
|
|
26
|
+
readonly schema: z.ZodType<D>;
|
|
27
|
+
/** Return null when the record does not belong to this contract; compatibility and transformations are determined by the schema, and failure preserves the raw record. */
|
|
28
|
+
decode(record: SessionRecord): Decoded<D> | null;
|
|
29
|
+
}
|
|
30
|
+
export function defineRecord<D extends Json>(id: string, definition: {
|
|
31
|
+
schema: z.ZodType<D>;
|
|
32
|
+
}): RecordContract<D> {
|
|
33
|
+
const { schema } = definition;
|
|
34
|
+
return {
|
|
35
|
+
id, schema,
|
|
36
|
+
decode(record) {
|
|
37
|
+
if (record.contract.id !== id) return null;
|
|
38
|
+
const data = structuredClone(record.data);
|
|
39
|
+
try {
|
|
40
|
+
const parsed = schema.safeParse(data);
|
|
41
|
+
return parsed.success ? { status: "known", record, data: parsed.data } : { status: "unknown", record, reason: "invalid_data" };
|
|
42
|
+
} catch { return { status: "unknown", record, reason: "invalid_data" }; }
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** timeline reads the complete ancestor path to the snapshot tip; session reads all session records within the snapshot's upper bound in save order. */
|
|
47
|
+
export interface RecordQuery { readonly scope: "timeline" | "session" }
|
|
48
|
+
/** An observation handle fixed at one read position; reads records on demand and does not switch to the latest state after await. */
|
|
49
|
+
export interface SessionView {
|
|
50
|
+
readonly id: string;
|
|
51
|
+
readonly title: string;
|
|
52
|
+
readonly records: {
|
|
53
|
+
all(query: RecordQuery): readonly SessionRecord[];
|
|
54
|
+
/** The earliest matching record; returns unknown if it cannot be decoded, without skipping it. */
|
|
55
|
+
first<D extends Json>(contract: RecordContract<D>, query: RecordQuery): Decoded<D> | null;
|
|
56
|
+
/** The latest matching record, located by scope and contract; decodes only that record and returns unknown if undecodable, without falling back. */
|
|
57
|
+
last<D extends Json>(contract: RecordContract<D>, query: RecordQuery): Decoded<D> | null;
|
|
58
|
+
query<D extends Json>(contract: RecordContract<D>, query: RecordQuery): Decoded<D>[];
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import type { PluginCalls } from "./calls.ts";
|
|
3
|
+
|
|
4
|
+
const Calls = createContext<PluginCalls | null>(null);
|
|
5
|
+
export const RendererCallsProvider = Calls.Provider;
|
|
6
|
+
/** Invoke a registered backend contract; associating an object does not change resource ownership. */
|
|
7
|
+
export function useRendererCalls(): PluginCalls {
|
|
8
|
+
const calls = useContext(Calls);
|
|
9
|
+
if (!calls) throw new Error("Renderer calls must be provided by the host.");
|
|
10
|
+
return calls;
|
|
11
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createContext, useContext, type ComponentType, type ReactNode } from "react";
|
|
2
|
+
import type { UiText } from "./i18n.ts";
|
|
3
|
+
|
|
4
|
+
/** Auxiliary panel identities are unique within a single card renderer; content receives the same validated projection as the body. */
|
|
5
|
+
export interface CardPanelDefinition<T> {
|
|
6
|
+
id: string;
|
|
7
|
+
label: UiText;
|
|
8
|
+
icon: ReactNode;
|
|
9
|
+
Body: ComponentType<{ data: T }>;
|
|
10
|
+
}
|
|
11
|
+
export interface CardPanelView {
|
|
12
|
+
id: string;
|
|
13
|
+
label: UiText;
|
|
14
|
+
icon: ReactNode;
|
|
15
|
+
body: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
/** Local card view operations; do not submit resource commands or write task history. */
|
|
18
|
+
export interface RendererCard {
|
|
19
|
+
readonly activePanel: string | null;
|
|
20
|
+
openPanel(id: string): void;
|
|
21
|
+
closePanel(): void;
|
|
22
|
+
}
|
|
23
|
+
const CardContext = createContext<RendererCard | null>(null);
|
|
24
|
+
export const RendererCardProvider = CardContext.Provider;
|
|
25
|
+
export function useRendererCard(): RendererCard {
|
|
26
|
+
const card = useContext(CardContext);
|
|
27
|
+
if (!card) throw new Error("The renderer must be bound to a card.");
|
|
28
|
+
return card;
|
|
29
|
+
}
|