@asterism-app/builtin-api 0.0.0-stage → 0.19.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/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chien Zhang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,21 @@
1
- # Temporary Holding Version
1
+ # Built-in public API
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ This package exposes domain contracts and extension capabilities shared by Asterism's built-in implementation and external plugins.
4
+
5
+ It lets a plugin interact with existing Agent behavior, offer creation capabilities, or contribute Agent presentation without importing the implementation.
6
+
7
+ Public declarations retain their identity across consumers. The package is a shared domain boundary, not a second installed plugin and not a grant of private object access.
8
+
9
+ General hosting facilities belong to the SDK. Built-in-specific behavior belongs here only when it is intentionally public.
10
+
11
+ ## Installation
12
+
13
+ Install the testing releases with `npm install --save-dev --save-exact @asterism-app/sdk@next @asterism-app/builtin-api@next`. Declare both packages as peer dependencies with `"*"` in the plugin package.
14
+
15
+ The package ships TypeScript source. Asterism supplies the built-in public API at runtime, preserving shared declaration identity. Use development versions compatible with the target application; installing these packages does not install or update the built-in implementation.
16
+
17
+ ## Authoring material
18
+
19
+ The source package includes `docs/` for built-in domain development guides and `examples/` for agent input, input preparation, resource creation, and agent UI extensions. These examples are fragments to compose into a plugin with compatible SDK and built-in API dependencies. They participate in repository type checking.
20
+
21
+ The application's manual exposes these directories directly. Generic plugin development material and the complete starter package belong to `@asterism-app/sdk`.
package/docs/agents.md ADDED
@@ -0,0 +1,31 @@
1
+ # Extend built-in agents
2
+
3
+ The built-in public API lets plugins interact with Asterism's existing agents through shared action contracts and UI extension points. The contributing plugin owns its business state; the built-in implementation owns the agent's input handling and execution.
4
+
5
+ ## Declare the dependency
6
+
7
+ Declare `@asterism-app/builtin-api` and `@asterism-app/sdk` in `peerDependencies` with `"*"`; the running application provides the public package instances, and the host does not gate loading on these ranges. For local development, source-package dependencies supply editor types. Import public contracts from the domain package so every participant uses the same declarations.
8
+
9
+ Examples are TypeScript and TSX fragments for a plugin's backend or renderer. They use public packages; install additional libraries imported by the chosen example, such as Nano ID for submission identities, in the plugin project. A plugin still supplies its manifest and composition entry points.
10
+
11
+ ## Submit input and cancel work
12
+
13
+ A backend plugin can obtain a session executor from its plugin context and submit input to an agent resource reference. This also works from external event callbacks; a tool invocation is not required. Use a resource's attachment lifetime for an object-owned connection, and pass its cancellation signal into ongoing submissions.
14
+
15
+ A bridge resource can query a fresh session snapshot for resources directly linked to itself, select agent references, and invoke their public input action. Link direction expresses which resources the receiver can work with. The reverse relationship is independent, and observing a relationship does not expose another plugin's private state.
16
+
17
+ Each input submission has a submission identity, parts, a delivery mode, and a sender. Reuse the submission identity when retrying the same input. `steer` delivers pending input at a request boundary; `follow` delivers it at a turn boundary. An idle agent starts execution after input admission. A successful submission call means input handling has completed, not that the agent has finished responding.
18
+
19
+ File parts use content references with explicit read eligibility. Pass the appropriate content source with the action when forwarding files. Cancellation of the submission is separate from cancellation of the agent's independent execution, which has its own public action.
20
+
21
+ ## Prepare incoming input
22
+
23
+ An external preparer can inspect, replace, or reject input using the original built-in action contract. Its replacement supplies raw action input that will be parsed again. Preparation has read capabilities and does not commit session mutations.
24
+
25
+ Keep a preparer's scope tied to its business purpose. It does not replace the agent implementation or acquire a private agent executor.
26
+
27
+ ## Extend presentation
28
+
29
+ Register agent UI extensions during frontend preparation and bind registration to its cancellation signal. Extensions can present controls, supplemental content, overlays, or modal interaction using the built-in presentation components.
30
+
31
+ The plugin supplies business state for the associated agent and callbacks for user actions. A component becoming invisible does not complete or cancel waiting backend work. Remounting should reread current state, and each submission entry owns its pending and error state.
@@ -0,0 +1,9 @@
1
+ # Offer resource creation to agents
2
+
3
+ A plugin can expose its resource's public creation contract through the built-in agent creation mechanism. The plugin still owns the resource type, initial data, projection, and lifetime. The built-in domain integration supplies the model-facing creation entry.
4
+
5
+ Declare creation once and pass that original contract to the integration. A straightforward entry accepts the creation contract's input directly. A specialized entry can define a smaller model-facing input and transform it into the same creation input, such as turning a short note into a titled document.
6
+
7
+ Creation participates in the host's resource and history operations. The created object links to the invoking agent. Optional initial resources remain within the request's captured resource scope. Explicit self-exposure supplies the reverse relationship; a new resource does not inherit every link or capability of its creator.
8
+
9
+ Use descriptions and schemas that explain what the resulting resource means and what inputs the model should supply. Creating another agent is also an explicit domain operation with its own configuration and initial links.
@@ -0,0 +1,24 @@
1
+ import { nanoid } from "nanoid";
2
+ import { SubmitAgentInput, CancelAgent, type AgentMessageSender } from "@asterism-app/builtin-api/contracts";
3
+ import type { ModelInputPart } from "@asterism-app/sdk/contracts";
4
+ import type { PluginContext, ResourceRef, ResourceActionOptions } from "@asterism-app/sdk/runtime";
5
+
6
+ /** Backend plugins submit complete input using a resource reference; returning means acceptance, while the Agent maintains its own execution state. */
7
+ export async function steerAgent(plugin: PluginContext, agent: ResourceRef, parts: ModelInputPart[], options?: ResourceActionOptions) {
8
+ await plugin.session(agent.sessionId).invoke(SubmitAgentInput, agent, {
9
+ submissionId: nanoid(16), mode: "steer", parts,
10
+ }, options);
11
+ }
12
+
13
+ /** Relay adapters provide their own resource address and the agent name on that resource, leaving the body unchanged. */
14
+ export async function deliverAgentMessage(plugin: PluginContext, agent: ResourceRef,
15
+ sender: Extract<AgentMessageSender, { kind: "agent" }>, parts: ModelInputPart[], options?: ResourceActionOptions) {
16
+ await plugin.session(agent.sessionId).invoke(SubmitAgentInput, agent, {
17
+ submissionId: nanoid(16), mode: "steer", sender, parts,
18
+ }, options);
19
+ }
20
+
21
+ /** The Agent's independent execution is canceled through a resource action, separately from cancellation of the submission request itself. */
22
+ export async function stopAgent(plugin: PluginContext, agent: ResourceRef) {
23
+ await plugin.session(agent.sessionId).invoke(CancelAgent, agent, {});
24
+ }
@@ -0,0 +1,62 @@
1
+ import { useState } from "react";
2
+ import type { ResourceRef } from "@asterism-app/sdk/contracts";
3
+ import { AgentModal, AgentOverlay, AgentScrollArea, agentUI, type AgentOverlayPosition } from "@asterism-app/builtin-api/renderer";
4
+ import { useRendererObject, type RendererModule } from "@asterism-app/sdk/renderer";
5
+
6
+ /** Plugins provide business state and submission callbacks for the associated Agent; the UI only consumes this interface. */
7
+ export interface AgentUIState {
8
+ question: { id: string; text: string; answer(value: string): Promise<void> } | null;
9
+ todos: readonly string[];
10
+ todoPosition: AgentOverlayPosition;
11
+ preview: { title: string; url: string } | null;
12
+ closePreview(): void;
13
+ }
14
+
15
+ /** useStateForAgent is provided by the plugin, which can subscribe to its own business service and reread the current state on remount. */
16
+ export function questionAndTodoViews(
17
+ useStateForAgent: (agent: ResourceRef) => AgentUIState,
18
+ ): RendererModule {
19
+ function Extension() {
20
+ const object = useRendererObject();
21
+ const state = useStateForAgent(object.ref);
22
+ return <>
23
+ {!!state.todos.length && <AgentOverlay position={state.todoPosition}>
24
+ <ul>{state.todos.map((text, index) => <li key={index}>{text}</li>)}</ul>
25
+ </AgentOverlay>}
26
+ {state.question && <AgentOverlay position="bottom" hideComposer>
27
+ <Question key={state.question.id} question={state.question} />
28
+ </AgentOverlay>}
29
+ <AgentModal open={!!state.preview} title={state.preview?.title ?? "Preview"}
30
+ onOpenChange={open => { if (!open) state.closePreview(); }}>
31
+ {state.preview && <img src={state.preview.url} alt={state.preview.title} />}
32
+ </AgentModal>
33
+ </>;
34
+ }
35
+ return {
36
+ resources: [], cards: [], tools: [],
37
+ async prepare({ signal }) {
38
+ agentUI.register({ id: "example.questions/agent-ui", Component: Extension }, { signal });
39
+ },
40
+ };
41
+ }
42
+
43
+ function Question({ question }: { question: NonNullable<AgentUIState["question"]> }) {
44
+ const [answer, setAnswer] = useState("");
45
+ const [busy, setBusy] = useState(false);
46
+ const [error, setError] = useState<string | null>(null);
47
+ // Third-party authors can use their own control libraries; this example depends only on public packages and React.
48
+ return <form onSubmit={event => {
49
+ event.preventDefault();
50
+ if (busy) return;
51
+ setBusy(true); setError(null);
52
+ void question.answer(answer).catch(error => {
53
+ setError(error instanceof Error ? error.message : String(error));
54
+ }).finally(() => { setBusy(false); });
55
+ }}>
56
+ <AgentScrollArea style={{ maxHeight: "16rem" }}>
57
+ <label>{question.text}<input value={answer} disabled={busy} onChange={event => setAnswer(event.target.value)} /></label>
58
+ </AgentScrollArea>
59
+ <button type="submit" disabled={busy}>Answer</button>
60
+ {error && <p role="alert">{error}</p>}
61
+ </form>;
62
+ }
@@ -0,0 +1,17 @@
1
+ import { SubmitAgentInput } from "@asterism-app/builtin-api/contracts";
2
+ import { pluginNamespace } from "@asterism-app/sdk/contracts";
3
+ import { defineModule, definePlugin, implementPreparer } from "@asterism-app/sdk/runtime";
4
+
5
+ /** External plugins consume built-in action declarations; they neither declare a second Agent type nor import its implementation directory. */
6
+ export default definePlugin({
7
+ namespace: pluginNamespace("example.input-review"),
8
+ modules: [defineModule(plugin => ({
9
+ preparers: [implementPreparer(SubmitAgentInput, {
10
+ id: "review-input",
11
+ prepare(input, { object }) {
12
+ plugin.log.info(`Preparing ${input.parts.length} input parts for agent ${object.ref.id}.`);
13
+ return { action: "accept" };
14
+ },
15
+ })],
16
+ }))],
17
+ });
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+ import { defineCreation, pluginNamespace } from "@asterism-app/sdk/contracts";
3
+ import { definePlugin, implementCreation, implementResource } from "@asterism-app/sdk/runtime";
4
+ import { defineAgentCreation } from "@asterism-app/builtin-api/runtime";
5
+
6
+ const ns = pluginNamespace("example.documents");
7
+ const Document = ns.resource("document", z.object({ text: z.string() }));
8
+ const View = ns.data("view", z.object({ text: z.string() }));
9
+ export const CreateDocument = defineCreation(Document, { id: ns.name("create"),
10
+ schema: z.object({ title: z.string().min(1), text: z.string().default("") }),
11
+ });
12
+
13
+ export default definePlugin({
14
+ namespace: ns,
15
+ setup() {
16
+ return {
17
+ resources: [implementResource(Document, View, {
18
+ creation: implementCreation(CreateDocument, { create: input => ({ title: input.title, data: { text: input.text } }) }),
19
+ project: data => data,
20
+ inspect: async data => data,
21
+ })],
22
+ modules: [
23
+ // Reuse the creation contract directly without copying the schema.
24
+ defineAgentCreation({ id: ns.name("document"), description: "Create a document.", creation: CreateDocument }),
25
+ // The same resource type can provide another model entry point that hides or transforms internal parameters.
26
+ defineAgentCreation({
27
+ id: ns.name("quick-note"), description: "Create a document from a short note.", creation: CreateDocument,
28
+ inputSchema: z.object({ note: z.string().min(1).describe("The note text.") }),
29
+ toInput: ({ note }) => ({ title: note.slice(0, 60), text: note }),
30
+ }),
31
+ ],
32
+ };
33
+ },
34
+ });
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
1
  {
2
2
  "name": "@asterism-app/builtin-api",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.19.1",
4
+ "description": "Public domain contracts and extensions for Asterism built-in features",
5
+ "license": "MIT",
6
+ "author": "Chien Zhang",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/d3ara1n/asterism.git",
10
+ "directory": "packages/builtin-api"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public",
14
+ "tag": "next",
15
+ "registry": "https://registry.npmjs.org/"
16
+ },
17
+ "type": "module",
18
+ "exports": {
19
+ "./contracts": "./src/contracts.ts",
20
+ "./renderer": "./src/renderer.ts",
21
+ "./runtime": "./src/runtime.ts"
22
+ },
23
+ "peerDependencies": {
24
+ "@asterism-app/sdk": "*",
25
+ "react": "^19.3.0",
26
+ "zod": "^4.6.5"
27
+ },
28
+ "peerDependenciesMeta": {
29
+ "react": { "optional": true }
30
+ },
31
+ "files": [
32
+ "src",
33
+ "!src/**/*.test.ts",
34
+ "docs",
35
+ "examples",
36
+ "README.md"
37
+ ]
38
+ }
package/src/agent.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { defineCreation, defineAction } from "@asterism-app/sdk/contracts";
2
+ import { z } from "zod";
3
+ import { builtin } from "./identity.ts";
4
+ import { userInputPartsSchema } from "./input.ts";
5
+ import { agentSelectionSchema } from "./configuration.ts";
6
+ import { agentMessageSenderSchema } from "./messaging.ts";
7
+
8
+ /** Agents are ordinary resources: creation events record only identity; mutable data such as model selection belongs to private resource state. */
9
+ export const Agent = builtin.resource("agent", z.object({}));
10
+ /** The built-in agent preset selected by default when starting from the home page. */
11
+ export const defaultAgentPreset = "worker";
12
+ /** The host draft target for the first submission from the home page; this is not an action routable to an existing object. */
13
+ export const startAgentContentTarget = { contractId: builtin.name("session.start"), resourceKind: Agent.kind } as const;
14
+ /** Pending input is delivered at request or turn boundaries; execution starts immediately when idle. */
15
+ export const agentInputModeSchema = z.enum(["steer", "follow"]);
16
+ /** Submissions and drafts share a 16-character Nano ID identity. */
17
+ export const submissionIdSchema = z.string().regex(/^[A-Za-z0-9_-]{16}$/u);
18
+ /** title is optional; when omitted, the creation entry point generates a random display name. */
19
+ export const agentCreateSchema = z.strictObject({
20
+ selection: agentSelectionSchema,
21
+ title: z.string().trim().min(1).max(160).optional().describe("Display title for the new Agent. When omitted, a random adjective-and-noun name such as HardStone is generated."),
22
+ }).describe("Agent configuration and title.");
23
+
24
+ export const CreateAgent = defineCreation(Agent, { id: builtin.name("agent.create"), schema: agentCreateSchema });
25
+ export const SubmitAgentInput = defineAction(Agent, {
26
+ id: builtin.name("agent.submit"),
27
+ schema: z.strictObject({
28
+ submissionId: submissionIdSchema, parts: userInputPartsSchema, mode: agentInputModeSchema,
29
+ sender: agentMessageSenderSchema.default({ kind: "user" }),
30
+ }),
31
+ });
32
+ export type SubmitAgentInputData = z.infer<typeof SubmitAgentInput.schema>;
33
+ export const ContinueAgent = defineAction(Agent, { id: builtin.name("agent.continue"), schema: z.strictObject({}) });
34
+ export const ConfigureAgent = defineAction(Agent, {
35
+ id: builtin.name("agent.configure"), schema: z.strictObject({ selection: agentSelectionSchema }),
36
+ });
37
+ export const CancelAgent = defineAction(Agent, { id: builtin.name("agent.cancel"), schema: z.strictObject({}) });
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+ import { modelCostSchema, modelReference, resolveModelSelection, thinkingSchema } from "@asterism-app/sdk/contracts";
3
+ import { agentSchema, type AgentSelection } from "./configuration.ts";
4
+ import { builtin } from "./identity.ts";
5
+
6
+ /** Catalogs projected by the application for built-in plugins; excludes configuration file formats, credentials, connection tests, and override editing state. */
7
+ export const AgentConfigurations = builtin.data("agent.configurations", z.array(agentSchema));
8
+ const modelProviderSchema = z.object({ id: z.string(), name: z.string(), enabled: z.boolean() });
9
+ const modelSchema = z.object({
10
+ providerId: z.string(), modelId: z.string(), name: z.string(), enabled: z.boolean(),
11
+ contextWindow: z.number().nullable(), cost: modelCostSchema.nullable(),
12
+ generation: z.object({ maxOutputTokens: z.number().nullable() }),
13
+ capabilities: z.object({ reasoning: z.boolean(), tools: z.boolean(), thinkingLevels: z.array(thinkingSchema) }),
14
+ });
15
+ const roleSchema = z.object({ id: z.string(), enabled: z.boolean(), model: z.string().nullable(), thinking: thinkingSchema.nullable() });
16
+ export const ModelConfiguration = builtin.data("model.configuration", z.object({
17
+ modelProviders: z.array(modelProviderSchema), models: z.array(modelSchema), roles: z.array(roleSchema), agents: z.array(agentSchema),
18
+ }));
19
+ export type ModelConfigurationSnapshot = z.infer<typeof ModelConfiguration.schema>;
20
+ export type CatalogModel = z.infer<typeof modelSchema>;
21
+
22
+ export function resolveCatalogModel<M extends CatalogModel>(snapshot: Omit<ModelConfigurationSnapshot, "models"> & { models: M[] }, reference: string | null): M | null {
23
+ return snapshot.models.find(model => model.enabled && modelReference(model) === reference
24
+ && snapshot.modelProviders.some(provider => provider.id === model.providerId && provider.enabled)) ?? null;
25
+ }
26
+ export function resolveAgentModel<M extends CatalogModel>(snapshot: Omit<ModelConfigurationSnapshot, "models"> & { models: M[] }, selection: { agent: string | null; model: string | null }): M | null {
27
+ return resolveAgentModelSelection(snapshot, { ...selection, thinking: null }).model;
28
+ }
29
+
30
+ /** Display resolution against the current catalogs; preserves raw overrides and invalid references, while requests independently use the configuration at their boundaries. */
31
+ export function resolveAgentModelSelection<M extends CatalogModel>(
32
+ snapshot: Omit<ModelConfigurationSnapshot, "models"> & { models: M[] },
33
+ selection: Omit<AgentSelection, "agent"> & { agent: string | null },
34
+ ) {
35
+ const agent = snapshot.agents.find(item => item.id === selection.agent && item.enabled);
36
+ const role = snapshot.roles.find(item => item.id === (agent?.modelRole ?? "default"));
37
+ const resolved = resolveModelSelection(role, selection);
38
+ return {
39
+ reference: resolved.model,
40
+ model: agent && role?.enabled ? resolveCatalogModel(snapshot, resolved.model) : null,
41
+ thinking: resolved.thinking,
42
+ };
43
+ }
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+ import { modelExpressionSchema, thinkingSchema } from "@asterism-app/sdk/contracts";
3
+ const id = z.string().min(1).max(160);
4
+ /** Configurations use readable slugs for identification by both users and models. */
5
+ export const configurationIdSchema = z.string().trim().min(1).max(160).regex(/^[a-z0-9]+(?:[-_][a-z0-9]+)*$/u, "Use lowercase letters, digits, hyphens or underscores.");
6
+ const names = z.array(id).max(200);
7
+ const agentFieldsSchema = z.strictObject({
8
+ enabled: z.boolean(), description: z.string().max(20_000), systemPrompt: z.string().max(100_000),
9
+ modelRole: configurationIdSchema.nullable(), fallbackRole: configurationIdSchema.nullable(), examples: z.array(z.string().max(4_000)).max(50),
10
+ tools: names.nullable(), excludeTools: names.nullable(), subagentRoles: z.array(configurationIdSchema).max(200).nullable(),
11
+ maxTurns: z.number().int().positive().nullable(),
12
+ });
13
+ export const agentInputSchema = agentFieldsSchema.refine(value => value.tools === null || value.excludeTools === null, "Tool allowlist and denylist are mutually exclusive.");
14
+ export const agentSchema = agentInputSchema.safeExtend({ id: configurationIdSchema, builtin: z.boolean() });
15
+ // Blank text removes the override on save; false, null, and empty lists retain their respective business meanings.
16
+ export const agentOverridesSchema = agentFieldsSchema.partial()
17
+ .refine(value => value.tools == null || value.excludeTools == null, "Tool allowlist and denylist are mutually exclusive.");
18
+
19
+ export type AgentConfiguration = z.infer<typeof agentSchema>;
20
+ export type AgentOverrides = z.infer<typeof agentOverridesSchema>;
21
+ export const agentSelectionSchema = z.strictObject({
22
+ agent: configurationIdSchema.describe("ID of the configuration supplying this Agent's instructions and tool policy."),
23
+ model: modelExpressionSchema.nullable().describe("Model for this Agent as provider/model. Null uses the model resolved from its configuration."),
24
+ thinking: thinkingSchema.nullable().describe("Thinking level for this Agent. Null uses the default resolved from its configuration."),
25
+ }).describe("The Agent's base configuration and per-instance model settings.");
26
+ export type AgentSelection = z.infer<typeof agentSelectionSchema>;
@@ -0,0 +1,13 @@
1
+ export * from "./identity.ts";
2
+ export * from "./agent.ts";
3
+ export * from "./messaging.ts";
4
+ export * from "./text.ts";
5
+ export * from "./tools.ts";
6
+ export * from "./search.ts";
7
+ export * from "./configuration.ts";
8
+ export * from "./input.ts";
9
+ export * from "./records.ts";
10
+ export * from "./request-context.ts";
11
+ export * from "./naming.ts";
12
+ export * from "./catalogs.ts";
13
+ export * from "./creations.ts";
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ import { defineCall } from "@asterism-app/sdk/contracts";
3
+ import { builtin } from "./identity.ts";
4
+
5
+ /** The public association between model-facing creation entry points and resource types; excludes instance titles and execution permissions. */
6
+ export const ReadAgentCreations = defineCall(builtin.name("creations.read"), {
7
+ scope: "plugin",
8
+ input: z.strictObject({}),
9
+ output: z.array(z.strictObject({ id: z.string(), resourceKind: z.string() })),
10
+ });
@@ -0,0 +1,4 @@
1
+ import { pluginNamespace } from "@asterism-app/sdk/contracts";
2
+
3
+ /** The built-in feature plugin; all built-in modules declare contributions under its namespace. */
4
+ export const builtin = pluginNamespace("asterism.builtin");
package/src/input.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { modelInputPartsSchema, modelInputText, type ModelInputPart } from "@asterism-app/sdk/contracts";
2
+
3
+ /** Business limits for a single user input; model requests accumulate history separately. */
4
+ export const userInputPartsSchema = modelInputPartsSchema.max(128)
5
+ .refine(parts => parts.some(part => part.kind === "file" || part.text.trim().length > 0), "A message requires text or a file.")
6
+ .refine(parts => modelInputText(parts).length <= 100_000, "The message text exceeds the size limit.");
7
+ export function inputSummary(parts: readonly ModelInputPart[]): string {
8
+ return modelInputText(parts).trim() || parts.flatMap(part => part.kind === "file" ? [part.filename ?? part.mediaType] : []).join(", ");
9
+ }
package/src/manual.ts ADDED
@@ -0,0 +1,5 @@
1
+ /** One directory exposed under a distinct relative path in the manual; files remain at their original location. */
2
+ export interface ManualSource {
3
+ readonly path: string;
4
+ readonly directory: string;
5
+ }
@@ -0,0 +1,30 @@
1
+ import { z } from "zod";
2
+ import { defineTool } from "@asterism-app/sdk/contracts";
3
+ import { builtin } from "./identity.ts";
4
+
5
+ /** The sender retains the communication resource and agent name; recipient addressing is declared by the resource tool implementation. */
6
+ export const agentMessageSenderSchema = z.discriminatedUnion("kind", [
7
+ z.strictObject({ kind: z.literal("user") }),
8
+ z.strictObject({
9
+ kind: z.literal("agent"),
10
+ resource: z.strictObject({ sessionId: z.string().min(1), id: z.string().min(1), kind: z.string().min(1) }),
11
+ name: z.string().min(1),
12
+ }),
13
+ ]);
14
+ export type AgentMessageSender = z.infer<typeof agentMessageSenderSchema>;
15
+
16
+ /** The host matches resource types; target is declared by each resource implementation. */
17
+ export const Send = defineTool({
18
+ id: builtin.name("tool.send"), name: "send",
19
+ description: "Send a message to an Agent through a linked resource.",
20
+ guidelines: [
21
+ "resource_id selects the messaging resource. Its target schema describes recipient selection.",
22
+ "The runtime supplies the sender's resource address and Agent name.",
23
+ "Accepted messages enter the next request boundary while the recipient is running, or start execution while idle.",
24
+ "Acceptance confirms delivery to the input queue; the recipient processes the message independently.",
25
+ ],
26
+ argsSchema: z.strictObject({
27
+ message: z.string().min(1).refine(value => value.trim().length > 0, "A message must contain text.")
28
+ .describe("Message text for the recipient Agent."),
29
+ }).describe("The message to send."),
30
+ });
package/src/naming.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ import { defineRecord } from "@asterism-app/sdk/contracts";
3
+ import { builtin } from "./identity.ts";
4
+
5
+ /**
6
+ * Session-level naming events: append one for each naming operation that takes effect; existing events are not rewritten.
7
+ * Automatic naming includes the triggering message, level, and generating model; subsequent decisions read only the latest event.
8
+ */
9
+ export const SessionNamed = defineRecord(builtin.name("session.named"), {
10
+ schema: z.strictObject({
11
+ title: z.string().min(1).max(160),
12
+ source: z.enum(["user", "agent", "auto"]),
13
+ auto: z.strictObject({
14
+ level: z.enum(["initial", "refined"]),
15
+ messageId: z.string(),
16
+ model: z.string(),
17
+ }).optional(),
18
+ }).refine(data => (data.source === "auto") === (data.auto !== undefined), "Only automatic names carry naming metadata."),
19
+ });
20
+
21
+ export interface NamingApi {
22
+ nameByUser(sessionId: string, title: string): void;
23
+ }
package/src/records.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ import { defineRecord, modelBlockSchema, modelCompletionSchema, modelInputPartsSchema } from "@asterism-app/sdk/contracts";
3
+ import { builtin } from "./identity.ts";
4
+ import { agentMessageSenderSchema } from "./messaging.ts";
5
+
6
+ /** The record source of an input message is the receiving agent; the communication sender is represented independently by sender. */
7
+ export const AgentInputMessage = defineRecord(builtin.name("agent.input-message"), {
8
+ schema: z.object({ parts: modelInputPartsSchema, startsTurn: z.boolean(), sender: agentMessageSenderSchema }),
9
+ });
10
+ export const Execution = defineRecord(builtin.name("agent.execution"), {
11
+ schema: z.strictObject({ type: z.literal("block"), block: modelBlockSchema }),
12
+ });
13
+ /** A summary replaces only the prefix of model input; keptFrom points to the first history node retained unchanged. */
14
+ export const ContextSummary = defineRecord(builtin.name("agent.context-summary"), {
15
+ schema: z.strictObject({
16
+ summary: z.string().trim().min(1),
17
+ keptFrom: z.string().min(1),
18
+ model: z.string().min(1),
19
+ tokensBefore: z.number().nonnegative(),
20
+ completion: modelCompletionSchema,
21
+ }),
22
+ });
@@ -0,0 +1,112 @@
1
+ import { createContext, createElement, useContext, type ComponentProps, type ComponentType, type ReactNode } from "react";
2
+ import { pluginOf, pluginNamespace, type ThinkingLevel, type UiText } from "@asterism-app/sdk/contracts";
3
+ import type { AgentSelection } from "./configuration.ts";
4
+ import type { CatalogModel } from "./catalogs.ts";
5
+
6
+ /** One instance mounts per expanded Agent card; obtains the associated object through SDK useRendererObject. */
7
+ export interface AgentUIContribution {
8
+ readonly id: string;
9
+ readonly Component: ComponentType;
10
+ }
11
+
12
+ /** The frontend registry shared by the built-in implementation and consumers; business state remains owned by the contributing plugin. */
13
+ function createAgentUIRegistry() {
14
+ let entries: readonly AgentUIContribution[] = [];
15
+ const listeners = new Set<() => void>();
16
+ const notify = () => { for (const listener of listeners) listener(); };
17
+ return {
18
+ getSnapshot: () => entries,
19
+ subscribe(listener: () => void) { listeners.add(listener); return () => { listeners.delete(listener); }; },
20
+ /** Register in renderer.prepare; released through signal on preparation failure, timeout, or application exit. */
21
+ register(contribution: AgentUIContribution, { signal }: { signal: AbortSignal }): () => void {
22
+ signal.throwIfAborted();
23
+ const plugin = pluginOf(contribution.id);
24
+ if (!plugin) throw new Error(`Agent UI contribution needs a namespaced ID: ${contribution.id}`);
25
+ pluginNamespace(plugin).name(contribution.id.slice(plugin.length + 1));
26
+ if (entries.some(entry => entry.id === contribution.id))
27
+ throw new Error(`Duplicate Agent UI contribution: ${contribution.id}`);
28
+ const entry = Object.freeze({ ...contribution });
29
+ const remove = () => {
30
+ signal.removeEventListener("abort", remove);
31
+ if (!entries.includes(entry)) return;
32
+ entries = entries.filter(value => value !== entry);
33
+ notify();
34
+ };
35
+ signal.addEventListener("abort", remove, { once: true });
36
+ entries = [...entries, entry];
37
+ notify();
38
+ return remove;
39
+ },
40
+ };
41
+ }
42
+
43
+ export const agentUI = createAgentUIRegistry();
44
+
45
+ /** The current selection and catalog resolution result; does not represent the model used by an executing request and contains no credentials. */
46
+ export interface AgentModelSelection {
47
+ readonly selection: Readonly<AgentSelection> | null;
48
+ readonly reference: string | null;
49
+ readonly model: Readonly<CatalogModel> | null;
50
+ readonly thinking: ThinkingLevel | null;
51
+ }
52
+ const ModelSelection = createContext<AgentModelSelection | null>(null);
53
+ /** The built-in Agent injects a read-only selection for the same card and all its contributions. */
54
+ export const AgentModelSelectionProvider = ModelSelection.Provider;
55
+ export function useAgentModelSelection(): AgentModelSelection {
56
+ const selection = useContext(ModelSelection);
57
+ if (!selection) throw new Error("Agent model selection must be read inside an Agent card.");
58
+ return selection;
59
+ }
60
+
61
+ export interface AgentToolbarItemProps {
62
+ children: ReactNode;
63
+ }
64
+ export type AgentOverlayPosition = "top" | "center" | "bottom";
65
+ export interface AgentOverlayProps {
66
+ position: AgentOverlayPosition;
67
+ /** Hides and disables the input area while the UI exists; multiple requests are combined with OR. */
68
+ hideComposer?: boolean;
69
+ children: ReactNode;
70
+ }
71
+ export interface AgentModalProps {
72
+ open: boolean;
73
+ title: UiText;
74
+ description?: UiText;
75
+ /** Wide content, such as proposal comparisons, can explicitly use a wider standard modal. */
76
+ size?: "default" | "wide";
77
+ /** Closing is only a UI intent; the plugin decides whether to end the business wait. No close control is provided when omitted. */
78
+ onOpenChange?(open: boolean): void;
79
+ hideComposer?: boolean;
80
+ children: ReactNode;
81
+ }
82
+ /** The standard scroll viewport within an overlay/modal; the built-in implementation consistently reserves space for control focus rings. */
83
+ export interface AgentScrollAreaProps extends ComponentProps<"div"> {
84
+ axis?: "vertical" | "horizontal" | "both";
85
+ }
86
+
87
+ /** The built-in Agent injects the layout implementation; the public package does not depend on implementation packages or control libraries. */
88
+ export interface AgentUIRenderer {
89
+ ToolbarItem: ComponentType<AgentToolbarItemProps>;
90
+ Overlay: ComponentType<AgentOverlayProps>;
91
+ Modal: ComponentType<AgentModalProps>;
92
+ ScrollArea: ComponentType<AgentScrollAreaProps>;
93
+ }
94
+ const Renderer = createContext<AgentUIRenderer | null>(null);
95
+ export const AgentUIRendererProvider = Renderer.Provider;
96
+ function useAgentUIRenderer(): AgentUIRenderer {
97
+ const renderer = useContext(Renderer);
98
+ if (!renderer) throw new Error("Agent UI must be rendered inside an Agent card.");
99
+ return renderer;
100
+ }
101
+ export function AgentOverlay(props: AgentOverlayProps): ReactNode {
102
+ return createElement(useAgentUIRenderer().Overlay, props);
103
+ }
104
+ export function AgentToolbarItem(props: AgentToolbarItemProps): ReactNode {
105
+ return createElement(useAgentUIRenderer().ToolbarItem, props);
106
+ }
107
+ export function AgentModal(props: AgentModalProps): ReactNode {
108
+ return createElement(useAgentUIRenderer().Modal, props);
109
+ }
110
+ export function AgentScrollArea(props: AgentScrollAreaProps): ReactNode {
111
+ return createElement(useAgentUIRenderer().ScrollArea, props);
112
+ }
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import { builtin } from "./identity.ts";
3
+
4
+ const characters = z.number().int().nonnegative();
5
+
6
+ /** Structural measurements of a work request, saved with completion; character counts use Unicode code points. */
7
+ export const AgentRequestContext = builtin.data("agent.request-context", z.object({
8
+ system: z.array(z.discriminatedUnion("kind", [
9
+ z.strictObject({ kind: z.literal("system"), from: z.string(), characters }),
10
+ z.strictObject({ kind: z.literal("agent"), role: z.string(), characters }),
11
+ z.strictObject({ kind: z.literal("context"), file: z.string(), characters }),
12
+ ])).optional(),
13
+ tools: z.array(z.string()).optional(),
14
+ }));
15
+ export type AgentRequestContextData = z.infer<typeof AgentRequestContext.schema>;
package/src/runtime.ts ADDED
@@ -0,0 +1,82 @@
1
+ import { z } from "zod";
2
+ import { belongsTo, jsonSchema, type CreationContract, type Json, type SessionDirectory } from "@asterism-app/sdk/contracts";
3
+ import { defineModule, PluginApiError, type PluginContext, type PluginModule, type ObjectReader, type TopLevelToolContext } from "@asterism-app/sdk/runtime";
4
+
5
+ export type { ManualSource } from "./manual.ts";
6
+
7
+ export interface AgentCreationDescription {
8
+ readonly id: string;
9
+ readonly resourceKind: string;
10
+ readonly description: string;
11
+ readonly guidelines: readonly string[];
12
+ readonly input: Json;
13
+ }
14
+ interface Entry {
15
+ readonly description: AgentCreationDescription;
16
+ create(input: Json, context: TopLevelToolContext): ReturnType<TopLevelToolContext["session"]["create"]>;
17
+ }
18
+ type CreationOptions = Parameters<TopLevelToolContext["session"]["create"]>[2];
19
+ /** Catalogs are isolated by host runtime instance; shared public exports do not mix plugin registrations from different hosts. */
20
+ const directories = new WeakMap<SessionDirectory, Map<string, Entry>>();
21
+ function directory(plugin: PluginContext): Map<string, Entry> {
22
+ plugin.lifetime.signal.throwIfAborted();
23
+ let entries = directories.get(plugin.sessions);
24
+ if (!entries) { entries = new Map(); directories.set(plugin.sessions, entries); }
25
+ return entries;
26
+ }
27
+ function entry(plugin: PluginContext, id: string): Entry {
28
+ const value = directory(plugin).get(id);
29
+ if (!value) throw new PluginApiError("not_found", "This Agent creation entry is unavailable. Read creation_list for available entries.");
30
+ return value;
31
+ }
32
+
33
+ /** The domain service shared by built-in creation tools and plugins; querying is not a prerequisite for creation. */
34
+ export const agentCreations = {
35
+ list(plugin: PluginContext) {
36
+ return [...directory(plugin).values()].map(({ description: { id, resourceKind, description } }) => ({ id, resourceKind, description }));
37
+ },
38
+ inspect(plugin: PluginContext, id: string): AgentCreationDescription { return structuredClone(entry(plugin, id).description); },
39
+ create(plugin: PluginContext, id: string, input: Json, context: TopLevelToolContext) {
40
+ return entry(plugin, id).create(input, context);
41
+ },
42
+ };
43
+
44
+ /** Agent creation entry points are independent domain contributions; they reuse creation input directly or explicitly adapt model input. */
45
+ export function defineAgentCreation<I, S extends Json, O extends Json, A extends Json = Json>(definition: {
46
+ id: string;
47
+ description: string;
48
+ guidelines?: readonly string[];
49
+ creation: CreationContract<I, S, O>;
50
+ } & ({ inputSchema?: never; toInput?: never; toOptions?: never } | {
51
+ inputSchema: z.ZodType<A>;
52
+ toInput(input: A, context: { object: ObjectReader }): I & Json;
53
+ toOptions?(input: A, context: { object: ObjectReader }): CreationOptions;
54
+ })): PluginModule {
55
+ return defineModule(plugin => {
56
+ if (!belongsTo(plugin.id, definition.id)) throw new PluginApiError("invalid_argument", "The Agent creation entry belongs to another plugin.");
57
+ if (!definition.description.trim()) throw new PluginApiError("invalid_argument", "An Agent creation entry needs a description.");
58
+ const entries = directory(plugin);
59
+ if (entries.has(definition.id)) throw new PluginApiError("conflict", `Duplicate Agent creation entry: ${definition.id}`);
60
+ const input = jsonSchema.parse(z.toJSONSchema(definition.inputSchema ?? definition.creation.schema, { io: "input" }));
61
+ const value: Entry = {
62
+ description: { id: definition.id, resourceKind: definition.creation.resource.kind,
63
+ description: definition.description, guidelines: [...definition.guidelines ?? []], input },
64
+ create(raw, context) {
65
+ plugin.lifetime.signal.throwIfAborted(); context.signal.throwIfAborted();
66
+ let converted: I & Json;
67
+ let options: CreationOptions;
68
+ if (definition.inputSchema) {
69
+ const parsed = definition.inputSchema.safeParse(raw);
70
+ if (!parsed.success) throw new PluginApiError("invalid_argument", parsed.error.message);
71
+ converted = jsonSchema.parse(definition.toInput(parsed.data, { object: context.object })) as I & Json;
72
+ options = definition.toOptions?.(parsed.data, { object: context.object });
73
+ } else converted = raw as I & Json;
74
+ return context.session.create(definition.creation, converted, options);
75
+ },
76
+ };
77
+ entries.set(definition.id, value);
78
+ // Remove registrations as soon as the plugin aborts: a plugin whose assembly failed must not remain publicly queryable during asynchronous cleanup.
79
+ plugin.lifetime.signal.addEventListener("abort", () => { if (entries.get(definition.id) === value) entries.delete(definition.id); }, { once: true });
80
+ return {};
81
+ });
82
+ }
package/src/search.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { z } from "zod";
2
+ import { defineTool } from "@asterism-app/sdk/contracts";
3
+ import { builtin } from "./identity.ts";
4
+
5
+ const pattern = z.string().min(1).refine(value => !value.includes("\0"), "Patterns must be free of NUL characters.");
6
+ const selection = {
7
+ hidden: z.boolean().optional().describe("Disable hidden-entry filtering; defaults to false. Ignore-file filtering is controlled separately by noIgnore."),
8
+ noIgnore: z.boolean().optional().describe("Disable ignore-file filtering; defaults to false. Hidden-entry filtering is controlled separately by hidden."),
9
+ limit: z.number().int().positive().max(2000).optional().describe("Maximum returned matching lines in grep content mode, or files otherwise; defaults to 500, maximum 2000. Context lines are bounded only by the output-byte budget. Matching lines with omitted content count toward the limit."),
10
+ };
11
+ const globGuidelines = [
12
+ "Globs use ripgrep semantics. A pattern without '/' such as '*.md' matches filenames at any depth.",
13
+ "Positive globs override hidden and ignore rules for entries they match. A directory that remains excluded is skipped with its descendants.",
14
+ ];
15
+
16
+ export const Find = defineTool({
17
+ id: builtin.name("tool.find"), name: "find",
18
+ description: "Find files by glob pattern within a resource.",
19
+ guidelines: globGuidelines,
20
+ argsSchema: z.strictObject({
21
+ pattern: pattern.describe("File glob relative to the selected search directory, using '/' separators; for example '**/*.ts' or '**/package.json'."),
22
+ ...selection,
23
+ }).describe("A file glob and search selection options."),
24
+ });
25
+
26
+ export const Grep = defineTool({
27
+ id: builtin.name("tool.grep"), name: "grep",
28
+ description: "Search file content for a regular expression or literal text within a resource.",
29
+ guidelines: [
30
+ "content mode groups matching and context lines under a per-file heading; when context lines are present, the heading lists the matching line numbers. files mode returns matching paths; count mode returns the number of matching lines per file.",
31
+ "Regular expressions use ripgrep's default Rust regex syntax. Matching is line-oriented.",
32
+ "Complete UTF-8 content rows include LINE#HASH edit anchors by default, shared with read and edit.",
33
+ ...globGuidelines,
34
+ ],
35
+ argsSchema: z.strictObject({
36
+ pattern: pattern.describe("Regular expression to search for, or literal text when literal is true."),
37
+ literal: z.boolean().optional().describe("Interpret pattern as literal text; defaults to false."),
38
+ ignoreCase: z.boolean().optional().describe("Use case-insensitive matching; defaults to false."),
39
+ word: z.boolean().optional().describe("Match only at word boundaries; defaults to false."),
40
+ glob: z.array(pattern).optional().describe("File globs relative to the search directory. Prefix exclusions with '!'; later globs take precedence."),
41
+ context: z.number().int().min(0).max(20).optional().describe("Lines before and after each match in content mode; defaults to 0."),
42
+ outputMode: z.enum(["content", "files", "count"]).optional().describe("Result format; defaults to content. Count means matching lines, rather than regex occurrences."),
43
+ anchored: z.boolean().optional().describe("Include edit anchors in content mode; defaults to true."),
44
+ ...selection,
45
+ }).describe("Content pattern, file selection and output options."),
46
+ });
47
+
48
+ export type FindArgs = z.infer<typeof Find.argsSchema>;
49
+ export type GrepArgs = z.infer<typeof Grep.argsSchema>;
package/src/text.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { z } from "zod";
2
+
3
+ export const lineAnchorSchema = z.strictObject({
4
+ line: z.number().int().positive().describe("1-based line number copied from an anchored read or edit result."),
5
+ hash: z.string().regex(/^[0-9A-HJKMNP-TV-Z]{4}$/).describe("The exact four-character content hash after # in an anchored read or edit result."),
6
+ }).describe("A LINE#HASH reference to the current content at one line.");
7
+ const body = z.array(z.string().regex(/^[^\r\n]*$/).describe("One content line without a line terminator; an empty string represents a blank line."))
8
+ .min(1).describe("New text lines to insert or use as replacement content.");
9
+ const anchor = lineAnchorSchema.describe("The first affected line, or the preserved line next to an insertion.");
10
+ const end = lineAnchorSchema.optional().describe("Inclusive last line for replace/delete. Omission selects a single line. Validation checks the cited endpoints.");
11
+ export const editOperationSchema = z.discriminatedUnion("op", [
12
+ z.strictObject({ op: z.literal("replace"), anchor, end, body }),
13
+ z.strictObject({ op: z.literal("delete"), anchor, end }),
14
+ z.strictObject({ op: z.literal("insert_after"), anchor, body }),
15
+ z.strictObject({ op: z.literal("insert_before"), anchor, body }),
16
+ z.strictObject({ op: z.literal("append"), body }),
17
+ z.strictObject({ op: z.literal("prepend"), body }),
18
+ ]).describe("Replace/delete an inclusive anchored range, insert beside a preserved anchor, or append/prepend without anchors.");
19
+
20
+ export const textRowSchema = z.strictObject({
21
+ line: z.number().int().positive().describe("The absolute 1-based line number in the complete text."),
22
+ content: z.string().describe("The line content without its terminator; for an excerpt, only the excerpt window."),
23
+ hash: z.string().nullable().describe("LINE#HASH content hash of the complete line, also for an excerpt; null when anchors were not requested."),
24
+ excerpt: z.strictObject({
25
+ start: z.number().int().nonnegative().describe("UTF-16 offset of content within the complete line."),
26
+ length: z.number().int().positive().describe("UTF-16 length of the complete line."),
27
+ }).nullable().describe("Present when the line is too long to return in full; content is then a window of the complete line. Null for a complete line."),
28
+ });
29
+ export type TextRow = z.infer<typeof textRowSchema>;
30
+ export const textReadSchema = z.strictObject({
31
+ totalLines: z.number().int().nonnegative().describe("Number of logical lines in the complete text; empty text has zero lines."),
32
+ fromLine: z.number().int().positive().describe("First requested line, counted from 1 in the complete text."),
33
+ rows: z.array(textRowSchema).describe("Selected lines and their available edit anchors; long lines are returned as excerpts."),
34
+ noFinalNewline: z.boolean().describe("Whether non-empty text lacks a final newline."),
35
+ nextOffset: z.number().int().positive().nullable().describe("First unread line after the returned range, or null at end of text. Use as target.offset only when this implementation supports line selection."),
36
+ }).describe("Selected text lines and edit anchors, final-newline information and a continuation position.");
37
+
38
+ export const textChangeSchema = z.strictObject({
39
+ changed: z.boolean().describe("Whether the target was created or its content changed."),
40
+ diff: z.string().describe("The actual unified diff, preserving final-newline and CRLF differences."),
41
+ added: z.number().int().nonnegative().describe("Number of added lines in the actual diff."),
42
+ removed: z.number().int().nonnegative().describe("Number of removed lines in the actual diff."),
43
+ anchors: z.array(textRowSchema).describe("Fresh anchors for lines produced by edit, including a line shifted into a deletion gap. Always empty for write."),
44
+ anchorsTruncated: z.boolean().describe("True when the fresh anchors cover a subset of the changed lines."),
45
+ }).describe("The committed content change with an actual diff and bounded fresh edit anchors. write returns an empty anchors array.");
46
+ export type TextRead = z.infer<typeof textReadSchema>;
47
+ export type TextChange = z.infer<typeof textChangeSchema>;
package/src/tools.ts ADDED
@@ -0,0 +1,88 @@
1
+ import { z } from "zod";
2
+ import { defineTool } from "@asterism-app/sdk/contracts";
3
+ import { builtin } from "./identity.ts";
4
+ import { editOperationSchema } from "./text.ts";
5
+
6
+ /**
7
+ * Public resource tools: declarations specify only parameters and semantics; each resource implementation declares what target selects and what results contain.
8
+ * Third-party resources can implement the same declaration, letting models operate on different resources through the same tool name.
9
+ */
10
+ export const List = defineTool({
11
+ id: builtin.name("tool.list"),
12
+ name: "list",
13
+ description: "List the entries in a container to discover its contents and entry types.",
14
+ guidelines: [
15
+ "target selects a container according to the resource implementation's schema. args is an empty object.",
16
+ "The result describes immediate entries and includes a truncation indicator.",
17
+ ],
18
+ argsSchema: z.strictObject({}).describe("Empty parameter object."),
19
+ });
20
+
21
+ export const Read = defineTool({
22
+ id: builtin.name("tool.read"),
23
+ name: "read",
24
+ description: "Read the selected content as text.",
25
+ guidelines: [
26
+ "target selects the content and its range according to the resource implementation's schema.",
27
+ "Editable text includes LINE#HASH edit anchors by default; args:{\"anchored\":false} omits them. Content that cannot be edited has no anchors.",
28
+ "The result reports positions and a continuation point where the implementation supports ranges.",
29
+ ],
30
+ argsSchema: z.strictObject({
31
+ anchored: z.boolean().optional().describe("Include edit anchors for editable text; defaults to true."),
32
+ }).describe("Reading behavior. Content selection and range belong to target."),
33
+ });
34
+
35
+ export const Edit = defineTool({
36
+ id: builtin.name("tool.edit"),
37
+ name: "edit",
38
+ description: "Apply a batch of line-anchored text edits to an existing target, and return the actual diff and fresh anchors.",
39
+ guidelines: [
40
+ "target selects existing editable text according to the resource implementation's schema. args.edits contains the operations.",
41
+ "Anchors contain a line number and its content hash, as returned by read or edit.",
42
+ "replace/delete affect the inclusive anchor..end range. An omitted end selects one line. Validation checks the cited endpoints.",
43
+ "insert_before/insert_after preserve the anchor line. body contains the new lines.",
44
+ "append/prepend take body. Each body item is one text line; an empty string represents a blank line.",
45
+ "All operations refer to the same pre-edit content and commit as one batch. Anchor or overlap failures preserve the original text.",
46
+ "Anchor mismatch results include current-line or shifted candidates. Successful results include the actual diff and fresh anchors.",
47
+ ],
48
+ argsSchema: z.strictObject({
49
+ edits: z.array(editOperationSchema).min(1).max(500).describe("One to 500 non-overlapping hashline operations, all referring to the same current text."),
50
+ }).describe("A complete batch of line-based edit operations. The target is selected separately."),
51
+ });
52
+
53
+ export const Write = defineTool({
54
+ id: builtin.name("tool.write"),
55
+ name: "write",
56
+ description: "Write the complete text to a target, creating it where supported or overwriting its existing content.",
57
+ guidelines: [
58
+ "args.content is the complete desired text, including line endings and final newline.",
59
+ "target selects the destination according to the resource implementation's schema, which states whether missing targets are created.",
60
+ "An existing target is overwritten. Success returns a brief confirmation and change statistics.",
61
+ ],
62
+ argsSchema: z.strictObject({
63
+ content: z.string().describe("The complete new UTF-8 text. Empty content clears the target."),
64
+ }).describe("Full replacement text. The implementation specifies its content size limit."),
65
+ });
66
+
67
+ export const See = defineTool({
68
+ id: builtin.name("tool.see"),
69
+ name: "see",
70
+ description: "View an image so its pixels are attached to the tool result for you to inspect.",
71
+ guidelines: [
72
+ "target selects the image according to the resource implementation's schema. args is an empty object.",
73
+ ],
74
+ argsSchema: z.strictObject({}).describe("Empty parameter object."),
75
+ });
76
+
77
+ export const Wait = defineTool({
78
+ id: builtin.name("tool.wait"),
79
+ name: "wait",
80
+ description: "Wait until a resource reaches its resting state, up to a timeout.",
81
+ guidelines: [
82
+ "The resource implementation defines its resting state. A resource already at rest returns immediately.",
83
+ "Timeout or cancellation ends only the wait; the resource continues its own work. The result reports the observed state.",
84
+ ],
85
+ argsSchema: z.strictObject({
86
+ timeout: z.number().positive().max(2147483).describe("Maximum wait in seconds."),
87
+ }).describe("A bounded wait."),
88
+ });