فهرست منبع

refactor(schema): tighten public contracts (#33771)

Kit Langton 1 ماه پیش
والد
کامیت
9e9d405d7e
49فایلهای تغییر یافته به همراه744 افزوده شده و 798 حذف شده
  1. 155 192
      packages/client/src/generated/types.ts
  2. 7 5
      packages/core/src/config/plugin/reference.ts
  3. 14 10
      packages/core/src/plugin/host.ts
  4. 9 9
      packages/core/src/reference.ts
  5. 3 2
      packages/core/src/v1/config/migrate.ts
  6. 0 1
      packages/core/test/config/config.test.ts
  7. 3 1
      packages/core/test/tool-todowrite.test.ts
  8. 2 2
      packages/opencode/src/session/todo.ts
  9. 1 12
      packages/opencode/src/tool/todo.ts
  10. 1 1
      packages/opencode/test/server/httpapi-exercise/index.ts
  11. 5 1
      packages/opencode/test/server/httpapi-exercise/types.ts
  12. 0 8
      packages/opencode/test/server/httpapi-reference.test.ts
  13. 1 1
      packages/opencode/test/session/schema-decoding.test.ts
  14. 7 6
      packages/schema/src/agent.ts
  15. 5 4
      packages/schema/src/command.ts
  16. 3 2
      packages/schema/src/credential.ts
  17. 4 5
      packages/schema/src/event.ts
  18. 5 4
      packages/schema/src/file-diff.ts
  19. 4 3
      packages/schema/src/filesystem.ts
  20. 18 17
      packages/schema/src/integration.ts
  21. 8 3
      packages/schema/src/llm.ts
  22. 2 5
      packages/schema/src/location.ts
  23. 17 14
      packages/schema/src/model.ts
  24. 1 1
      packages/schema/src/permission-saved.ts
  25. 6 5
      packages/schema/src/permission.ts
  26. 5 4
      packages/schema/src/project-copy.ts
  27. 8 5
      packages/schema/src/project.ts
  28. 7 6
      packages/schema/src/prompt.ts
  29. 13 10
      packages/schema/src/provider.ts
  30. 2 1
      packages/schema/src/pty-ticket.ts
  31. 19 13
      packages/schema/src/pty.ts
  32. 17 10
      packages/schema/src/question.ts
  33. 11 8
      packages/schema/src/reference.ts
  34. 8 7
      packages/schema/src/revert.ts
  35. 1 1
      packages/schema/src/schema.ts
  36. 21 20
      packages/schema/src/session-event.ts
  37. 2 1
      packages/schema/src/session-input.ts
  38. 26 25
      packages/schema/src/session-message.ts
  39. 3 2
      packages/schema/src/session-status-event.ts
  40. 4 2
      packages/schema/src/session-todo.ts
  41. 7 7
      packages/schema/src/session.ts
  42. 3 2
      packages/schema/src/skill.ts
  43. 2 1
      packages/schema/src/tui-event.ts
  44. 2 1
      packages/schema/src/vcs-event.ts
  45. 2 2
      packages/schema/src/workspace-event.ts
  46. 2 1
      packages/schema/src/worktree-event.ts
  47. 67 0
      packages/schema/test/contract-hygiene.test.ts
  48. 7 21
      packages/sdk/js/src/v2/gen/sdk.gen.ts
  49. 224 334
      packages/sdk/js/src/v2/gen/types.gen.ts

+ 155 - 192
packages/client/src/generated/types.ts

@@ -152,8 +152,8 @@ export type SessionsListOutput = {
     readonly id: string
     readonly parentID?: string
     readonly projectID: string
-    readonly agent?: string | null
-    readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string | null } | null
+    readonly agent?: string
+    readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
     readonly cost: number
     readonly tokens: {
       readonly input: number
@@ -161,23 +161,23 @@ export type SessionsListOutput = {
       readonly reasoning: number
       readonly cache: { readonly read: number; readonly write: number }
     }
-    readonly time: { readonly created: number; readonly updated: number; readonly archived?: number | null }
+    readonly time: { readonly created: number; readonly updated: number; readonly archived?: number }
     readonly title: string
-    readonly location: { readonly directory: string; readonly workspaceID?: string | null | null }
-    readonly subpath?: string | null
+    readonly location: { readonly directory: string; readonly workspaceID?: string }
+    readonly subpath?: string
     readonly revert?: {
       readonly messageID: string
-      readonly partID?: string | null
-      readonly snapshot?: string | null
-      readonly diff?: string | null
+      readonly partID?: string
+      readonly snapshot?: string
+      readonly diff?: string
       readonly files?: ReadonlyArray<{
         readonly path: string
         readonly status: "added" | "modified" | "deleted"
         readonly additions: number
         readonly deletions: number
         readonly patch: string
-      }> | null
-    } | null
+      }>
+    }
   }>
   readonly cursor: { readonly previous?: string | null; readonly next?: string | null }
 }
@@ -186,26 +186,26 @@ export type SessionsCreateInput = {
   readonly id?: {
     readonly id?: string | null
     readonly agent?: string | null
-    readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string | null } | null
-    readonly location?: { readonly directory: string; readonly workspaceID?: string | null | null } | null
+    readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
+    readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
   }["id"]
   readonly agent?: {
     readonly id?: string | null
     readonly agent?: string | null
-    readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string | null } | null
-    readonly location?: { readonly directory: string; readonly workspaceID?: string | null | null } | null
+    readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
+    readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
   }["agent"]
   readonly model?: {
     readonly id?: string | null
     readonly agent?: string | null
-    readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string | null } | null
-    readonly location?: { readonly directory: string; readonly workspaceID?: string | null | null } | null
+    readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
+    readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
   }["model"]
   readonly location?: {
     readonly id?: string | null
     readonly agent?: string | null
-    readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string | null } | null
-    readonly location?: { readonly directory: string; readonly workspaceID?: string | null | null } | null
+    readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
+    readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
   }["location"]
 }
 
@@ -214,8 +214,8 @@ export type SessionsCreateOutput = {
     readonly id: string
     readonly parentID?: string
     readonly projectID: string
-    readonly agent?: string | null
-    readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string | null } | null
+    readonly agent?: string
+    readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
     readonly cost: number
     readonly tokens: {
       readonly input: number
@@ -223,23 +223,23 @@ export type SessionsCreateOutput = {
       readonly reasoning: number
       readonly cache: { readonly read: number; readonly write: number }
     }
-    readonly time: { readonly created: number; readonly updated: number; readonly archived?: number | null }
+    readonly time: { readonly created: number; readonly updated: number; readonly archived?: number }
     readonly title: string
-    readonly location: { readonly directory: string; readonly workspaceID?: string | null | null }
-    readonly subpath?: string | null
+    readonly location: { readonly directory: string; readonly workspaceID?: string }
+    readonly subpath?: string
     readonly revert?: {
       readonly messageID: string
-      readonly partID?: string | null
-      readonly snapshot?: string | null
-      readonly diff?: string | null
+      readonly partID?: string
+      readonly snapshot?: string
+      readonly diff?: string
       readonly files?: ReadonlyArray<{
         readonly path: string
         readonly status: "added" | "modified" | "deleted"
         readonly additions: number
         readonly deletions: number
         readonly patch: string
-      }> | null
-    } | null
+      }>
+    }
   }
 }["data"]
 
@@ -250,8 +250,8 @@ export type SessionsGetOutput = {
     readonly id: string
     readonly parentID?: string
     readonly projectID: string
-    readonly agent?: string | null
-    readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string | null } | null
+    readonly agent?: string
+    readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
     readonly cost: number
     readonly tokens: {
       readonly input: number
@@ -259,23 +259,23 @@ export type SessionsGetOutput = {
       readonly reasoning: number
       readonly cache: { readonly read: number; readonly write: number }
     }
-    readonly time: { readonly created: number; readonly updated: number; readonly archived?: number | null }
+    readonly time: { readonly created: number; readonly updated: number; readonly archived?: number }
     readonly title: string
-    readonly location: { readonly directory: string; readonly workspaceID?: string | null | null }
-    readonly subpath?: string | null
+    readonly location: { readonly directory: string; readonly workspaceID?: string }
+    readonly subpath?: string
     readonly revert?: {
       readonly messageID: string
-      readonly partID?: string | null
-      readonly snapshot?: string | null
-      readonly diff?: string | null
+      readonly partID?: string
+      readonly snapshot?: string
+      readonly diff?: string
       readonly files?: ReadonlyArray<{
         readonly path: string
         readonly status: "added" | "modified" | "deleted"
         readonly additions: number
         readonly deletions: number
         readonly patch: string
-      }> | null
-    } | null
+      }>
+    }
   }
 }["data"]
 
@@ -289,7 +289,7 @@ export type SessionsSwitchAgentOutput = void
 export type SessionsSwitchModelInput = {
   readonly sessionID: { readonly sessionID: string }["sessionID"]
   readonly model: {
-    readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string | undefined }
+    readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
   }["model"]
 }
 
@@ -298,96 +298,80 @@ export type SessionsSwitchModelOutput = void
 export type SessionsPromptInput = {
   readonly sessionID: { readonly sessionID: string }["sessionID"]
   readonly id?: {
-    readonly id?: string | undefined
+    readonly id?: string | null
     readonly prompt: {
       readonly text: string
-      readonly files?:
-        | ReadonlyArray<{
-            readonly uri: string
-            readonly mime: string
-            readonly name?: string | undefined
-            readonly description?: string | undefined
-            readonly source?: { readonly start: number; readonly end: number; readonly text: string } | undefined
-          }>
-        | undefined
-      readonly agents?:
-        | ReadonlyArray<{
-            readonly name: string
-            readonly source?: { readonly start: number; readonly end: number; readonly text: string } | undefined
-          }>
-        | undefined
+      readonly files?: ReadonlyArray<{
+        readonly uri: string
+        readonly mime: string
+        readonly name?: string
+        readonly description?: string
+        readonly source?: { readonly start: number; readonly end: number; readonly text: string }
+      }>
+      readonly agents?: ReadonlyArray<{
+        readonly name: string
+        readonly source?: { readonly start: number; readonly end: number; readonly text: string }
+      }>
     }
-    readonly delivery?: "steer" | "queue" | undefined
-    readonly resume?: boolean | undefined
+    readonly delivery?: "steer" | "queue" | null
+    readonly resume?: boolean | null
   }["id"]
   readonly prompt: {
-    readonly id?: string | undefined
+    readonly id?: string | null
     readonly prompt: {
       readonly text: string
-      readonly files?:
-        | ReadonlyArray<{
-            readonly uri: string
-            readonly mime: string
-            readonly name?: string | undefined
-            readonly description?: string | undefined
-            readonly source?: { readonly start: number; readonly end: number; readonly text: string } | undefined
-          }>
-        | undefined
-      readonly agents?:
-        | ReadonlyArray<{
-            readonly name: string
-            readonly source?: { readonly start: number; readonly end: number; readonly text: string } | undefined
-          }>
-        | undefined
+      readonly files?: ReadonlyArray<{
+        readonly uri: string
+        readonly mime: string
+        readonly name?: string
+        readonly description?: string
+        readonly source?: { readonly start: number; readonly end: number; readonly text: string }
+      }>
+      readonly agents?: ReadonlyArray<{
+        readonly name: string
+        readonly source?: { readonly start: number; readonly end: number; readonly text: string }
+      }>
     }
-    readonly delivery?: "steer" | "queue" | undefined
-    readonly resume?: boolean | undefined
+    readonly delivery?: "steer" | "queue" | null
+    readonly resume?: boolean | null
   }["prompt"]
   readonly delivery?: {
-    readonly id?: string | undefined
+    readonly id?: string | null
     readonly prompt: {
       readonly text: string
-      readonly files?:
-        | ReadonlyArray<{
-            readonly uri: string
-            readonly mime: string
-            readonly name?: string | undefined
-            readonly description?: string | undefined
-            readonly source?: { readonly start: number; readonly end: number; readonly text: string } | undefined
-          }>
-        | undefined
-      readonly agents?:
-        | ReadonlyArray<{
-            readonly name: string
-            readonly source?: { readonly start: number; readonly end: number; readonly text: string } | undefined
-          }>
-        | undefined
+      readonly files?: ReadonlyArray<{
+        readonly uri: string
+        readonly mime: string
+        readonly name?: string
+        readonly description?: string
+        readonly source?: { readonly start: number; readonly end: number; readonly text: string }
+      }>
+      readonly agents?: ReadonlyArray<{
+        readonly name: string
+        readonly source?: { readonly start: number; readonly end: number; readonly text: string }
+      }>
     }
-    readonly delivery?: "steer" | "queue" | undefined
-    readonly resume?: boolean | undefined
+    readonly delivery?: "steer" | "queue" | null
+    readonly resume?: boolean | null
   }["delivery"]
   readonly resume?: {
-    readonly id?: string | undefined
+    readonly id?: string | null
     readonly prompt: {
       readonly text: string
-      readonly files?:
-        | ReadonlyArray<{
-            readonly uri: string
-            readonly mime: string
-            readonly name?: string | undefined
-            readonly description?: string | undefined
-            readonly source?: { readonly start: number; readonly end: number; readonly text: string } | undefined
-          }>
-        | undefined
-      readonly agents?:
-        | ReadonlyArray<{
-            readonly name: string
-            readonly source?: { readonly start: number; readonly end: number; readonly text: string } | undefined
-          }>
-        | undefined
+      readonly files?: ReadonlyArray<{
+        readonly uri: string
+        readonly mime: string
+        readonly name?: string
+        readonly description?: string
+        readonly source?: { readonly start: number; readonly end: number; readonly text: string }
+      }>
+      readonly agents?: ReadonlyArray<{
+        readonly name: string
+        readonly source?: { readonly start: number; readonly end: number; readonly text: string }
+      }>
     }
-    readonly delivery?: "steer" | "queue" | undefined
-    readonly resume?: boolean | undefined
+    readonly delivery?: "steer" | "queue" | null
+    readonly resume?: boolean | null
   }["resume"]
 }
 
@@ -401,18 +385,18 @@ export type SessionsPromptOutput = {
       readonly files?: ReadonlyArray<{
         readonly uri: string
         readonly mime: string
-        readonly name?: string | null
-        readonly description?: string | null
-        readonly source?: { readonly start: number; readonly end: number; readonly text: string } | null
-      }> | null
+        readonly name?: string
+        readonly description?: string
+        readonly source?: { readonly start: number; readonly end: number; readonly text: string }
+      }>
       readonly agents?: ReadonlyArray<{
         readonly name: string
-        readonly source?: { readonly start: number; readonly end: number; readonly text: string } | null
-      }> | null
+        readonly source?: { readonly start: number; readonly end: number; readonly text: string }
+      }>
     }
     readonly delivery: "steer" | "queue"
     readonly timeCreated: number
-    readonly promotedSeq?: number | null
+    readonly promotedSeq?: number
   }
 }["data"]
 
@@ -433,18 +417,16 @@ export type SessionsStageInput = {
 export type SessionsStageOutput = {
   readonly data: {
     readonly messageID: string
-    readonly partID?: string | undefined
-    readonly snapshot?: string | undefined
-    readonly diff?: string | undefined
-    readonly files?:
-      | ReadonlyArray<{
-          readonly path: string
-          readonly status: "added" | "modified" | "deleted"
-          readonly additions: number
-          readonly deletions: number
-          readonly patch: string
-        }>
-      | undefined
+    readonly partID?: string
+    readonly snapshot?: string
+    readonly diff?: string
+    readonly files?: ReadonlyArray<{
+      readonly path: string
+      readonly status: "added" | "modified" | "deleted"
+      readonly additions: number
+      readonly deletions: number
+      readonly patch: string
+    }>
   }
 }["data"]
 
@@ -462,39 +444,39 @@ export type SessionsContextOutput = {
   readonly data: ReadonlyArray<
     | {
         readonly id: string
-        readonly metadata?: { readonly [x: string]: JsonValue } | null
+        readonly metadata?: { readonly [x: string]: JsonValue }
         readonly time: { readonly created: number }
         readonly type: "agent-switched"
         readonly agent: string
       }
     | {
         readonly id: string
-        readonly metadata?: { readonly [x: string]: JsonValue } | null
+        readonly metadata?: { readonly [x: string]: JsonValue }
         readonly time: { readonly created: number }
         readonly type: "model-switched"
-        readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string | null }
+        readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
       }
     | {
         readonly id: string
-        readonly metadata?: { readonly [x: string]: JsonValue } | null
+        readonly metadata?: { readonly [x: string]: JsonValue }
         readonly time: { readonly created: number }
         readonly text: string
         readonly files?: ReadonlyArray<{
           readonly uri: string
           readonly mime: string
-          readonly name?: string | null
-          readonly description?: string | null
-          readonly source?: { readonly start: number; readonly end: number; readonly text: string } | null
-        }> | null
+          readonly name?: string
+          readonly description?: string
+          readonly source?: { readonly start: number; readonly end: number; readonly text: string }
+        }>
         readonly agents?: ReadonlyArray<{
           readonly name: string
-          readonly source?: { readonly start: number; readonly end: number; readonly text: string } | null
-        }> | null
+          readonly source?: { readonly start: number; readonly end: number; readonly text: string }
+        }>
         readonly type: "user"
       }
     | {
         readonly id: string
-        readonly metadata?: { readonly [x: string]: JsonValue } | null
+        readonly metadata?: { readonly [x: string]: JsonValue }
         readonly time: { readonly created: number }
         readonly sessionID: string
         readonly text: string
@@ -502,15 +484,15 @@ export type SessionsContextOutput = {
       }
     | {
         readonly id: string
-        readonly metadata?: { readonly [x: string]: JsonValue } | null
+        readonly metadata?: { readonly [x: string]: JsonValue }
         readonly time: { readonly created: number }
         readonly type: "system"
         readonly text: string
       }
     | {
         readonly id: string
-        readonly metadata?: { readonly [x: string]: JsonValue } | null
-        readonly time: { readonly created: number; readonly completed?: number | null }
+        readonly metadata?: { readonly [x: string]: JsonValue }
+        readonly time: { readonly created: number; readonly completed?: number }
         readonly type: "shell"
         readonly callID: string
         readonly command: string
@@ -518,18 +500,18 @@ export type SessionsContextOutput = {
       }
     | {
         readonly id: string
-        readonly metadata?: { readonly [x: string]: JsonValue } | null
-        readonly time: { readonly created: number; readonly completed?: number | null }
+        readonly metadata?: { readonly [x: string]: JsonValue }
+        readonly time: { readonly created: number; readonly completed?: number }
         readonly type: "assistant"
         readonly agent: string
-        readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string | null }
+        readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
         readonly content: ReadonlyArray<
           | { readonly type: "text"; readonly id: string; readonly text: string }
           | {
               readonly type: "reasoning"
               readonly id: string
               readonly text: string
-              readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } | null
+              readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
             }
           | {
               readonly type: "tool"
@@ -537,23 +519,18 @@ export type SessionsContextOutput = {
               readonly name: string
               readonly provider?: {
                 readonly executed: boolean
-                readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } | null
-                readonly resultMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } } | null
-              } | null
+                readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
+                readonly resultMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
+              }
               readonly state:
                 | { readonly status: "pending"; readonly input: string }
                 | {
                     readonly status: "running"
                     readonly input: { readonly [x: string]: JsonValue }
-                    readonly structured: { readonly [x: string]: any }
+                    readonly structured: { readonly [x: string]: JsonValue }
                     readonly content: ReadonlyArray<
                       | { readonly type: "text"; readonly text: string }
-                      | {
-                          readonly type: "file"
-                          readonly uri: string
-                          readonly mime: string
-                          readonly name?: string | null
-                        }
+                      | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string }
                     >
                   }
                 | {
@@ -562,61 +539,47 @@ export type SessionsContextOutput = {
                     readonly attachments?: ReadonlyArray<{
                       readonly uri: string
                       readonly mime: string
-                      readonly name?: string | null
-                      readonly description?: string | null
-                      readonly source?: { readonly start: number; readonly end: number; readonly text: string } | null
-                    }> | null
+                      readonly name?: string
+                      readonly description?: string
+                      readonly source?: { readonly start: number; readonly end: number; readonly text: string }
+                    }>
                     readonly content: ReadonlyArray<
                       | { readonly type: "text"; readonly text: string }
-                      | {
-                          readonly type: "file"
-                          readonly uri: string
-                          readonly mime: string
-                          readonly name?: string | null
-                        }
+                      | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string }
                     >
-                    readonly outputPaths?: ReadonlyArray<string> | null
-                    readonly structured: { readonly [x: string]: any }
-                    readonly result?: JsonValue | null
+                    readonly outputPaths?: ReadonlyArray<string>
+                    readonly structured: { readonly [x: string]: JsonValue }
+                    readonly result?: JsonValue
                   }
                 | {
                     readonly status: "error"
                     readonly input: { readonly [x: string]: JsonValue }
                     readonly content: ReadonlyArray<
                       | { readonly type: "text"; readonly text: string }
-                      | {
-                          readonly type: "file"
-                          readonly uri: string
-                          readonly mime: string
-                          readonly name?: string | null
-                        }
+                      | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string }
                     >
-                    readonly structured: { readonly [x: string]: any }
+                    readonly structured: { readonly [x: string]: JsonValue }
                     readonly error: { readonly type: "unknown"; readonly message: string }
-                    readonly result?: JsonValue | null
+                    readonly result?: JsonValue
                   }
               readonly time: {
                 readonly created: number
-                readonly ran?: number | null
-                readonly completed?: number | null
-                readonly pruned?: number | null
+                readonly ran?: number
+                readonly completed?: number
+                readonly pruned?: number
               }
             }
         >
-        readonly snapshot?: {
-          readonly start?: string | null
-          readonly end?: string | null
-          readonly files?: ReadonlyArray<string> | null
-        } | null
-        readonly finish?: string | null
-        readonly cost?: number | null
+        readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
+        readonly finish?: string
+        readonly cost?: number
         readonly tokens?: {
           readonly input: number
           readonly output: number
           readonly reasoning: number
           readonly cache: { readonly read: number; readonly write: number }
-        } | null
-        readonly error?: { readonly type: "unknown"; readonly message: string } | null
+        }
+        readonly error?: { readonly type: "unknown"; readonly message: string }
       }
     | {
         readonly type: "compaction"
@@ -624,7 +587,7 @@ export type SessionsContextOutput = {
         readonly summary: string
         readonly recent: string
         readonly id: string
-        readonly metadata?: { readonly [x: string]: JsonValue } | null
+        readonly metadata?: { readonly [x: string]: JsonValue }
         readonly time: { readonly created: number }
       }
   >

+ 7 - 5
packages/core/src/config/plugin/reference.ts

@@ -25,6 +25,8 @@ export const Plugin = define({
           const directory = doc.path ? path.dirname(doc.path) : location.directory
           for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
             if (!validAlias(name)) continue
+            const description = typeof entry === "string" ? undefined : entry.description
+            const hidden = typeof entry === "string" ? undefined : entry.hidden
             entries.set(
               name,
               local(entry)
@@ -33,15 +35,15 @@ export const Plugin = define({
                     path: AbsolutePath.make(
                       localPath(directory, global.home, typeof entry === "string" ? entry : entry.path),
                     ),
-                    description: typeof entry === "string" ? undefined : entry.description,
-                    hidden: typeof entry === "string" ? undefined : entry.hidden,
+                    ...(description === undefined ? {} : { description }),
+                    ...(hidden === undefined ? {} : { hidden }),
                   })
                 : Reference.GitSource.make({
                     type: "git",
                     repository: typeof entry === "string" ? entry : entry.repository,
-                    branch: typeof entry === "string" ? undefined : entry.branch,
-                    description: typeof entry === "string" ? undefined : entry.description,
-                    hidden: typeof entry === "string" ? undefined : entry.hidden,
+                    ...(entry.branch === undefined ? {} : { branch: entry.branch }),
+                    ...(description === undefined ? {} : { description }),
+                    ...(hidden === undefined ? {} : { hidden }),
                   }),
             )
           }

+ 14 - 10
packages/core/src/plugin/host.ts

@@ -12,8 +12,11 @@ import { ModelV2 } from "../model"
 import { PluginV2 } from "../plugin"
 import { ProviderV2 } from "../provider"
 import { Reference } from "../reference"
+import type { DeepMutable } from "../schema"
 import { SkillV2 } from "../skill"
 
+const mutable = <T>(value: T) => value as DeepMutable<T>
+
 export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) {
   const agents = yield* AgentV2.Service
   const aisdk = yield* AISDK.Service
@@ -30,8 +33,8 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
       transform: (callback) =>
         agents.transform((draft) =>
           callback({
-            list: draft.list,
-            get: (id) => draft.get(AgentV2.ID.make(id)),
+            list: () => mutable(draft.list()),
+            get: (id) => mutable(draft.get(AgentV2.ID.make(id))),
             default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)),
             update: (id, update) => draft.update(AgentV2.ID.make(id), update),
             remove: (id) => draft.remove(AgentV2.ID.make(id)),
@@ -42,7 +45,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
       sdk: (callback) =>
         aisdk.hook.sdk((event) => {
           const output = {
-            model: event.model,
+            model: mutable(event.model),
             package: event.package,
             options: event.options,
             sdk: event.sdk,
@@ -55,7 +58,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
       language: (callback) =>
         aisdk.hook.language((event) => {
           const output = {
-            model: event.model,
+            model: mutable(event.model),
             sdk: event.sdk,
             options: event.options,
             language: event.language,
@@ -72,13 +75,14 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
         catalog.transform((draft) =>
           callback({
             provider: {
-              list: draft.provider.list,
-              get: (id) => draft.provider.get(ProviderV2.ID.make(id)),
+              list: () => mutable(draft.provider.list()),
+              get: (id) => mutable(draft.provider.get(ProviderV2.ID.make(id))),
               update: (id, update) => draft.provider.update(ProviderV2.ID.make(id), update),
               remove: (id) => draft.provider.remove(ProviderV2.ID.make(id)),
             },
             model: {
-              get: (providerID, modelID) => draft.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
+              get: (providerID, modelID) =>
+                mutable(draft.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID))),
               update: (providerID, modelID, update) =>
                 draft.model.update(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID), update),
               remove: (providerID, modelID) =>
@@ -108,12 +112,12 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
       transform: (callback) =>
         integration.transform((draft) =>
           callback({
-            list: draft.list,
-            get: (id) => draft.get(Integration.ID.make(id)),
+            list: () => mutable(draft.list()),
+            get: (id) => mutable(draft.get(Integration.ID.make(id))),
             update: (id, update) => draft.update(Integration.ID.make(id), update),
             remove: (id) => draft.remove(Integration.ID.make(id)),
             method: {
-              list: (id) => draft.method.list(Integration.ID.make(id)),
+              list: (id) => mutable(draft.method.list(Integration.ID.make(id))),
               update: (input) => {
                 if ("authorize" in input) {
                   const methodID = Integration.MethodID.make(input.method.id)

+ 9 - 9
packages/core/src/reference.ts

@@ -65,8 +65,8 @@ export const layer = Layer.effect(
                 new Info({
                   name,
                   path: source.path,
-                  description: source.description,
-                  hidden: source.hidden,
+                  ...(source.description === undefined ? {} : { description: source.description }),
+                  ...(source.hidden === undefined ? {} : { hidden: source.hidden }),
                   source,
                 }),
               )
@@ -86,13 +86,13 @@ export const layer = Layer.effect(
             seen.set(target, source.branch)
             materialized.set(
               name,
-              new Info({
-                name,
-                path: AbsolutePath.make(target),
-                description: source.description,
-                hidden: source.hidden,
-                source,
-              }),
+                new Info({
+                  name,
+                  path: AbsolutePath.make(target),
+                  ...(source.description === undefined ? {} : { description: source.description }),
+                  ...(source.hidden === undefined ? {} : { hidden: source.hidden }),
+                  source,
+                }),
             )
             yield* cache.ensure({ reference: repository, branch: source.branch, refresh: true }).pipe(
               Effect.catchCause((cause) =>

+ 3 - 2
packages/core/src/v1/config/migrate.ts

@@ -170,6 +170,7 @@ function providers(info?: Readonly<Record<string, ConfigProviderV1.Info>>) {
 function migrateProvider(info: ConfigProviderV1.Info) {
   const lowerer = ConfigProviderOptionsV1.get(info.npm)
   const options = lowerer.provider(info.options ?? {})
+  const url = info.api ?? options.url
   return {
     name: info.name,
     env: info.env,
@@ -177,7 +178,7 @@ function migrateProvider(info: ConfigProviderV1.Info) {
       ? {
           type: "aisdk" as const,
           package: info.npm,
-          url: info.api ?? options.url,
+          ...(url === undefined ? {} : { url }),
           settings: options.settings ?? {},
         }
       : undefined,
@@ -221,7 +222,7 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type, packageName?: st
           ...(info.id === undefined ? {} : { id: info.id }),
           type: "aisdk" as const,
           package: info.provider.npm,
-          url: info.provider.api,
+          ...(info.provider.api === undefined ? {} : { url: info.provider.api }),
           settings: {},
         }
       : info.id === undefined

+ 0 - 1
packages/core/test/config/config.test.ts

@@ -106,7 +106,6 @@ describe("Config", () => {
       expect(migrated.providers?.bedrock?.api).toEqual({
         type: "aisdk",
         package: "@ai-sdk/amazon-bedrock",
-        url: undefined,
         settings: { region: "us-east-1", profile: "dev" },
       })
       expect(migrated.providers?.bedrock?.request).toEqual({

+ 3 - 1
packages/core/test/tool-todowrite.test.ts

@@ -77,7 +77,9 @@ describe("TodoWriteTool", () => {
       yield* setup
       const registry = yield* ToolRegistry.Service
       const service = yield* SessionTodo.Service
-      const todoList = [{ content: "Implement slice", status: "in_progress", priority: "high" }]
+      const todoList: ReadonlyArray<SessionTodo.Info> = [
+        { content: "Implement slice", status: "in_progress", priority: "high" },
+      ]
 
       expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual([TodoWriteTool.name])
       expect(yield* settleTool(registry, call(todoList))).toEqual({

+ 2 - 2
packages/opencode/src/session/todo.ts

@@ -14,7 +14,7 @@ export type Info = SessionTodo.Info
 export const Event = SessionTodo.Event
 
 export interface Interface {
-  readonly update: (input: { sessionID: SessionID; todos: Info[] }) => Effect.Effect<void>
+  readonly update: (input: { sessionID: SessionID; todos: ReadonlyArray<Info> }) => Effect.Effect<void>
   readonly get: (sessionID: SessionID) => Effect.Effect<Info[]>
 }
 
@@ -26,7 +26,7 @@ export const layer = Layer.effect(
     const events = yield* EventV2Bridge.Service
     const { db } = yield* Database.Service
 
-    const update = Effect.fn("Todo.update")(function* (input: { sessionID: SessionID; todos: Info[] }) {
+    const update = Effect.fn("Todo.update")(function* (input: { sessionID: SessionID; todos: ReadonlyArray<Info> }) {
       yield* db
         .transaction((tx) =>
           Effect.gen(function* () {

+ 1 - 12
packages/opencode/src/tool/todo.ts

@@ -3,19 +3,8 @@ import * as Tool from "./tool"
 import DESCRIPTION_WRITE from "./todowrite.txt"
 import { Todo } from "../session/todo"
 
-// Todo.Info is still a zod schema (session/todo.ts). Inline the field shape
-// here rather than referencing its `.shape` — the LLM-visible JSON Schema is
-// identical, and it removes the last zod dependency from this tool.
-const TodoItem = Schema.Struct({
-  content: Schema.String.annotate({ description: "Brief description of the task" }),
-  status: Schema.String.annotate({
-    description: "Current status of the task: pending, in_progress, completed, cancelled",
-  }),
-  priority: Schema.String.annotate({ description: "Priority level of the task: high, medium, low" }),
-})
-
 export const Parameters = Schema.Struct({
-  todos: Schema.mutable(Schema.Array(TodoItem)).annotate({ description: "The updated todo list" }),
+  todos: Schema.mutable(Schema.Array(Todo.Info)).annotate({ description: "The updated todo list" }),
 })
 
 type Metadata = {

+ 1 - 1
packages/opencode/test/server/httpapi-exercise/index.ts

@@ -1195,7 +1195,7 @@ const scenarios: Scenario[] = [
     .seeded((ctx) =>
       Effect.gen(function* () {
         const session = yield* ctx.session({ title: "Todo session" })
-        const todos = [{ content: "cover session todo", status: "pending", priority: "high" }]
+        const todos = [{ content: "cover session todo", status: "pending" as const, priority: "high" as const }]
         yield* ctx.todos(session.id, todos)
         return { session, todos }
       }),

+ 5 - 1
packages/opencode/test/server/httpapi-exercise/types.ts

@@ -119,5 +119,9 @@ export type Result =
   | { status: "skip"; scenario: TodoScenario }
 
 export type SessionInfo = { id: SessionID; title: string; parentID?: SessionID }
-export type TodoInfo = { content: string; status: string; priority: string }
+export type TodoInfo = {
+  content: string
+  status: "pending" | "in_progress" | "completed" | "cancelled"
+  priority: "high" | "medium" | "low"
+}
 export type MessageSeed = { info: SessionV1.User; part: SessionV1.TextPart }

+ 0 - 8
packages/opencode/test/server/httpapi-reference.test.ts

@@ -44,26 +44,18 @@ describe("reference HttpApi", () => {
       {
         name: "docs",
         path: path.join(tmp.path, "docs"),
-        description: null,
-        hidden: null,
         source: {
           type: "local",
           path: path.join(tmp.path, "docs"),
-          description: null,
-          hidden: null,
         },
       },
       {
         name: "effect",
         path: path.join(Global.Path.repos, "github.com", "Effect-TS", "effect"),
-        description: null,
-        hidden: null,
         source: {
           type: "git",
           repository: "Effect-TS/effect",
           branch: "main",
-          description: null,
-          hidden: null,
         },
       },
     ])

+ 1 - 1
packages/opencode/test/session/schema-decoding.test.ts

@@ -256,7 +256,7 @@ describe("Todo.Info", () => {
   const decode = decodeUnknown(Todo.Info)
 
   test("three-field round-trip", () => {
-    const input = { content: "do a thing", status: "pending", priority: "high" }
+    const input = Todo.Info.make({ content: "do a thing", status: "pending", priority: "high" })
     expect(decode(input)).toEqual(input)
   })
 })

+ 7 - 6
packages/schema/src/agent.ts

@@ -1,6 +1,7 @@
 export * as Agent from "./agent"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { Model } from "./model"
 import { Permission } from "./permission"
 import { Provider } from "./provider"
@@ -12,20 +13,20 @@ export type ID = typeof ID.Type
 export const Color = Schema.Union([
   Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
   Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
-])
+]).annotate({ identifier: "Agent.Color" })
 export type Color = typeof Color.Type
 
 export interface Info extends Schema.Schema.Type<typeof Info> {}
 export const Info = Schema.Struct({
   id: ID,
-  model: Model.Ref.pipe(Schema.optional),
+  model: Model.Ref.pipe(optional),
   request: Provider.Request,
-  system: Schema.String.pipe(Schema.optional),
-  description: Schema.String.pipe(Schema.optional),
+  system: Schema.String.pipe(optional),
+  description: Schema.String.pipe(optional),
   mode: Schema.Literals(["subagent", "primary", "all"]),
   hidden: Schema.Boolean,
-  color: Color.pipe(Schema.optional),
-  steps: PositiveInt.pipe(Schema.optional),
+  color: Color.pipe(optional),
+  steps: PositiveInt.pipe(optional),
   permissions: Permission.Ruleset,
 })
   .annotate({ identifier: "AgentV2.Info" })

+ 5 - 4
packages/schema/src/command.ts

@@ -1,14 +1,15 @@
 export * as Command from "./command"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { Model } from "./model"
 
 export interface Info extends Schema.Schema.Type<typeof Info> {}
 export const Info = Schema.Struct({
   name: Schema.String,
   template: Schema.String,
-  description: Schema.String.pipe(Schema.optional),
-  agent: Schema.String.pipe(Schema.optional),
-  model: Model.Ref.pipe(Schema.optional),
-  subtask: Schema.Boolean.pipe(Schema.optional),
+  description: Schema.String.pipe(optional),
+  agent: Schema.String.pipe(optional),
+  model: Model.Ref.pipe(optional),
+  subtask: Schema.Boolean.pipe(optional),
 }).annotate({ identifier: "CommandV2.Info" })

+ 3 - 2
packages/schema/src/credential.ts

@@ -1,6 +1,7 @@
 export * as Credential from "./credential"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { IntegrationMethodID } from "./integration-id"
 import { ascending } from "./identifier"
 import { NonNegativeInt, statics } from "./schema"
@@ -18,14 +19,14 @@ export const OAuth = Schema.Struct({
   refresh: Schema.String,
   access: Schema.String,
   expires: NonNegativeInt,
-  metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
+  metadata: optional(Schema.Record(Schema.String, Schema.Unknown)),
 }).annotate({ identifier: "Credential.OAuth" })
 
 export interface Key extends Schema.Schema.Type<typeof Key> {}
 export const Key = Schema.Struct({
   type: Schema.Literal("key"),
   key: Schema.String,
-  metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
+  metadata: optional(Schema.Record(Schema.String, Schema.Unknown)),
 }).annotate({ identifier: "Credential.Key" })
 
 export const Value = Schema.Union([OAuth, Key])

+ 4 - 5
packages/schema/src/event.ts

@@ -1,6 +1,7 @@
 export * as Event from "./event"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { ascending } from "./identifier"
 import { Location } from "./location"
 import { statics } from "./schema"
@@ -53,12 +54,10 @@ export function define<
   return Object.assign(
     Schema.Struct({
       id: ID,
-      metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
+      metadata: optional(Schema.Record(Schema.String, Schema.Unknown)),
       type: Schema.Literal(input.type),
-      durable: Schema.optional(
-        Schema.Struct({ aggregateID: Schema.String, seq: Schema.Number, version: Schema.Number }),
-      ),
-      location: Schema.optional(Location.Ref),
+      durable: optional(Schema.Struct({ aggregateID: Schema.String, seq: Schema.Number, version: Schema.Number })),
+      location: optional(Location.Ref),
       data,
     }).annotate({ identifier: input.type }),
     {

+ 5 - 4
packages/schema/src/file-diff.ts

@@ -1,12 +1,13 @@
 export * as FileDiff from "./file-diff"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 
 export const Info = Schema.Struct({
-  file: Schema.optional(Schema.String),
-  patch: Schema.optional(Schema.String),
+  file: optional(Schema.String),
+  patch: optional(Schema.String),
   additions: Schema.Finite,
   deletions: Schema.Finite,
-  status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])),
+  status: optional(Schema.Literals(["added", "deleted", "modified"])),
 }).annotate({ identifier: "SnapshotFileDiff" })
-export type Info = typeof Info.Type
+export interface Info extends Schema.Schema.Type<typeof Info> {}

+ 4 - 3
packages/schema/src/filesystem.ts

@@ -1,6 +1,7 @@
 export * as FileSystem from "./filesystem"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { define, inventory } from "./event"
 import { NonNegativeInt, PositiveInt, RelativePath } from "./schema"
 
@@ -21,7 +22,7 @@ export const Submatch = Schema.Struct({
   text: Schema.String,
   start: NonNegativeInt,
   end: NonNegativeInt,
-})
+}).annotate({ identifier: "FileSystem.Submatch" })
 
 export interface Match extends Schema.Schema.Type<typeof Match> {}
 export const Match = Schema.Struct({
@@ -34,6 +35,6 @@ export const Match = Schema.Struct({
 
 export class FindInput extends Schema.Class<FindInput>("FileSystem.FindInput")({
   query: Schema.String,
-  type: Schema.Literals(["file", "directory"]).pipe(Schema.optional),
-  limit: PositiveInt.pipe(Schema.optional),
+  type: Schema.Literals(["file", "directory"]).pipe(optional),
+  limit: PositiveInt.pipe(optional),
 }) {}

+ 18 - 17
packages/schema/src/integration.ts

@@ -1,6 +1,7 @@
 export * as Integration from "./integration"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { define, inventory } from "./event"
 import { Connection } from "./connection"
 import { ascending } from "./identifier"
@@ -25,8 +26,8 @@ export const TextPrompt = Schema.Struct({
   type: Schema.Literal("text"),
   key: Schema.String,
   message: Schema.String,
-  placeholder: Schema.optional(Schema.String),
-  when: Schema.optional(When),
+  placeholder: optional(Schema.String),
+  when: optional(When),
 }).annotate({ identifier: "Integration.TextPrompt" })
 
 export interface SelectPrompt extends Schema.Schema.Type<typeof SelectPrompt> {}
@@ -34,16 +35,14 @@ export const SelectPrompt = Schema.Struct({
   type: Schema.Literal("select"),
   key: Schema.String,
   message: Schema.String,
-  options: Schema.mutable(
-    Schema.Array(
-      Schema.Struct({
-        label: Schema.String,
-        value: Schema.String,
-        hint: Schema.optional(Schema.String),
-      }),
-    ),
+  options: Schema.Array(
+    Schema.Struct({
+      label: Schema.String,
+      value: Schema.String,
+      hint: optional(Schema.String),
+    }),
   ),
-  when: Schema.optional(When),
+  when: optional(When),
 }).annotate({ identifier: "Integration.SelectPrompt" })
 
 export const Prompt = Schema.Union([TextPrompt, SelectPrompt]).pipe(Schema.toTaggedUnion("type"))
@@ -54,19 +53,19 @@ export const OAuthMethod = Schema.Struct({
   id: MethodID,
   type: Schema.Literal("oauth"),
   label: Schema.String,
-  prompts: Schema.optional(Schema.mutable(Schema.Array(Prompt))),
+  prompts: optional(Schema.Array(Prompt)),
 }).annotate({ identifier: "Integration.OAuthMethod" })
 
 export interface KeyMethod extends Schema.Schema.Type<typeof KeyMethod> {}
 export const KeyMethod = Schema.Struct({
   type: Schema.Literal("key"),
-  label: Schema.optional(Schema.String),
+  label: optional(Schema.String),
 }).annotate({ identifier: "Integration.KeyMethod" })
 
 export interface EnvMethod extends Schema.Schema.Type<typeof EnvMethod> {}
 export const EnvMethod = Schema.Struct({
   type: Schema.Literal("env"),
-  names: Schema.mutable(Schema.Array(Schema.String)),
+  names: Schema.Array(Schema.String),
 }).annotate({ identifier: "Integration.EnvMethod" })
 
 export const Method = Schema.Union([OAuthMethod, KeyMethod, EnvMethod])
@@ -96,8 +95,8 @@ export const Ref = Schema.Struct({
 export class Info extends Schema.Class<Info>("Integration.Info")({
   id: ID,
   name: Schema.String,
-  methods: Schema.mutable(Schema.Array(Method)),
-  connections: Schema.mutable(Schema.Array(Connection.Info)),
+  methods: Schema.Array(Method),
+  connections: Schema.Array(Connection.Info),
 }) {}
 
 export const AttemptID = Schema.String.pipe(
@@ -124,5 +123,7 @@ export const AttemptStatus = Schema.Union([
   Schema.Struct({ status: Schema.Literal("complete"), time: AttemptTime }),
   Schema.Struct({ status: Schema.Literal("failed"), message: Schema.String, time: AttemptTime }),
   Schema.Struct({ status: Schema.Literal("expired"), time: AttemptTime }),
-]).pipe(Schema.toTaggedUnion("status"))
+])
+  .pipe(Schema.toTaggedUnion("status"))
+  .annotate({ identifier: "Integration.AttemptStatus" })
 export type AttemptStatus = typeof AttemptStatus.Type

+ 8 - 3
packages/schema/src/llm.ts

@@ -1,8 +1,11 @@
 export * as LLM from "./llm"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 
-export const ProviderMetadata = Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown))
+export const ProviderMetadata = Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown)).annotate({
+  identifier: "LLM.ProviderMetadata",
+})
 export type ProviderMetadata = Schema.Schema.Type<typeof ProviderMetadata>
 
 export interface ToolTextContent extends Schema.Schema.Type<typeof ToolTextContent> {}
@@ -16,8 +19,10 @@ export const ToolFileContent = Schema.Struct({
   type: Schema.Literal("file"),
   uri: Schema.String,
   mime: Schema.String,
-  name: Schema.optional(Schema.String),
+  name: optional(Schema.String),
 }).annotate({ identifier: "Tool.FileContent" })
 
-export const ToolContent = Schema.Union([ToolTextContent, ToolFileContent]).pipe(Schema.toTaggedUnion("type"))
+export const ToolContent = Schema.Union([ToolTextContent, ToolFileContent])
+  .pipe(Schema.toTaggedUnion("type"))
+  .annotate({ identifier: "LLM.ToolContent" })
 export type ToolContent = Schema.Schema.Type<typeof ToolContent>

+ 2 - 5
packages/schema/src/location.ts

@@ -1,6 +1,6 @@
 export * as Location from "./location"
 
-import { Effect, Schema } from "effect"
+import { Schema } from "effect"
 import { AbsolutePath, optional } from "./schema"
 import { ProjectID } from "./project-id"
 import { WorkspaceID } from "./workspace-id"
@@ -8,10 +8,7 @@ import { WorkspaceID } from "./workspace-id"
 export interface Ref extends Schema.Schema.Type<typeof Ref> {}
 export const Ref = Schema.Struct({
   directory: AbsolutePath,
-  workspaceID: Schema.optional(WorkspaceID).pipe(
-    Schema.withDecodingDefault(Effect.succeed(undefined)),
-    Schema.withConstructorDefault(Effect.succeed(undefined)),
-  ),
+  workspaceID: optional(WorkspaceID),
 }).annotate({ identifier: "Location.Ref" })
 
 export class Info extends Schema.Class<Info>("Location.Info")({

+ 17 - 14
packages/schema/src/model.ts

@@ -1,6 +1,7 @@
 export * as Model from "./model"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { Provider } from "./provider"
 import { statics } from "./schema"
 
@@ -13,9 +14,9 @@ export type VariantID = typeof VariantID.Type
 export const Ref = Schema.Struct({
   id: ID,
   providerID: Provider.ID,
-  variant: VariantID.pipe(Schema.optional),
-})
-export type Ref = typeof Ref.Type
+  variant: VariantID.pipe(optional),
+}).annotate({ identifier: "Model.Ref" })
+export interface Ref extends Schema.Schema.Type<typeof Ref> {}
 
 export const Family = Schema.String.pipe(Schema.brand("Family"))
 export type Family = typeof Family.Type
@@ -23,23 +24,23 @@ export type Family = typeof Family.Type
 export interface Capabilities extends Schema.Schema.Type<typeof Capabilities> {}
 export const Capabilities = Schema.Struct({
   tools: Schema.Boolean,
-  input: Schema.String.pipe(Schema.Array, Schema.mutable),
-  output: Schema.String.pipe(Schema.Array, Schema.mutable),
-})
+  input: Schema.Array(Schema.String),
+  output: Schema.Array(Schema.String),
+}).annotate({ identifier: "Model.Capabilities" })
 
 export interface Cost extends Schema.Schema.Type<typeof Cost> {}
 export const Cost = Schema.Struct({
   tier: Schema.Struct({
     type: Schema.Literal("context"),
     size: Schema.Int,
-  }).pipe(Schema.optional),
+  }).pipe(optional),
   input: Schema.Finite,
   output: Schema.Finite,
   cache: Schema.Struct({
     read: Schema.Finite,
     write: Schema.Finite,
   }),
-})
+}).annotate({ identifier: "Model.Cost" })
 
 export const Api = Schema.Union([
   Schema.Struct({
@@ -50,34 +51,36 @@ export const Api = Schema.Union([
     id: ID,
     ...Provider.Native.fields,
   }),
-]).pipe(Schema.toTaggedUnion("type"))
+])
+  .pipe(Schema.toTaggedUnion("type"))
+  .annotate({ identifier: "Model.Api" })
 export type Api = typeof Api.Type
 
 export interface Info extends Schema.Schema.Type<typeof Info> {}
 export const Info = Schema.Struct({
   id: ID,
   providerID: Provider.ID,
-  family: Family.pipe(Schema.optional),
+  family: Family.pipe(optional),
   name: Schema.String,
   api: Api,
   capabilities: Capabilities,
   request: Schema.Struct({
     ...Provider.Request.fields,
-    variant: Schema.String.pipe(Schema.optional),
+    variant: Schema.String.pipe(optional),
   }),
   variants: Schema.Struct({
     id: VariantID,
     ...Provider.Request.fields,
-  }).pipe(Schema.Array, Schema.mutable),
+  }).pipe(Schema.Array),
   time: Schema.Struct({
     released: Schema.Finite,
   }),
-  cost: Cost.pipe(Schema.Array, Schema.mutable),
+  cost: Schema.Array(Cost),
   status: Schema.Literals(["alpha", "beta", "deprecated", "active"]),
   enabled: Schema.Boolean,
   limit: Schema.Struct({
     context: Schema.Int,
-    input: Schema.Int.pipe(Schema.optional),
+    input: Schema.Int.pipe(optional),
     output: Schema.Int,
   }),
 })

+ 1 - 1
packages/schema/src/permission-saved.ts

@@ -17,4 +17,4 @@ export const Info = Schema.Struct({
   action: Schema.String,
   resource: Schema.String,
 }).annotate({ identifier: "PermissionSaved.Info" })
-export type Info = typeof Info.Type
+export interface Info extends Schema.Schema.Type<typeof Info> {}

+ 6 - 5
packages/schema/src/permission.ts

@@ -1,6 +1,7 @@
 export * as Permission from "./permission"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { define, inventory } from "./event"
 import { ascending } from "./identifier"
 import { SessionID } from "./session-id"
@@ -25,16 +26,16 @@ const RequestFields = {
   sessionID: SessionID,
   action: Schema.String,
   resources: Schema.Array(Schema.String),
-  save: Schema.Array(Schema.String).pipe(Schema.optional),
-  metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
-  source: Source.pipe(Schema.optional),
+  save: Schema.Array(Schema.String).pipe(optional),
+  metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(optional),
+  source: Source.pipe(optional),
 }
 
 export const Request = Schema.Struct({
   id: ID,
   ...RequestFields,
 }).annotate({ identifier: "PermissionV2.Request" })
-export type Request = typeof Request.Type
+export interface Request extends Schema.Schema.Type<typeof Request> {}
 
 export const Reply = Schema.Literals(["once", "always", "reject"]).annotate({ identifier: "PermissionV2.Reply" })
 export type Reply = typeof Reply.Type
@@ -60,5 +61,5 @@ export const Rule = Schema.Struct({
   effect: Effect,
 }).annotate({ identifier: "PermissionV2.Rule" })
 
-export const Ruleset = Schema.mutable(Schema.Array(Rule)).annotate({ identifier: "PermissionV2.Ruleset" })
+export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionV2.Ruleset" })
 export type Ruleset = typeof Ruleset.Type

+ 5 - 4
packages/schema/src/project-copy.ts

@@ -1,6 +1,7 @@
 export * as ProjectCopy from "./project-copy"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { ProjectID } from "./project-id"
 import { AbsolutePath } from "./schema"
 
@@ -12,18 +13,18 @@ export const CreateInput = Schema.Struct({
   strategy: StrategyID,
   sourceDirectory: AbsolutePath,
   directory: AbsolutePath,
-  name: Schema.optional(Schema.String),
+  name: optional(Schema.String),
 }).annotate({ identifier: "ProjectCopy.CreateInput" })
-export type CreateInput = typeof CreateInput.Type
+export interface CreateInput extends Schema.Schema.Type<typeof CreateInput> {}
 
 export const RemoveInput = Schema.Struct({
   projectID: ProjectID,
   directory: AbsolutePath,
   force: Schema.Boolean,
 }).annotate({ identifier: "ProjectCopy.RemoveInput" })
-export type RemoveInput = typeof RemoveInput.Type
+export interface RemoveInput extends Schema.Schema.Type<typeof RemoveInput> {}
 
 export const Copy = Schema.Struct({
   directory: AbsolutePath,
 }).annotate({ identifier: "ProjectCopy.Copy" })
-export type Copy = typeof Copy.Type
+export interface Copy extends Schema.Schema.Type<typeof Copy> {}

+ 8 - 5
packages/schema/src/project.ts

@@ -8,22 +8,25 @@ import { ProjectID } from "./project-id"
 export const ID = ProjectID
 export type ID = typeof ID.Type
 
-export const Vcs = Schema.Literal("git")
+export const Vcs = Schema.Literal("git").annotate({ identifier: "Project.Vcs" })
 export const Icon = Schema.Struct({
   url: optional(Schema.String),
   override: optional(Schema.String),
   color: optional(Schema.String),
-})
+}).annotate({ identifier: "Project.Icon" })
+export interface Icon extends Schema.Schema.Type<typeof Icon> {}
 export const Commands = Schema.Struct({
   start: optional(
     Schema.String.annotate({ description: "Startup script to run when creating a new workspace (worktree)" }),
   ),
-})
+}).annotate({ identifier: "Project.Commands" })
+export interface Commands extends Schema.Schema.Type<typeof Commands> {}
 export const Time = Schema.Struct({
   created: NonNegativeInt,
   updated: NonNegativeInt,
   initialized: optional(NonNegativeInt),
-})
+}).annotate({ identifier: "Project.Time" })
+export interface Time extends Schema.Schema.Type<typeof Time> {}
 
 export const Info = Schema.Struct({
   id: ID,
@@ -35,7 +38,7 @@ export const Info = Schema.Struct({
   time: Time,
   sandboxes: Schema.Array(Schema.String),
 }).annotate({ identifier: "Project" })
-export type Info = typeof Info.Type
+export interface Info extends Schema.Schema.Type<typeof Info> {}
 
 const Updated = define({ type: "project.updated", schema: Info.fields })
 export const Event = { Updated, Definitions: inventory(Updated) }

+ 7 - 6
packages/schema/src/prompt.ts

@@ -1,4 +1,5 @@
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { statics } from "./schema"
 
 export interface Source extends Schema.Schema.Type<typeof Source> {}
@@ -12,9 +13,9 @@ export interface FileAttachment extends Schema.Schema.Type<typeof FileAttachment
 export const FileAttachment = Schema.Struct({
   uri: Schema.String,
   mime: Schema.String,
-  name: Schema.String.pipe(Schema.optional),
-  description: Schema.String.pipe(Schema.optional),
-  source: Source.pipe(Schema.optional),
+  name: Schema.String.pipe(optional),
+  description: Schema.String.pipe(optional),
+  source: Source.pipe(optional),
 })
   .annotate({ identifier: "Prompt.FileAttachment" })
   .pipe(
@@ -33,14 +34,14 @@ export const FileAttachment = Schema.Struct({
 export interface AgentAttachment extends Schema.Schema.Type<typeof AgentAttachment> {}
 export const AgentAttachment = Schema.Struct({
   name: Schema.String,
-  source: Source.pipe(Schema.optional),
+  source: Source.pipe(optional),
 }).annotate({ identifier: "Prompt.AgentAttachment" })
 
 export interface Prompt extends Schema.Schema.Type<typeof Prompt> {}
 export const Prompt = Schema.Struct({
   text: Schema.String,
-  files: Schema.Array(FileAttachment).pipe(Schema.optional),
-  agents: Schema.Array(AgentAttachment).pipe(Schema.optional),
+  files: Schema.Array(FileAttachment).pipe(optional),
+  agents: Schema.Array(AgentAttachment).pipe(optional),
 })
   .annotate({ identifier: "Prompt" })
   .pipe(

+ 13 - 10
packages/schema/src/provider.ts

@@ -1,6 +1,7 @@
 export * as Provider from "./provider"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { Integration } from "./integration"
 import { statics } from "./schema"
 
@@ -26,32 +27,34 @@ export interface AISDK extends Schema.Schema.Type<typeof AISDK> {}
 export const AISDK = Schema.Struct({
   type: Schema.Literal("aisdk"),
   package: Schema.String,
-  url: Schema.String.pipe(Schema.optional),
-  settings: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
-})
+  url: Schema.String.pipe(optional),
+  settings: Schema.Record(Schema.String, Schema.Unknown).pipe(optional),
+}).annotate({ identifier: "Provider.AISDK" })
 
 export interface Native extends Schema.Schema.Type<typeof Native> {}
 export const Native = Schema.Struct({
   type: Schema.Literal("native"),
-  url: Schema.String.pipe(Schema.optional),
+  url: Schema.String.pipe(optional),
   settings: Schema.Record(Schema.String, Schema.Unknown),
-})
+}).annotate({ identifier: "Provider.Native" })
 
-export const Api = Schema.Union([AISDK, Native]).pipe(Schema.toTaggedUnion("type"))
+export const Api = Schema.Union([AISDK, Native])
+  .pipe(Schema.toTaggedUnion("type"))
+  .annotate({ identifier: "Provider.Api" })
 export type Api = typeof Api.Type
 
 export interface Request extends Schema.Schema.Type<typeof Request> {}
 export const Request = Schema.Struct({
   headers: Schema.Record(Schema.String, Schema.String),
-  body: Schema.Record(Schema.String, Schema.Any),
-})
+  body: Schema.Record(Schema.String, Schema.Json),
+}).annotate({ identifier: "Provider.Request" })
 
 export interface Info extends Schema.Schema.Type<typeof Info> {}
 export const Info = Schema.Struct({
   id: ID,
-  integrationID: Integration.ID.pipe(Schema.optional),
+  integrationID: Integration.ID.pipe(optional),
   name: Schema.String,
-  disabled: Schema.Boolean.pipe(Schema.optional),
+  disabled: Schema.Boolean.pipe(optional),
   api: Api,
   request: Request,
 })

+ 2 - 1
packages/schema/src/pty-ticket.ts

@@ -6,4 +6,5 @@ import { PositiveInt } from "./schema"
 export const ConnectToken = Schema.Struct({
   ticket: Schema.String,
   expires_in: PositiveInt,
-})
+}).annotate({ identifier: "PtyTicket.ConnectToken" })
+export interface ConnectToken extends Schema.Schema.Type<typeof ConnectToken> {}

+ 19 - 13
packages/schema/src/pty.ts

@@ -1,6 +1,7 @@
 export * as Pty from "./pty"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { define, inventory } from "./event"
 import { ascending } from "./identifier"
 import { NonNegativeInt, PositiveInt, statics } from "./schema"
@@ -8,9 +9,13 @@ import { NonNegativeInt, PositiveInt, statics } from "./schema"
 const IDSchema = Schema.String.check(Schema.isStartsWith("pty")).pipe(Schema.brand("PtyID"))
 
 export const ID = IDSchema.pipe(
-  statics((schema: typeof IDSchema) => ({
-    ascending: (id?: string) => schema.make(id ?? "pty_" + ascending()),
-  })),
+  statics((schema: typeof IDSchema) => {
+    const create = () => schema.make("pty_" + ascending())
+    return {
+      create,
+      ascending: (id?: string) => (id === undefined ? create() : schema.make(id)),
+    }
+  }),
 )
 export type ID = typeof ID.Type
 
@@ -22,8 +27,9 @@ export const Info = Schema.Struct({
   cwd: Schema.String,
   status: Schema.Literals(["running", "exited"]),
   pid: NonNegativeInt,
-  exitCode: Schema.optional(NonNegativeInt),
+  exitCode: optional(NonNegativeInt),
 }).annotate({ identifier: "Pty" })
+export interface Info extends Schema.Schema.Type<typeof Info> {}
 
 const Created = define({ type: "pty.created", schema: { info: Info } })
 const Updated = define({ type: "pty.updated", schema: { info: Info } })
@@ -32,21 +38,21 @@ const Deleted = define({ type: "pty.deleted", schema: { id: ID } })
 export const Event = { Created, Updated, Exited, Deleted, Definitions: inventory(Created, Updated, Exited, Deleted) }
 
 export const CreateInput = Schema.Struct({
-  command: Schema.optional(Schema.String),
-  args: Schema.optional(Schema.Array(Schema.String)),
-  cwd: Schema.optional(Schema.String),
-  title: Schema.optional(Schema.String),
-  env: Schema.optional(Schema.Record(Schema.String, Schema.String)),
+  command: optional(Schema.String),
+  args: optional(Schema.Array(Schema.String)),
+  cwd: optional(Schema.String),
+  title: optional(Schema.String),
+  env: optional(Schema.Record(Schema.String, Schema.String)),
 })
-export type CreateInput = typeof CreateInput.Type
+export interface CreateInput extends Schema.Schema.Type<typeof CreateInput> {}
 
 export const UpdateInput = Schema.Struct({
-  title: Schema.optional(Schema.String),
-  size: Schema.optional(
+  title: optional(Schema.String),
+  size: optional(
     Schema.Struct({
       rows: PositiveInt,
       cols: PositiveInt,
     }),
   ),
 })
-export type UpdateInput = typeof UpdateInput.Type
+export interface UpdateInput extends Schema.Schema.Type<typeof UpdateInput> {}

+ 17 - 10
packages/schema/src/question.ts

@@ -1,6 +1,7 @@
 export * as Question from "./question"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { define, inventory } from "./event"
 import { ascending } from "./identifier"
 import { SessionID } from "./session-id"
@@ -8,7 +9,13 @@ import { statics } from "./schema"
 
 export const ID = Schema.String.check(Schema.isStartsWith("que")).pipe(
   Schema.brand("QuestionV2.ID"),
-  statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "que_" + ascending()) })),
+  statics((schema) => {
+    const create = () => schema.make("que_" + ascending())
+    return {
+      create,
+      ascending: (id?: string) => (id === undefined ? create() : schema.make(id)),
+    }
+  }),
 )
 export type ID = typeof ID.Type
 
@@ -16,39 +23,39 @@ export const Option = Schema.Struct({
   label: Schema.String.annotate({ description: "Display text (1-5 words, concise)" }),
   description: Schema.String.annotate({ description: "Explanation of choice" }),
 }).annotate({ identifier: "QuestionV2.Option" })
-export type Option = typeof Option.Type
+export interface Option extends Schema.Schema.Type<typeof Option> {}
 
 const base = {
   question: Schema.String.annotate({ description: "Complete question" }),
   header: Schema.String.annotate({ description: "Very short label (max 30 chars)" }),
   options: Schema.Array(Option).annotate({ description: "Available choices" }),
-  multiple: Schema.Boolean.pipe(Schema.optional).annotate({ description: "Allow selecting multiple choices" }),
+  multiple: Schema.Boolean.pipe(optional).annotate({ description: "Allow selecting multiple choices" }),
 }
 
 export const Info = Schema.Struct({
   ...base,
-  custom: Schema.Boolean.pipe(Schema.optional).annotate({
+  custom: Schema.Boolean.pipe(optional).annotate({
     description: "Allow typing a custom answer (default: true)",
   }),
 }).annotate({ identifier: "QuestionV2.Info" })
-export type Info = typeof Info.Type
+export interface Info extends Schema.Schema.Type<typeof Info> {}
 
 export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionV2.Prompt" })
-export type Prompt = typeof Prompt.Type
+export interface Prompt extends Schema.Schema.Type<typeof Prompt> {}
 
 export const Tool = Schema.Struct({
   messageID: Schema.String,
   callID: Schema.String,
 }).annotate({ identifier: "QuestionV2.Tool" })
-export type Tool = typeof Tool.Type
+export interface Tool extends Schema.Schema.Type<typeof Tool> {}
 
 export const Request = Schema.Struct({
   id: ID,
   sessionID: SessionID,
   questions: Schema.Array(Info).annotate({ description: "Questions to ask" }),
-  tool: Tool.pipe(Schema.optional),
+  tool: Tool.pipe(optional),
 }).annotate({ identifier: "QuestionV2.Request" })
-export type Request = typeof Request.Type
+export interface Request extends Schema.Schema.Type<typeof Request> {}
 
 export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionV2.Answer" })
 export type Answer = typeof Answer.Type
@@ -58,7 +65,7 @@ export const Reply = Schema.Struct({
     description: "User answers in order of questions (each answer is an array of selected labels)",
   }),
 }).annotate({ identifier: "QuestionV2.Reply" })
-export type Reply = typeof Reply.Type
+export interface Reply extends Schema.Schema.Type<typeof Reply> {}
 
 const Asked = define({ type: "question.v2.asked", schema: Request.fields })
 const Replied = define({

+ 11 - 8
packages/schema/src/reference.ts

@@ -1,6 +1,7 @@
 export * as Reference from "./reference"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { define, inventory } from "./event"
 import { AbsolutePath } from "./schema"
 
@@ -11,26 +12,28 @@ export interface LocalSource extends Schema.Schema.Type<typeof LocalSource> {}
 export const LocalSource = Schema.Struct({
   type: Schema.Literal("local"),
   path: AbsolutePath,
-  description: Schema.String.pipe(Schema.optional),
-  hidden: Schema.Boolean.pipe(Schema.optional),
+  description: Schema.String.pipe(optional),
+  hidden: Schema.Boolean.pipe(optional),
 }).annotate({ identifier: "Reference.LocalSource" })
 
 export interface GitSource extends Schema.Schema.Type<typeof GitSource> {}
 export const GitSource = Schema.Struct({
   type: Schema.Literal("git"),
   repository: Schema.String,
-  branch: Schema.String.pipe(Schema.optional),
-  description: Schema.String.pipe(Schema.optional),
-  hidden: Schema.Boolean.pipe(Schema.optional),
+  branch: Schema.String.pipe(optional),
+  description: Schema.String.pipe(optional),
+  hidden: Schema.Boolean.pipe(optional),
 }).annotate({ identifier: "Reference.GitSource" })
 
-export const Source = Schema.Union([LocalSource, GitSource]).pipe(Schema.toTaggedUnion("type"))
+export const Source = Schema.Union([LocalSource, GitSource])
+  .pipe(Schema.toTaggedUnion("type"))
+  .annotate({ identifier: "Reference.Source" })
 export type Source = typeof Source.Type
 
 export class Info extends Schema.Class<Info>("Reference.Info")({
   name: Schema.String,
   path: AbsolutePath,
-  description: Schema.String.pipe(Schema.optional),
-  hidden: Schema.Boolean.pipe(Schema.optional),
+  description: Schema.String.pipe(optional),
+  hidden: Schema.Boolean.pipe(optional),
   source: Source,
 }) {}

+ 8 - 7
packages/schema/src/revert.ts

@@ -1,6 +1,7 @@
 export * as Revert from "./revert"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { NonNegativeInt, RelativePath } from "./schema"
 import { SessionMessage } from "./session-message"
 
@@ -11,13 +12,13 @@ export const FileDiff = Schema.Struct({
   deletions: NonNegativeInt,
   patch: Schema.String,
 }).annotate({ identifier: "File.Diff" })
-export type FileDiff = typeof FileDiff.Type
+export interface FileDiff extends Schema.Schema.Type<typeof FileDiff> {}
 
 export const State = Schema.Struct({
   messageID: SessionMessage.ID,
-  partID: Schema.String.pipe(Schema.optional),
-  snapshot: Schema.String.pipe(Schema.optional),
-  diff: Schema.String.pipe(Schema.optional),
-  files: Schema.Array(FileDiff).pipe(Schema.optional),
-})
-export type State = typeof State.Type
+  partID: Schema.String.pipe(optional),
+  snapshot: Schema.String.pipe(optional),
+  diff: Schema.String.pipe(optional),
+  files: Schema.Array(FileDiff).pipe(optional),
+}).annotate({ identifier: "Revert.State" })
+export interface State extends Schema.Schema.Type<typeof State> {}

+ 1 - 1
packages/schema/src/schema.ts

@@ -11,7 +11,7 @@ export type AbsolutePath = typeof AbsolutePath.Type
 
 export const optional = <S extends Schema.Top>(schema: S) =>
   Schema.optionalKey(schema).pipe(
-    Schema.decodeTo(Schema.optional(schema), {
+    Schema.decodeTo(Schema.optional(Schema.toType(schema)), {
       decode: SchemaGetter.passthrough({ strict: false }),
       encode: SchemaGetter.transformOptional(Option.filter((value) => value !== undefined)),
     }),

+ 21 - 20
packages/schema/src/session-event.ts

@@ -1,6 +1,7 @@
 export * as SessionEvent from "./session-event"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { Event } from "./event"
 import { ProviderMetadata, ToolContent } from "./llm"
 import { Delivery } from "./session-delivery"
@@ -21,7 +22,7 @@ export const Source = Schema.Struct({
 }).annotate({
   identifier: "session.next.event.source",
 })
-export type Source = typeof Source.Type
+export interface Source extends Schema.Schema.Type<typeof Source> {}
 
 const Base = {
   timestamp: DateTimeUtcFromMillis,
@@ -78,7 +79,7 @@ export const Moved = Event.define({
   schema: {
     ...Base,
     location: Location.Ref,
-    subdirectory: RelativePath.pipe(Schema.optional),
+    subdirectory: RelativePath.pipe(optional),
   },
 })
 export type Moved = typeof Moved.Type
@@ -153,7 +154,7 @@ export namespace Step {
       assistantMessageID: SessionMessage.ID,
       agent: Schema.String,
       model: Model.Ref,
-      snapshot: Schema.String.pipe(Schema.optional),
+      snapshot: Schema.String.pipe(optional),
     },
   })
   export type Started = typeof Started.Type
@@ -175,8 +176,8 @@ export namespace Step {
           write: Schema.Finite,
         }),
       }),
-      snapshot: Schema.String.pipe(Schema.optional),
-      files: Schema.Array(RelativePath).pipe(Schema.optional),
+      snapshot: Schema.String.pipe(optional),
+      files: Schema.Array(RelativePath).pipe(optional),
     },
   })
   export type Ended = typeof Ended.Type
@@ -238,7 +239,7 @@ export namespace Reasoning {
       ...Base,
       assistantMessageID: SessionMessage.ID,
       reasoningID: Schema.String,
-      providerMetadata: ProviderMetadata.pipe(Schema.optional),
+      providerMetadata: ProviderMetadata.pipe(optional),
     },
   })
   export type Started = typeof Started.Type
@@ -263,7 +264,7 @@ export namespace Reasoning {
       assistantMessageID: SessionMessage.ID,
       reasoningID: Schema.String,
       text: Schema.String,
-      providerMetadata: ProviderMetadata.pipe(Schema.optional),
+      providerMetadata: ProviderMetadata.pipe(optional),
     },
   })
   export type Ended = typeof Ended.Type
@@ -317,7 +318,7 @@ export namespace Tool {
       input: Schema.Record(Schema.String, Schema.Unknown),
       provider: Schema.Struct({
         executed: Schema.Boolean,
-        metadata: ProviderMetadata.pipe(Schema.optional),
+        metadata: ProviderMetadata.pipe(optional),
       }),
     },
   })
@@ -332,7 +333,7 @@ export namespace Tool {
     ...options,
     schema: {
       ...ToolBase,
-      structured: Schema.Record(Schema.String, Schema.Any),
+      structured: Schema.Record(Schema.String, Schema.Unknown),
       content: Schema.Array(ToolContent),
     },
   })
@@ -343,13 +344,13 @@ export namespace Tool {
     ...options,
     schema: {
       ...ToolBase,
-      structured: Schema.Record(Schema.String, Schema.Any),
+      structured: Schema.Record(Schema.String, Schema.Unknown),
       content: Schema.Array(ToolContent),
-      outputPaths: Schema.Array(Schema.String).pipe(Schema.optional),
-      result: Schema.Unknown.pipe(Schema.optional),
+      outputPaths: Schema.Array(Schema.String).pipe(optional),
+      result: Schema.Unknown.pipe(optional),
       provider: Schema.Struct({
         executed: Schema.Boolean,
-        metadata: ProviderMetadata.pipe(Schema.optional),
+        metadata: ProviderMetadata.pipe(optional),
       }),
     },
   })
@@ -361,10 +362,10 @@ export namespace Tool {
     schema: {
       ...ToolBase,
       error: UnknownError,
-      result: Schema.Unknown.pipe(Schema.optional),
+      result: Schema.Unknown.pipe(optional),
       provider: Schema.Struct({
         executed: Schema.Boolean,
-        metadata: ProviderMetadata.pipe(Schema.optional),
+        metadata: ProviderMetadata.pipe(optional),
       }),
     },
   })
@@ -373,15 +374,15 @@ export namespace Tool {
 
 export const RetryError = Schema.Struct({
   message: Schema.String,
-  statusCode: Schema.Finite.pipe(Schema.optional),
+  statusCode: Schema.Finite.pipe(optional),
   isRetryable: Schema.Boolean,
-  responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
-  responseBody: Schema.String.pipe(Schema.optional),
-  metadata: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
+  responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(optional),
+  responseBody: Schema.String.pipe(optional),
+  metadata: Schema.Record(Schema.String, Schema.String).pipe(optional),
 }).annotate({
   identifier: "session.next.retry_error",
 })
-export type RetryError = typeof RetryError.Type
+export interface RetryError extends Schema.Schema.Type<typeof RetryError> {}
 
 export const Retried = Event.define({
   type: "session.next.retried",

+ 2 - 1
packages/schema/src/session-input.ts

@@ -1,6 +1,7 @@
 export * as SessionInput from "./session-input"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { Prompt } from "./prompt"
 import { DateTimeUtcFromMillis, NonNegativeInt } from "./schema"
 import { SessionDelivery } from "./session-delivery"
@@ -18,5 +19,5 @@ export const Admitted = Schema.Struct({
   prompt: Prompt,
   delivery: Delivery,
   timeCreated: DateTimeUtcFromMillis,
-  promotedSeq: NonNegativeInt.pipe(Schema.optional),
+  promotedSeq: NonNegativeInt.pipe(optional),
 }).annotate({ identifier: "SessionInput.Admitted" })

+ 26 - 25
packages/schema/src/session-message.ts

@@ -1,6 +1,7 @@
 export * as SessionMessage from "./session-message"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { ProviderMetadata, ToolContent } from "./llm"
 import { Model } from "./model"
 import { FileAttachment, Prompt } from "./prompt"
@@ -22,7 +23,7 @@ export const UnknownError = Schema.Struct({
 
 const Base = {
   id: ID,
-  metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
+  metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(optional),
   time: Schema.Struct({ created: DateTimeUtcFromMillis }),
 }
 
@@ -73,7 +74,7 @@ export const Shell = Schema.Struct({
   output: Schema.String,
   time: Schema.Struct({
     created: DateTimeUtcFromMillis,
-    completed: DateTimeUtcFromMillis.pipe(Schema.optional),
+    completed: DateTimeUtcFromMillis.pipe(optional),
   }),
 }).annotate({ identifier: "Session.Message.Shell" })
 
@@ -87,7 +88,7 @@ export interface ToolStateRunning extends Schema.Schema.Type<typeof ToolStateRun
 export const ToolStateRunning = Schema.Struct({
   status: Schema.Literal("running"),
   input: Schema.Record(Schema.String, Schema.Unknown),
-  structured: Schema.Record(Schema.String, Schema.Any),
+  structured: Schema.Record(Schema.String, Schema.Unknown),
   content: ToolContent.pipe(Schema.Array),
 }).annotate({ identifier: "Session.Message.ToolState.Running" })
 
@@ -95,11 +96,11 @@ export interface ToolStateCompleted extends Schema.Schema.Type<typeof ToolStateC
 export const ToolStateCompleted = Schema.Struct({
   status: Schema.Literal("completed"),
   input: Schema.Record(Schema.String, Schema.Unknown),
-  attachments: FileAttachment.pipe(Schema.Array, Schema.optional),
+  attachments: FileAttachment.pipe(Schema.Array, optional),
   content: ToolContent.pipe(Schema.Array),
-  outputPaths: Schema.Array(Schema.String).pipe(Schema.optional),
-  structured: Schema.Record(Schema.String, Schema.Any),
-  result: Schema.Unknown.pipe(Schema.optional),
+  outputPaths: Schema.Array(Schema.String).pipe(optional),
+  structured: Schema.Record(Schema.String, Schema.Unknown),
+  result: Schema.Unknown.pipe(optional),
 }).annotate({ identifier: "Session.Message.ToolState.Completed" })
 
 export interface ToolStateError extends Schema.Schema.Type<typeof ToolStateError> {}
@@ -107,9 +108,9 @@ export const ToolStateError = Schema.Struct({
   status: Schema.Literal("error"),
   input: Schema.Record(Schema.String, Schema.Unknown),
   content: ToolContent.pipe(Schema.Array),
-  structured: Schema.Record(Schema.String, Schema.Any),
+  structured: Schema.Record(Schema.String, Schema.Unknown),
   error: UnknownError,
-  result: Schema.Unknown.pipe(Schema.optional),
+  result: Schema.Unknown.pipe(optional),
 }).annotate({ identifier: "Session.Message.ToolState.Error" })
 
 export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]).pipe(
@@ -124,15 +125,15 @@ export const AssistantTool = Schema.Struct({
   name: Schema.String,
   provider: Schema.Struct({
     executed: Schema.Boolean,
-    metadata: ProviderMetadata.pipe(Schema.optional),
-    resultMetadata: ProviderMetadata.pipe(Schema.optional),
-  }).pipe(Schema.optional),
+    metadata: ProviderMetadata.pipe(optional),
+    resultMetadata: ProviderMetadata.pipe(optional),
+  }).pipe(optional),
   state: ToolState,
   time: Schema.Struct({
     created: DateTimeUtcFromMillis,
-    ran: DateTimeUtcFromMillis.pipe(Schema.optional),
-    completed: DateTimeUtcFromMillis.pipe(Schema.optional),
-    pruned: DateTimeUtcFromMillis.pipe(Schema.optional),
+    ran: DateTimeUtcFromMillis.pipe(optional),
+    completed: DateTimeUtcFromMillis.pipe(optional),
+    pruned: DateTimeUtcFromMillis.pipe(optional),
   }),
 }).annotate({ identifier: "Session.Message.Assistant.Tool" })
 
@@ -148,7 +149,7 @@ export const AssistantReasoning = Schema.Struct({
   type: Schema.Literal("reasoning"),
   id: Schema.String,
   text: Schema.String,
-  providerMetadata: ProviderMetadata.pipe(Schema.optional),
+  providerMetadata: ProviderMetadata.pipe(optional),
 }).annotate({ identifier: "Session.Message.Assistant.Reasoning" })
 
 export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning, AssistantTool]).pipe(
@@ -164,22 +165,22 @@ export const Assistant = Schema.Struct({
   model: Model.Ref,
   content: AssistantContent.pipe(Schema.Array),
   snapshot: Schema.Struct({
-    start: Schema.String.pipe(Schema.optional),
-    end: Schema.String.pipe(Schema.optional),
-    files: Schema.Array(RelativePath).pipe(Schema.optional),
-  }).pipe(Schema.optional),
-  finish: Schema.String.pipe(Schema.optional),
-  cost: Schema.Finite.pipe(Schema.optional),
+    start: Schema.String.pipe(optional),
+    end: Schema.String.pipe(optional),
+    files: Schema.Array(RelativePath).pipe(optional),
+  }).pipe(optional),
+  finish: Schema.String.pipe(optional),
+  cost: Schema.Finite.pipe(optional),
   tokens: Schema.Struct({
     input: Schema.Finite,
     output: Schema.Finite,
     reasoning: Schema.Finite,
     cache: Schema.Struct({ read: Schema.Finite, write: Schema.Finite }),
-  }).pipe(Schema.optional),
-  error: UnknownError.pipe(Schema.optional),
+  }).pipe(optional),
+  error: UnknownError.pipe(optional),
   time: Schema.Struct({
     created: DateTimeUtcFromMillis,
-    completed: DateTimeUtcFromMillis.pipe(Schema.optional),
+    completed: DateTimeUtcFromMillis.pipe(optional),
   }),
 }).annotate({ identifier: "Session.Message.Assistant" })
 

+ 3 - 2
packages/schema/src/session-status-event.ts

@@ -1,6 +1,7 @@
 export * as SessionStatusEvent from "./session-status-event"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { Event } from "./event"
 import { NonNegativeInt } from "./schema"
 import { SessionID } from "./session-id"
@@ -13,14 +14,14 @@ export const Info = Schema.Union([
     type: Schema.Literal("retry"),
     attempt: NonNegativeInt,
     message: Schema.String,
-    action: Schema.optional(
+    action: optional(
       Schema.Struct({
         reason: Schema.String,
         provider: Schema.String,
         title: Schema.String,
         message: Schema.String,
         label: Schema.String,
-        link: Schema.optional(Schema.String),
+        link: optional(Schema.String),
       }),
     ),
     next: NonNegativeInt,

+ 4 - 2
packages/schema/src/session-todo.ts

@@ -9,9 +9,11 @@ export const Info = Schema.Struct({
   status: Schema.String.annotate({
     description: "Current status of the task: pending, in_progress, completed, cancelled",
   }),
-  priority: Schema.String.annotate({ description: "Priority level of the task: high, medium, low" }),
+  priority: Schema.String.annotate({
+    description: "Priority level of the task: high, medium, low",
+  }),
 }).annotate({ identifier: "Todo" })
-export type Info = typeof Info.Type
+export interface Info extends Schema.Schema.Type<typeof Info> {}
 
 const Updated = define({
   type: "todo.updated",

+ 7 - 7
packages/schema/src/session.ts

@@ -20,8 +20,8 @@ export const Info = Schema.Struct({
   id: ID,
   parentID: ID.pipe(optional),
   projectID: Project.ID,
-  agent: Agent.ID.pipe(Schema.optional),
-  model: Model.Ref.pipe(Schema.optional),
+  agent: Agent.ID.pipe(optional),
+  model: Model.Ref.pipe(optional),
   cost: Schema.Finite,
   tokens: Schema.Struct({
     input: Schema.Finite,
@@ -35,17 +35,17 @@ export const Info = Schema.Struct({
   time: Schema.Struct({
     created: DateTimeUtcFromMillis,
     updated: DateTimeUtcFromMillis,
-    archived: DateTimeUtcFromMillis.pipe(Schema.optional),
+    archived: DateTimeUtcFromMillis.pipe(optional),
   }),
   title: Schema.String,
   location: Location.Ref,
-  subpath: RelativePath.pipe(Schema.optional),
-  revert: Revert.State.pipe(Schema.optional),
+  subpath: RelativePath.pipe(optional),
+  revert: Revert.State.pipe(optional),
 }).annotate({ identifier: "SessionV2.Info" })
 
 export const ListAnchor = Schema.Struct({
   id: ID,
   time: Schema.Finite,
   direction: Schema.Literals(["previous", "next"]),
-})
-export type ListAnchor = typeof ListAnchor.Type
+}).annotate({ identifier: "Session.ListAnchor" })
+export interface ListAnchor extends Schema.Schema.Type<typeof ListAnchor> {}

+ 3 - 2
packages/schema/src/skill.ts

@@ -1,6 +1,7 @@
 export * as Skill from "./skill"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { AbsolutePath } from "./schema"
 
 export interface DirectorySource extends Schema.Schema.Type<typeof DirectorySource> {}
@@ -18,8 +19,8 @@ export const UrlSource = Schema.Struct({
 export interface Info extends Schema.Schema.Type<typeof Info> {}
 export const Info = Schema.Struct({
   name: Schema.String,
-  description: Schema.String.pipe(Schema.optional),
-  slash: Schema.Boolean.pipe(Schema.optional),
+  description: Schema.String.pipe(optional),
+  slash: Schema.Boolean.pipe(optional),
   location: AbsolutePath,
   content: Schema.String,
 }).annotate({ identifier: "SkillV2.Info" })

+ 2 - 1
packages/schema/src/tui-event.ts

@@ -1,6 +1,7 @@
 export * as TuiEvent from "./tui-event"
 
 import { Effect, Schema } from "effect"
+import { optional } from "./schema"
 import { Event } from "./event"
 import { PositiveInt } from "./schema"
 import { SessionID } from "./session-id"
@@ -39,7 +40,7 @@ export const CommandExecute = Event.define({
 export const ToastShow = Event.define({
   type: "tui.toast.show",
   schema: {
-    title: Schema.optional(Schema.String),
+    title: optional(Schema.String),
     message: Schema.String,
     variant: Schema.Literals(["info", "success", "warning", "error"]),
     duration: PositiveInt.pipe(Schema.withDecodingDefault(Effect.succeed(DEFAULT_TOAST_DURATION))).annotate({

+ 2 - 1
packages/schema/src/vcs-event.ts

@@ -1,12 +1,13 @@
 export * as VcsEvent from "./vcs-event"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { Event } from "./event"
 
 export const BranchUpdated = Event.define({
   type: "vcs.branch.updated",
   schema: {
-    branch: Schema.optional(Schema.String),
+    branch: optional(Schema.String),
   },
 })
 

+ 2 - 2
packages/schema/src/workspace-event.ts

@@ -7,8 +7,8 @@ import { WorkspaceID } from "./workspace-id"
 export const ConnectionStatus = Schema.Struct({
   workspaceID: WorkspaceID,
   status: Schema.Literals(["connected", "connecting", "disconnected", "error"]),
-})
-export type ConnectionStatus = typeof ConnectionStatus.Type
+}).annotate({ identifier: "WorkspaceEvent.ConnectionStatus" })
+export interface ConnectionStatus extends Schema.Schema.Type<typeof ConnectionStatus> {}
 
 export const Ready = Event.define({
   type: "workspace.ready",

+ 2 - 1
packages/schema/src/worktree-event.ts

@@ -1,13 +1,14 @@
 export * as WorktreeEvent from "./worktree-event"
 
 import { Schema } from "effect"
+import { optional } from "./schema"
 import { Event } from "./event"
 
 export const Ready = Event.define({
   type: "worktree.ready",
   schema: {
     name: Schema.String,
-    branch: Schema.optional(Schema.String),
+    branch: optional(Schema.String),
   },
 })
 

+ 67 - 0
packages/schema/test/contract-hygiene.test.ts

@@ -0,0 +1,67 @@
+import { describe, expect, test } from "bun:test"
+import { Schema } from "effect"
+import { Agent } from "../src/agent"
+import { FileSystem } from "../src/filesystem"
+import { Model } from "../src/model"
+import { Project } from "../src/project"
+import { Pty } from "../src/pty"
+import { Question } from "../src/question"
+import { Session } from "../src/session"
+import { SessionEvent } from "../src/session-event"
+import { SessionTodo } from "../src/session-todo"
+import { optional } from "../src/schema"
+
+describe("contract hygiene", () => {
+  test("optional properties preserve transformations and omit undefined while encoding", () => {
+    const Value = Schema.Struct({ value: optional(Schema.FiniteFromString) })
+    expect(Schema.decodeUnknownSync(Value)({ value: "1" })).toEqual({ value: 1 })
+    expect(Schema.encodeSync(Value)({ value: 1 })).toEqual({ value: "1" })
+    expect(Schema.encodeSync(Value)({ value: undefined })).toEqual({})
+  })
+
+  test("todo status and priority preserve arbitrary strings", () => {
+    const decode = Schema.decodeUnknownSync(SessionTodo.Info)
+    expect(decode({ content: "ship", status: "waiting", priority: "urgent" })).toEqual({
+      content: "ship",
+      status: "waiting",
+      priority: "urgent",
+    })
+  })
+
+  test("current ID constructors expose create", () => {
+    expect(Question.ID.create()).toStartWith("que_")
+    expect(Pty.ID.create()).toStartWith("pty_")
+  })
+
+  test("reusable public identifiers are stable and unique", () => {
+    const identifiers = [
+      Agent.Color,
+      FileSystem.Submatch,
+      Model.Ref,
+      Model.Capabilities,
+      Model.Cost,
+      Model.Api,
+      Project.Icon,
+      Project.Commands,
+      Project.Time,
+      Project.Info,
+      Pty.Info,
+      Session.ListAnchor,
+    ].map((schema) => schema.ast.annotations?.identifier)
+
+    expect(identifiers.every((identifier) => typeof identifier === "string")).toBe(true)
+    expect(new Set(identifiers).size).toBe(identifiers.length)
+  })
+
+  test("current source avoids Any and mutable contract wrappers", async () => {
+    const files = [...new Bun.Glob("*.ts").scanSync(new URL("../src", import.meta.url).pathname)].filter(
+      (file) => !file.endsWith("-v1.ts"),
+    )
+    const source = await Promise.all(
+      files.map((file) => Bun.file(new URL(`../src/${file}`, import.meta.url)).text()),
+    ).then((values) => values.join("\n"))
+
+    expect(source).not.toContain("Schema.Any")
+    expect(source).not.toContain("Schema.mutable")
+  })
+})

+ 7 - 21
packages/sdk/js/src/v2/gen/sdk.gen.ts

@@ -111,6 +111,7 @@ import type {
   McpRemoteConfig,
   McpStatusErrors,
   McpStatusResponses,
+  ModelRef,
   MoveSessionDestination,
   OutputFormat,
   Part as Part2,
@@ -129,10 +130,12 @@ import type {
   PermissionRuleset,
   PermissionV2Reply,
   PermissionV2Source,
+  ProjectCommands,
   ProjectCurrentErrors,
   ProjectCurrentResponses,
   ProjectDirectoriesErrors,
   ProjectDirectoriesResponses,
+  ProjectIcon,
   ProjectInitGitErrors,
   ProjectInitGitResponses,
   ProjectListErrors,
@@ -2616,17 +2619,8 @@ export class Project extends HeyApiClient {
       directory?: string
       workspace?: string
       name?: string
-      icon?: {
-        url?: string
-        override?: string
-        color?: string
-      }
-      commands?: {
-        /**
-         * Startup script to run when creating a new workspace (worktree)
-         */
-        start?: string
-      }
+      icon?: ProjectIcon
+      commands?: ProjectCommands
     },
     options?: Options<never, ThrowOnError>,
   ) {
@@ -5471,11 +5465,7 @@ export class Session3 extends HeyApiClient {
     parameters?: {
       id?: string
       agent?: string
-      model?: {
-        id: string
-        providerID: string
-        variant?: string
-      }
+      model?: ModelRef
       location?: LocationRef
     },
     options?: Options<never, ThrowOnError>,
@@ -5571,11 +5561,7 @@ export class Session3 extends HeyApiClient {
   public switchModel<ThrowOnError extends boolean = false>(
     parameters: {
       sessionID: string
-      model?: {
-        id: string
-        providerID: string
-        variant?: string
-      }
+      model?: ModelRef
     },
     options?: Options<never, ThrowOnError>,
   ) {

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 224 - 334
packages/sdk/js/src/v2/gen/types.gen.ts


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است