diff --git a/.github/workflows/prompty-swift-check.yml b/.github/workflows/prompty-swift-check.yml new file mode 100644 index 000000000..9ed326167 --- /dev/null +++ b/.github/workflows/prompty-swift-check.yml @@ -0,0 +1,54 @@ +name: prompty Swift build and test +on: + pull_request: + paths: + - 'runtime/swift/**' + - 'schema/**' + - '.github/workflows/prompty-swift-check.yml' + + workflow_call: + +jobs: + swift-tests: + name: test Swift on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + + # Linux has no Xcode, so install a pinned standalone Swift toolchain. + - name: Install Swift toolchain (Linux) + if: runner.os == 'Linux' + uses: swift-actions/setup-swift@v2 + with: + swift-version: '6.0' + + # macOS ships Xcode whose bundled Swift compiler is matched to its SDK. + # A standalone toolchain from setup-swift pins the compiler but still uses + # the runner's newer Xcode SDK, which fails with "this SDK is not supported + # by the compiler". Select Xcode explicitly and use its bundled Swift so the + # compiler and SDK stay in lockstep. See issue #474 (toolchain pinning). + - name: Select Xcode (macOS) + if: runner.os == 'macOS' + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Report Swift version + run: swift --version + + # The generated model package is standalone (no remote dependencies) and + # is the source of truth for the runtime contract, so it is built first. + - name: Build and test prompty-model + working-directory: runtime/swift/prompty-model + run: swift test + + # The hand-written runtime depends on prompty-model (local path) and Yams. + - name: Build and test prompty runtime + working-directory: runtime/swift/prompty + run: swift test diff --git a/.gitignore b/.gitignore index 7463ed573..09a2c680f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,11 @@ schema/tsp-output/.typra-generated/report.json !schema/tsp-output/json-ast/ !schema/tsp-output/json-ast/model.json .playwright-mcp/ + +# SwiftPM build artifacts and resolved dependency pins. +# Package.resolved is untracked because the Swift runtime is a library: pinning +# transitive versions here would constrain every consumer. +.build/ +.swiftpm/ +Package.resolved +.swift-toolchain-stamp diff --git a/runtime/swift/README.md b/runtime/swift/README.md new file mode 100644 index 000000000..045a17aa9 --- /dev/null +++ b/runtime/swift/README.md @@ -0,0 +1,222 @@ +# Prompty for Swift + +A Swift implementation of the Prompty runtime: load a `.prompty` file, render it, +turn it into messages, call a model, and read the result back. + +The Rust runtime is the behavioral reference for this port, and both are checked +against the same cross-runtime vectors in [`spec/vectors`](../../spec/vectors). + +## Layout + +Two SwiftPM packages live here, and the split is deliberate. + +| Package | Module | Contents | Hand-written? | +| --------------- | -------------- | ------------------------------------------- | ------------------ | +| `prompty-model` | `PromptyModel` | The domain types and pipeline protocols | No — generated | +| `prompty` | `Prompty` | Loader, renderers, parser, registry, harness | Yes | +| `prompty` | `PromptyOpenAI`| The OpenAI executor and processor | Yes | + +`prompty-model` is emitted from the TypeSpec definitions in [`schema`](../../schema) +by the Typra emitter. **Never edit anything under `prompty-model/Sources` by hand.** +Every file there is overwritten by the next generation run. If a generated type is +wrong, the fix belongs in the schema or in the emitter — see +[Generated model](#generated-model) below. + +The runtime does not define its own domain types. `Prompty`, `Model`, `Message`, +`ContentPart`, `Tool` and the four pipeline protocols (`Renderer`, `Parser`, +`Executor`, `Processor`) all come from `PromptyModel`, and the hand-written code +conforms to them. + +## Using it + +```swift +import Prompty +import PromptyOpenAI + +Registry.shared.registerDefaults() // jinja2 + mustache renderers, prompty parser +registerOpenAI() // openai executor + processor + +let answer = try await Pipeline.invoke( + path: "basic.prompty", + inputs: ["question": "What is the capital of Iceland?"] +) +``` + +`Pipeline.invoke` is the whole flow. The individual stages are available when you +need to step into the middle of it: + +```swift +let agent = try Loader.load(path: "basic.prompty") +let messages = try await Pipeline.prepare(agent, inputs: inputs) // render + parse +let raw = try await Pipeline.run(agent, messages: messages) // execute + process +``` + +### Tool calls + +When a prompt declares tools, the host drives the loop. Read the calls, then ask +for the arguments the tool should actually receive — that second step is where +tool bindings are applied: + +```swift +let raw = try await Pipeline.run(agent, messages: messages) + +for call in Pipeline.toolCalls(in: raw) { + let args = Pipeline.boundArguments(agent, call: call, inputs: inputs) + let result = try myTools[call.name]!(args) + results.append(result) +} +``` + +The recorded `call` is left as the provider sent it. That matters: a bound value +is hidden from the model on purpose, and `Pipeline.toolMessages` replays the +call's own `arguments` on the next round, so writing the value back into the +call would hand the model exactly what the binding withheld. + +A parameter listed under a tool's `bindings` is deliberately hidden from the +model, and the runtime supplies it from the prompt's own inputs instead: + +```yaml +tools: + - name: get_weather + kind: function + bindings: + unit: + input: preferred_unit # the model never sees `unit`; this fills it in +``` + +`Pipeline.toolCalls(in:)` always returns the model's arguments untouched, so +`Pipeline.boundArguments(_:call:inputs:)` at the dispatch site is what makes a +binding take effect. Skipping it leaves the bound parameter missing entirely — +it was already stripped from the schema, so the model never supplied it. + +Bindings are applied only when the provider's payload is a JSON object (or is +empty, which is the no-argument call). An array, a scalar, or malformed JSON is +passed through rather than replaced by an object holding only the bound values. + +Streaming, structured output and tool calls are covered in +[`Tests/PromptyTests/LiveOpenAITests.swift`](prompty/Tests/PromptyTests/LiveOpenAITests.swift), +which exercises each of them against the real API. + +## Building and testing + +Requires a Swift 6.x toolchain. + +```bash +cd runtime/swift/prompty +swift build +swift test +``` + +### On Windows + +SwiftPM shells out to `git`, and a bare repository in the parent tree makes those +calls fail. Set the escape hatch before building: + +```powershell +$env:GIT_CONFIG_COUNT='1' +$env:GIT_CONFIG_KEY_0='safe.bareRepository' +$env:GIT_CONFIG_VALUE_0='all' +``` + +Incremental builds suppress warnings that a clean build reports. When you care +about the warning output, clean first: + +```powershell +swift package clean +swift build --build-tests +``` + +### Live tests + +Most tests are offline. The tests in `LiveOpenAITests` call the real OpenAI API +and **skip themselves** when `OPENAI_API_KEY` is missing, so a checkout without +credentials still runs a full green suite. + +To run them, put a `.env` beside `Package.swift`: + +``` +OPENAI_API_KEY=sk-... +OPENAI_MODEL=gpt-4o-mini +``` + +That file is ignored by git and must never be committed. The runtime itself never +reads `.env` — populating the environment is the host's job, so the loading lives +in the test, not the library. + +## Generated model + +Regenerate after changing anything in [`schema`](../../schema): + +```bash +cd schema +npm install +npm run generate +``` + +Generation also touches the other runtimes. Keep a Swift change reviewable by +reverting the rest: + +```bash +git checkout -- runtime/rust runtime/python runtime/typescript runtime/go runtime/csharp vscode +``` + +### The emitter shim + +`schema/scripts/patch-swift-emitter-defects.mjs` runs as part of generation and +repairs output that the Swift emitter gets wrong today — most importantly, base +fields dropped from types that `extend` another type, which affects three +`Property` subtypes and all five `Tool` subtypes. + +The shim is a scripted post-generation step, so the generated files are still +never hand-edited. It is pinned to the emitter version it was written against and +**fails loudly** rather than silently mis-patching when it sees a version it does +not recognise, when an anchor it expects is missing, or when it finds a file in a +half-patched state. + +Each defect has been reported upstream. When a release fixes one, delete the +corresponding patch and re-run generation: the shim is meant to shrink to nothing +and then be removed. + +`GeneratedModelRoundTripTests` covers every field the shim injects, so a silently +regressed patch fails the suite rather than the runtime. + +## Conformance + +`Tests/PromptyTests` runs the shared vectors from [`spec/vectors`](../../spec/vectors) +— loading, rendering, parsing, provider wire format, response processing, and +harness replay — plus Swift-specific regression tests for defects the vectors +cannot express, such as Windows line endings. + +### Coverage against the shared vectors + +This port is **not parity-complete**. Six of the ten shared vector files are +exercised, and two of those six run only their OpenAI subset. The other four +describe surface area this runtime does not implement. That is a deliberate +scoping decision for an initial port, not an oversight. + +| Vector file | Cases | Status | +| --------------------------------------- | ------: | ------------------------------------- | +| `load/load_vectors.json` | 25 | Run | +| `render/render_vectors.json` | 23 | Run | +| `parse/parse_vectors.json` | 15 | Run | +| `wire/wire_vectors.json` | 22 / 27 | Run — 5 Anthropic cases skipped | +| `process/process_vectors.json` | 17 / 21 | Run — 4 Anthropic cases skipped | +| `harness/replay_vectors.json` | 5 | Run | +| `engine/turn_vectors.json` | 5 | **Not wired** — engine incomplete | +| `agent/agent_vectors.json` | 28 | **Not implemented** — no agent layer | +| `discovery/discovery_vectors.json` | 7 | **Not implemented** — no discovery | +| `discovery/enrichment_vectors.json` | 9 | **Not implemented** — no enrichment | + +The nine skipped Anthropic cases are provider coverage, not a contract gap: this +package ships the OpenAI provider only, so `WireVectorTests` and +`ProcessVectorTests` filter on `input.provider`. An Anthropic package would pick +them up unchanged. + +The turn engine is the substantive gap. `ReferenceTurnRunner` already implements +the iteration loop, permission mediation, host tool execution, and checkpointing, +so three of the five engine vectors (`final_output`, `ordered_tool_round`, +`permission_denial_is_model_visible`) describe behavior that exists but is not +yet asserted against the shared file. The remaining two — `delegated_provider_state` +and `cancel_before_context` — need delegated provider state and cancellation, +which this port does not provide. Wiring the engine vectors and closing those two +capabilities is follow-up work tracked separately from this PR. diff --git a/runtime/swift/prompty-model/Package.swift b/runtime/swift/prompty-model/Package.swift new file mode 100644 index 000000000..3c61c07f6 --- /dev/null +++ b/runtime/swift/prompty-model/Package.swift @@ -0,0 +1,15 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "PromptyModel", + platforms: [.macOS(.v12), .iOS(.v15)], + products: [.library(name: "PromptyModel", targets: ["PromptyModel"])], + dependencies: [ + .package(url: "https://github.com/jpsim/Yams.git", from: "5.1.3") + ], + targets: [ + .target(name: "PromptyModel", dependencies: [.product(name: "Yams", package: "Yams")], path: "Sources/PromptyModel"), + .testTarget(name: "PromptyModelTests", dependencies: ["PromptyModel"], path: "Tests/PromptyModelTests") + ] +) diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Connectivity/authorization_code_flow.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Connectivity/authorization_code_flow.swift new file mode 100644 index 000000000..2c209f97a --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Connectivity/authorization_code_flow.swift @@ -0,0 +1,68 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Provider-neutral initialization result for an authorization-code flow. +public struct AuthorizationCodeFlow: TypraModel { + public static let shorthandProperty: String? = nil + public var authUrl: String = "" + public var codeVerifier: String = "" + + public init(authUrl: String = "", codeVerifier: String = "") { + self.authUrl = authUrl + self.codeVerifier = codeVerifier + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> AuthorizationCodeFlow { + let object = try TypraRuntime.object(data, typeName: "AuthorizationCodeFlow") + var instance = AuthorizationCodeFlow() + if let value = object["authUrl"] { + instance.authUrl = try TypraRuntime.string(value, field: "authUrl") + } + if let value = object["codeVerifier"] { + instance.codeVerifier = try TypraRuntime.string(value, field: "codeVerifier") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["authUrl"] = self.authUrl + result["codeVerifier"] = self.codeVerifier + return result + } + + public func toWire(_ provider: String, context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + let wireNameAuthUrl: String? + switch provider { + case "foundry": wireNameAuthUrl = "auth_url" + default: wireNameAuthUrl = nil + } + if let wireKey = wireNameAuthUrl { result[wireKey] = self.authUrl } + let wireNameCodeVerifier: String? + switch provider { + case "foundry": wireNameCodeVerifier = "code_verifier" + default: wireNameCodeVerifier = nil + } + if let wireKey = wireNameCodeVerifier { result[wireKey] = self.codeVerifier } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> AuthorizationCodeFlow { + return try load(TypraRuntime.jsonObject(from: json, typeName: "AuthorizationCodeFlow"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> AuthorizationCodeFlow { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "AuthorizationCodeFlow"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Connectivity/connection.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Connectivity/connection.swift new file mode 100644 index 000000000..1abace8d9 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Connectivity/connection.swift @@ -0,0 +1,529 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum AuthenticationMode: String, Codable, CaseIterable { + case user = "user" + case system = "system" + public static func parse(_ value: String) throws -> AuthenticationMode { + switch value { + case "user": return .user + case "system": return .system + default: throw TypraRuntimeError.invalidEnum(type: "AuthenticationMode", value: value) + } + } +} + +public enum Connection: TypraModel { + case referenceConnection(ReferenceConnection) + case remoteConnection(RemoteConnection) + case apiKeyConnection(ApiKeyConnection) + case anonymousConnection(AnonymousConnection) + case oAuthConnection(OAuthConnection) + case foundryConnection(FoundryConnection) + case unknown([String: Any]) + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> Connection { + let normalizedData: Any = data + let object = try TypraRuntime.object(normalizedData, typeName: "Connection") + let discriminator = try TypraRuntime.string(object["kind"] ?? NSNull(), field: "kind") + if discriminator.isEmpty { + throw TypraRuntimeError.invalidField(field: context.at("kind").path, expected: "non-blank string") + } + switch discriminator { + case "reference": return .referenceConnection(try ReferenceConnection.load(normalizedData, context: context)) + case "remote": return .remoteConnection(try RemoteConnection.load(normalizedData, context: context)) + case "key": return .apiKeyConnection(try ApiKeyConnection.load(normalizedData, context: context)) + case "anonymous": return .anonymousConnection(try AnonymousConnection.load(normalizedData, context: context)) + case "oauth": return .oAuthConnection(try OAuthConnection.load(normalizedData, context: context)) + case "foundry": return .foundryConnection(try FoundryConnection.load(normalizedData, context: context)) + default: return .unknown(object) + } + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + switch self { + case .referenceConnection(let value): return try value.save(context) + case .remoteConnection(let value): return try value.save(context) + case .apiKeyConnection(let value): return try value.save(context) + case .anonymousConnection(let value): return try value.save(context) + case .oAuthConnection(let value): return try value.save(context) + case .foundryConnection(let value): return try value.save(context) + case .unknown(let value): return value + } + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> Connection { + return try load(TypraRuntime.jsonObject(from: json, typeName: "Connection"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> Connection { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "Connection"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +/// Connection configuration for AI services using named connections. +public struct ReferenceConnection: TypraModel { + public static let shorthandProperty: String? = nil + public var kind: String = "reference" + public var authenticationMode: AuthenticationMode? = nil + public var usageDescription: String? = nil + public var name: String = "" + public var target: String? = nil + + public init(kind: String = "reference", authenticationMode: AuthenticationMode? = nil, usageDescription: String? = nil, name: String = "", target: String? = nil) { + self.kind = kind + self.authenticationMode = authenticationMode + self.usageDescription = usageDescription + self.name = name + self.target = target + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ReferenceConnection { + let object = try TypraRuntime.object(data, typeName: "ReferenceConnection") + var instance = ReferenceConnection() + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "reference" + } + if let value = object["authenticationMode"] { + instance.authenticationMode = try AuthenticationMode.parse(try TypraRuntime.string(value, field: "authenticationMode")) + } + if let value = object["usageDescription"] { + instance.usageDescription = try TypraRuntime.string(value, field: "usageDescription") + } + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + if let value = object["target"] { + instance.target = try TypraRuntime.string(value, field: "target") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["kind"] = self.kind + if let value = self.authenticationMode { + result["authenticationMode"] = value.rawValue + } + if let value = self.usageDescription { + result["usageDescription"] = value + } + result["name"] = self.name + if let value = self.target { + result["target"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ReferenceConnection { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ReferenceConnection"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ReferenceConnection { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ReferenceConnection"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +/// Connection configuration for AI services using named connections. +public struct RemoteConnection: TypraModel { + public static let shorthandProperty: String? = nil + public var kind: String = "remote" + public var authenticationMode: AuthenticationMode? = nil + public var usageDescription: String? = nil + public var name: String = "" + public var endpoint: String = "" + + public init(kind: String = "remote", authenticationMode: AuthenticationMode? = nil, usageDescription: String? = nil, name: String = "", endpoint: String = "") { + self.kind = kind + self.authenticationMode = authenticationMode + self.usageDescription = usageDescription + self.name = name + self.endpoint = endpoint + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> RemoteConnection { + let object = try TypraRuntime.object(data, typeName: "RemoteConnection") + var instance = RemoteConnection() + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "remote" + } + if let value = object["authenticationMode"] { + instance.authenticationMode = try AuthenticationMode.parse(try TypraRuntime.string(value, field: "authenticationMode")) + } + if let value = object["usageDescription"] { + instance.usageDescription = try TypraRuntime.string(value, field: "usageDescription") + } + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + if let value = object["endpoint"] { + instance.endpoint = try TypraRuntime.string(value, field: "endpoint") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["kind"] = self.kind + if let value = self.authenticationMode { + result["authenticationMode"] = value.rawValue + } + if let value = self.usageDescription { + result["usageDescription"] = value + } + result["name"] = self.name + result["endpoint"] = self.endpoint + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> RemoteConnection { + return try load(TypraRuntime.jsonObject(from: json, typeName: "RemoteConnection"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> RemoteConnection { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "RemoteConnection"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +/// Connection configuration for AI services using API keys. +public struct ApiKeyConnection: TypraModel { + public static let shorthandProperty: String? = nil + public var kind: String = "key" + public var authenticationMode: AuthenticationMode? = nil + public var usageDescription: String? = nil + public var endpoint: String = "" + public var apiKey: String = "" + + public init(kind: String = "key", authenticationMode: AuthenticationMode? = nil, usageDescription: String? = nil, endpoint: String = "", apiKey: String = "") { + self.kind = kind + self.authenticationMode = authenticationMode + self.usageDescription = usageDescription + self.endpoint = endpoint + self.apiKey = apiKey + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ApiKeyConnection { + let object = try TypraRuntime.object(data, typeName: "ApiKeyConnection") + var instance = ApiKeyConnection() + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "key" + } + if let value = object["authenticationMode"] { + instance.authenticationMode = try AuthenticationMode.parse(try TypraRuntime.string(value, field: "authenticationMode")) + } + if let value = object["usageDescription"] { + instance.usageDescription = try TypraRuntime.string(value, field: "usageDescription") + } + if let value = object["endpoint"] { + instance.endpoint = try TypraRuntime.string(value, field: "endpoint") + } + if let value = object["apiKey"] { + instance.apiKey = try TypraRuntime.string(value, field: "apiKey") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["kind"] = self.kind + if let value = self.authenticationMode { + result["authenticationMode"] = value.rawValue + } + if let value = self.usageDescription { + result["usageDescription"] = value + } + result["endpoint"] = self.endpoint + result["apiKey"] = self.apiKey + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ApiKeyConnection { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ApiKeyConnection"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ApiKeyConnection { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ApiKeyConnection"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +public struct AnonymousConnection: TypraModel { + public static let shorthandProperty: String? = nil + public var kind: String = "anonymous" + public var authenticationMode: AuthenticationMode? = nil + public var usageDescription: String? = nil + public var endpoint: String = "" + + public init(kind: String = "anonymous", authenticationMode: AuthenticationMode? = nil, usageDescription: String? = nil, endpoint: String = "") { + self.kind = kind + self.authenticationMode = authenticationMode + self.usageDescription = usageDescription + self.endpoint = endpoint + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> AnonymousConnection { + let object = try TypraRuntime.object(data, typeName: "AnonymousConnection") + var instance = AnonymousConnection() + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "anonymous" + } + if let value = object["authenticationMode"] { + instance.authenticationMode = try AuthenticationMode.parse(try TypraRuntime.string(value, field: "authenticationMode")) + } + if let value = object["usageDescription"] { + instance.usageDescription = try TypraRuntime.string(value, field: "usageDescription") + } + if let value = object["endpoint"] { + instance.endpoint = try TypraRuntime.string(value, field: "endpoint") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["kind"] = self.kind + if let value = self.authenticationMode { + result["authenticationMode"] = value.rawValue + } + if let value = self.usageDescription { + result["usageDescription"] = value + } + result["endpoint"] = self.endpoint + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> AnonymousConnection { + return try load(TypraRuntime.jsonObject(from: json, typeName: "AnonymousConnection"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> AnonymousConnection { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "AnonymousConnection"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +/// Connection configuration using OAuth 2.0 client credentials. Useful for tools and services that require OAuth authentication, such as MCP servers, OpenAPI endpoints, or other REST APIs. +public struct OAuthConnection: TypraModel { + public static let shorthandProperty: String? = nil + public var kind: String = "oauth" + public var authenticationMode: AuthenticationMode? = nil + public var usageDescription: String? = nil + public var endpoint: String = "" + public var clientId: String = "" + public var clientSecret: String = "" + public var tokenUrl: String = "" + public var scopes: [String]? = nil + + public init(kind: String = "oauth", authenticationMode: AuthenticationMode? = nil, usageDescription: String? = nil, endpoint: String = "", clientId: String = "", clientSecret: String = "", tokenUrl: String = "", scopes: [String]? = nil) { + self.kind = kind + self.authenticationMode = authenticationMode + self.usageDescription = usageDescription + self.endpoint = endpoint + self.clientId = clientId + self.clientSecret = clientSecret + self.tokenUrl = tokenUrl + self.scopes = scopes + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> OAuthConnection { + let object = try TypraRuntime.object(data, typeName: "OAuthConnection") + var instance = OAuthConnection() + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "oauth" + } + if let value = object["authenticationMode"] { + instance.authenticationMode = try AuthenticationMode.parse(try TypraRuntime.string(value, field: "authenticationMode")) + } + if let value = object["usageDescription"] { + instance.usageDescription = try TypraRuntime.string(value, field: "usageDescription") + } + if let value = object["endpoint"] { + instance.endpoint = try TypraRuntime.string(value, field: "endpoint") + } + if let value = object["clientId"] { + instance.clientId = try TypraRuntime.string(value, field: "clientId") + } + if let value = object["clientSecret"] { + instance.clientSecret = try TypraRuntime.string(value, field: "clientSecret") + } + if let value = object["tokenUrl"] { + instance.tokenUrl = try TypraRuntime.string(value, field: "tokenUrl") + } + if let value = object["scopes"] { + instance.scopes = try TypraRuntime.array(value, field: "scopes").map { try TypraRuntime.string($0, field: "scopes") } + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["kind"] = self.kind + if let value = self.authenticationMode { + result["authenticationMode"] = value.rawValue + } + if let value = self.usageDescription { + result["usageDescription"] = value + } + result["endpoint"] = self.endpoint + result["clientId"] = self.clientId + result["clientSecret"] = self.clientSecret + result["tokenUrl"] = self.tokenUrl + if let value = self.scopes { + result["scopes"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> OAuthConnection { + return try load(TypraRuntime.jsonObject(from: json, typeName: "OAuthConnection"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> OAuthConnection { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "OAuthConnection"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +/// Connection configuration for Microsoft Foundry projects. Provides project-scoped access to models, tools, and services via Entra ID (DefaultAzureCredential) authentication. +public struct FoundryConnection: TypraModel { + public static let shorthandProperty: String? = nil + public var kind: String = "foundry" + public var authenticationMode: AuthenticationMode? = nil + public var usageDescription: String? = nil + public var endpoint: String = "" + public var name: String? = nil + public var connectionType: String? = nil + + public init(kind: String = "foundry", authenticationMode: AuthenticationMode? = nil, usageDescription: String? = nil, endpoint: String = "", name: String? = nil, connectionType: String? = nil) { + self.kind = kind + self.authenticationMode = authenticationMode + self.usageDescription = usageDescription + self.endpoint = endpoint + self.name = name + self.connectionType = connectionType + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> FoundryConnection { + let object = try TypraRuntime.object(data, typeName: "FoundryConnection") + var instance = FoundryConnection() + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "foundry" + } + if let value = object["authenticationMode"] { + instance.authenticationMode = try AuthenticationMode.parse(try TypraRuntime.string(value, field: "authenticationMode")) + } + if let value = object["usageDescription"] { + instance.usageDescription = try TypraRuntime.string(value, field: "usageDescription") + } + if let value = object["endpoint"] { + instance.endpoint = try TypraRuntime.string(value, field: "endpoint") + } + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + if let value = object["connectionType"] { + instance.connectionType = try TypraRuntime.string(value, field: "connectionType") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["kind"] = self.kind + if let value = self.authenticationMode { + result["authenticationMode"] = value.rawValue + } + if let value = self.usageDescription { + result["usageDescription"] = value + } + result["endpoint"] = self.endpoint + if let value = self.name { + result["name"] = value + } + if let value = self.connectionType { + result["connectionType"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> FoundryConnection { + return try load(TypraRuntime.jsonObject(from: json, typeName: "FoundryConnection"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> FoundryConnection { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "FoundryConnection"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Connectivity/device_authorization.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Connectivity/device_authorization.swift new file mode 100644 index 000000000..59d949969 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Connectivity/device_authorization.swift @@ -0,0 +1,119 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Provider-neutral device authorization challenge shown to a user. +public struct DeviceAuthorization: TypraModel { + public static let shorthandProperty: String? = nil + public var deviceCode: String = "" + public var userCode: String = "" + public var verificationUri: String = "" + public var expiresIn: Int64 = 0 + public var interval: Int64 = 0 + public var message: String = "" + + public init(deviceCode: String = "", userCode: String = "", verificationUri: String = "", expiresIn: Int64 = 0, interval: Int64 = 0, message: String = "") { + self.deviceCode = deviceCode + self.userCode = userCode + self.verificationUri = verificationUri + self.expiresIn = expiresIn + self.interval = interval + self.message = message + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> DeviceAuthorization { + let object = try TypraRuntime.object(data, typeName: "DeviceAuthorization") + var instance = DeviceAuthorization() + if let value = object["deviceCode"] { + instance.deviceCode = try TypraRuntime.string(value, field: "deviceCode") + } + if let value = object["userCode"] { + instance.userCode = try TypraRuntime.string(value, field: "userCode") + } + if let value = object["verificationUri"] { + instance.verificationUri = try TypraRuntime.string(value, field: "verificationUri") + } + if let value = object["expiresIn"] { + instance.expiresIn = try TypraRuntime.int64(value, field: "expiresIn") + } + if let value = object["interval"] { + instance.interval = try TypraRuntime.int64(value, field: "interval") + } + if let value = object["message"] { + instance.message = try TypraRuntime.string(value, field: "message") + } + else { + instance.message = "" + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["deviceCode"] = self.deviceCode + result["userCode"] = self.userCode + result["verificationUri"] = self.verificationUri + result["expiresIn"] = self.expiresIn + result["interval"] = self.interval + result["message"] = self.message + return result + } + + public func toWire(_ provider: String, context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + let wireNameDeviceCode: String? + switch provider { + case "foundry": wireNameDeviceCode = "device_code" + default: wireNameDeviceCode = nil + } + if let wireKey = wireNameDeviceCode { result[wireKey] = self.deviceCode } + let wireNameUserCode: String? + switch provider { + case "foundry": wireNameUserCode = "user_code" + default: wireNameUserCode = nil + } + if let wireKey = wireNameUserCode { result[wireKey] = self.userCode } + let wireNameVerificationUri: String? + switch provider { + case "foundry": wireNameVerificationUri = "verification_uri" + default: wireNameVerificationUri = nil + } + if let wireKey = wireNameVerificationUri { result[wireKey] = self.verificationUri } + let wireNameExpiresIn: String? + switch provider { + case "foundry": wireNameExpiresIn = "expires_in" + default: wireNameExpiresIn = nil + } + if let wireKey = wireNameExpiresIn { result[wireKey] = self.expiresIn } + let wireNameInterval: String? + switch provider { + case "foundry": wireNameInterval = "interval" + default: wireNameInterval = nil + } + if let wireKey = wireNameInterval { result[wireKey] = self.interval } + let wireNameMessage: String? + switch provider { + case "foundry": wireNameMessage = "message" + default: wireNameMessage = nil + } + if let wireKey = wireNameMessage { result[wireKey] = self.message } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> DeviceAuthorization { + return try load(TypraRuntime.jsonObject(from: json, typeName: "DeviceAuthorization"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> DeviceAuthorization { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "DeviceAuthorization"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Connectivity/o_auth_token.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Connectivity/o_auth_token.swift new file mode 100644 index 000000000..36b37f6bc --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Connectivity/o_auth_token.swift @@ -0,0 +1,108 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Provider-neutral OAuth token material returned by an authorization server. +public struct OAuthToken: TypraModel { + public static let shorthandProperty: String? = nil + public var accessToken: String = "" + public var tokenType: String = "" + public var expiresIn: Int64 = 0 + public var refreshToken: String? = nil + public var scope: String? = nil + + public init(accessToken: String = "", tokenType: String = "", expiresIn: Int64 = 0, refreshToken: String? = nil, scope: String? = nil) { + self.accessToken = accessToken + self.tokenType = tokenType + self.expiresIn = expiresIn + self.refreshToken = refreshToken + self.scope = scope + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> OAuthToken { + let object = try TypraRuntime.object(data, typeName: "OAuthToken") + var instance = OAuthToken() + if let value = object["accessToken"] { + instance.accessToken = try TypraRuntime.string(value, field: "accessToken") + } + if let value = object["tokenType"] { + instance.tokenType = try TypraRuntime.string(value, field: "tokenType") + } + if let value = object["expiresIn"] { + instance.expiresIn = try TypraRuntime.int64(value, field: "expiresIn") + } + if let value = object["refreshToken"] { + instance.refreshToken = try TypraRuntime.string(value, field: "refreshToken") + } + if let value = object["scope"] { + instance.scope = try TypraRuntime.string(value, field: "scope") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["accessToken"] = self.accessToken + result["tokenType"] = self.tokenType + result["expiresIn"] = self.expiresIn + if let value = self.refreshToken { + result["refreshToken"] = value + } + if let value = self.scope { + result["scope"] = value + } + return result + } + + public func toWire(_ provider: String, context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + let wireNameAccessToken: String? + switch provider { + case "foundry": wireNameAccessToken = "access_token" + default: wireNameAccessToken = nil + } + if let wireKey = wireNameAccessToken { result[wireKey] = self.accessToken } + let wireNameTokenType: String? + switch provider { + case "foundry": wireNameTokenType = "token_type" + default: wireNameTokenType = nil + } + if let wireKey = wireNameTokenType { result[wireKey] = self.tokenType } + let wireNameExpiresIn: String? + switch provider { + case "foundry": wireNameExpiresIn = "expires_in" + default: wireNameExpiresIn = nil + } + if let wireKey = wireNameExpiresIn { result[wireKey] = self.expiresIn } + let wireNameRefreshToken: String? + switch provider { + case "foundry": wireNameRefreshToken = "refresh_token" + default: wireNameRefreshToken = nil + } + if let wireKey = wireNameRefreshToken, let value = self.refreshToken { result[wireKey] = value } + let wireNameScope: String? + switch provider { + case "foundry": wireNameScope = "scope" + default: wireNameScope = nil + } + if let wireKey = wireNameScope, let value = self.scope { result[wireKey] = value } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> OAuthToken { + return try load(TypraRuntime.jsonObject(from: json, typeName: "OAuthToken"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> OAuthToken { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "OAuthToken"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Conversation/content_part.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Conversation/content_part.swift new file mode 100644 index 000000000..cc9b07470 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Conversation/content_part.swift @@ -0,0 +1,284 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum ContentPart: TypraModel { + case textPart(TextPart) + case imagePart(ImagePart) + case filePart(FilePart) + case audioPart(AudioPart) + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ContentPart { + let normalizedData: Any = data + let object = try TypraRuntime.object(normalizedData, typeName: "ContentPart") + let discriminator = try TypraRuntime.string(object["kind"] ?? NSNull(), field: "kind") + if discriminator.isEmpty { + throw TypraRuntimeError.invalidField(field: context.at("kind").path, expected: "non-blank string") + } + switch discriminator { + case "text": return .textPart(try TextPart.load(normalizedData, context: context)) + case "image": return .imagePart(try ImagePart.load(normalizedData, context: context)) + case "file": return .filePart(try FilePart.load(normalizedData, context: context)) + case "audio": return .audioPart(try AudioPart.load(normalizedData, context: context)) + default: throw TypraRuntimeError.unknownDiscriminator(type: "ContentPart", field: "kind", value: discriminator) + } + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + switch self { + case .textPart(let value): return try value.save(context) + case .imagePart(let value): return try value.save(context) + case .filePart(let value): return try value.save(context) + case .audioPart(let value): return try value.save(context) + } + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ContentPart { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ContentPart"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ContentPart { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ContentPart"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +/// A text content part. +public struct TextPart: TypraModel { + public static let shorthandProperty: String? = nil + public var kind: String = "text" + public var value: String = "" + + public init(kind: String = "text", value: String = "") { + self.kind = kind + self.value = value + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> TextPart { + let object = try TypraRuntime.object(data, typeName: "TextPart") + var instance = TextPart() + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "text" + } + if let value = object["value"] { + instance.value = try TypraRuntime.string(value, field: "value") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["kind"] = self.kind + result["value"] = self.value + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> TextPart { + return try load(TypraRuntime.jsonObject(from: json, typeName: "TextPart"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> TextPart { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "TextPart"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +/// An image content part. The source may be a URL or base64-encoded data. +public struct ImagePart: TypraModel { + public static let shorthandProperty: String? = nil + public var kind: String = "image" + public var source: String = "" + public var detail: String? = nil + public var mediaType: String? = nil + + public init(kind: String = "image", source: String = "", detail: String? = nil, mediaType: String? = nil) { + self.kind = kind + self.source = source + self.detail = detail + self.mediaType = mediaType + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ImagePart { + let object = try TypraRuntime.object(data, typeName: "ImagePart") + var instance = ImagePart() + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "image" + } + if let value = object["source"] { + instance.source = try TypraRuntime.string(value, field: "source") + } + if let value = object["detail"] { + instance.detail = try TypraRuntime.string(value, field: "detail") + } + if let value = object["mediaType"] { + instance.mediaType = try TypraRuntime.string(value, field: "mediaType") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["kind"] = self.kind + result["source"] = self.source + if let value = self.detail { + result["detail"] = value + } + if let value = self.mediaType { + result["mediaType"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ImagePart { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ImagePart"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ImagePart { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ImagePart"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +/// A file content part. The source may be a URL or base64-encoded data. +public struct FilePart: TypraModel { + public static let shorthandProperty: String? = nil + public var kind: String = "file" + public var source: String = "" + public var mediaType: String? = nil + + public init(kind: String = "file", source: String = "", mediaType: String? = nil) { + self.kind = kind + self.source = source + self.mediaType = mediaType + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> FilePart { + let object = try TypraRuntime.object(data, typeName: "FilePart") + var instance = FilePart() + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "file" + } + if let value = object["source"] { + instance.source = try TypraRuntime.string(value, field: "source") + } + if let value = object["mediaType"] { + instance.mediaType = try TypraRuntime.string(value, field: "mediaType") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["kind"] = self.kind + result["source"] = self.source + if let value = self.mediaType { + result["mediaType"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> FilePart { + return try load(TypraRuntime.jsonObject(from: json, typeName: "FilePart"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> FilePart { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "FilePart"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +/// An audio content part. The source may be a URL or base64-encoded data. +public struct AudioPart: TypraModel { + public static let shorthandProperty: String? = nil + public var kind: String = "audio" + public var source: String = "" + public var mediaType: String? = nil + + public init(kind: String = "audio", source: String = "", mediaType: String? = nil) { + self.kind = kind + self.source = source + self.mediaType = mediaType + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> AudioPart { + let object = try TypraRuntime.object(data, typeName: "AudioPart") + var instance = AudioPart() + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "audio" + } + if let value = object["source"] { + instance.source = try TypraRuntime.string(value, field: "source") + } + if let value = object["mediaType"] { + instance.mediaType = try TypraRuntime.string(value, field: "mediaType") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["kind"] = self.kind + result["source"] = self.source + if let value = self.mediaType { + result["mediaType"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> AudioPart { + return try load(TypraRuntime.jsonObject(from: json, typeName: "AudioPart"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> AudioPart { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "AudioPart"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Conversation/message.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Conversation/message.swift new file mode 100644 index 000000000..b13b5c583 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Conversation/message.swift @@ -0,0 +1,98 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum Role: String, Codable, CaseIterable { + case system = "system" + case user = "user" + case assistant = "assistant" + case developer = "developer" + case tool = "tool" + public static func parse(_ value: String) throws -> Role { + switch value { + case "system": return .system + case "user": return .user + case "assistant": return .assistant + case "developer": return .developer + case "tool": return .tool + default: throw TypraRuntimeError.invalidEnum(type: "Role", value: value) + } + } +} + +/// A message in a conversation. Messages have a role and a list of content parts representing the different modalities of the message content. +public struct Message: TypraModel { + public static let shorthandProperty: String? = nil + public var role: Role = (try! Role.parse("user")) + public var parts: [ContentPart] = [] + public var metadata: [String: Any] = [:] + + public init(role: Role = (try! Role.parse("user")), parts: [ContentPart] = [], metadata: [String: Any] = [:]) { + self.role = role + self.parts = parts + self.metadata = metadata + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> Message { + let object = try TypraRuntime.object(data, typeName: "Message") + var instance = Message() + if let value = object["role"] { + instance.role = try Role.parse(try TypraRuntime.string(value, field: "role")) + } + else { + instance.role = (try! Role.parse("user")) + } + if let value = object["parts"] { + instance.parts = try TypraRuntime.array(value, field: "parts").enumerated().map { try ContentPart.load($1, context: context.at("parts").atIndex($0)) } + } + if let value = object["metadata"] { + instance.metadata = try TypraRuntime.dictionary(value, field: "metadata") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["role"] = self.role.rawValue + result["parts"] = try self.parts.map { try $0.save(context) } + result["metadata"] = self.metadata + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> Message { + return try load(TypraRuntime.jsonObject(from: json, typeName: "Message"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> Message { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "Message"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } + + public static func assistant(text: String) -> Message { + return Message(role: .assistant, parts: [.textPart(TextPart(kind: "text", value: text))]) + } + + public static func system(text: String) -> Message { + return Message(role: .system, parts: [.textPart(TextPart(kind: "text", value: text))]) + } + + public static func user(text: String) -> Message { + return Message(role: .user, parts: [.textPart(TextPart(kind: "text", value: text))]) + } + + public func toTextContent() async throws -> Any { + throw TypraRuntimeError.unsupported("toTextContent must be implemented by hand-authored code.") + } + + public func text() async throws -> String { + throw TypraRuntimeError.unsupported("text must be implemented by hand-authored code.") + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Conversation/thread_marker.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Conversation/thread_marker.swift new file mode 100644 index 000000000..0bc5aade3 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Conversation/thread_marker.swift @@ -0,0 +1,57 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Positional marker for conversation history insertion during template rendering. During `prepare()`, nonce strings in rendered text are replaced with ThreadMarker objects. The pipeline then replaces them with actual conversation messages from the inputs. +public struct ThreadMarker: TypraModel { + public static let shorthandProperty: String? = nil + public var name: String = "thread" + public var kind: String = "thread" + + public init(name: String = "thread", kind: String = "thread") { + self.name = name + self.kind = kind + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ThreadMarker { + let object = try TypraRuntime.object(data, typeName: "ThreadMarker") + var instance = ThreadMarker() + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + else { + instance.name = "thread" + } + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "thread" + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["name"] = self.name + result["kind"] = self.kind + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ThreadMarker { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ThreadMarker"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ThreadMarker { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ThreadMarker"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Conversation/tool_call.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Conversation/tool_call.swift new file mode 100644 index 000000000..eb5ca2313 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Conversation/tool_call.swift @@ -0,0 +1,57 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// A tool call requested by the LLM. Contains the function name and serialized arguments that should be dispatched to the appropriate tool handler. +public struct ToolCall: TypraModel { + public static let shorthandProperty: String? = nil + public var id: String = "" + public var name: String = "" + public var arguments: String = "" + + public init(id: String = "", name: String = "", arguments: String = "") { + self.id = id + self.name = name + self.arguments = arguments + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ToolCall { + let object = try TypraRuntime.object(data, typeName: "ToolCall") + var instance = ToolCall() + if let value = object["id"] { + instance.id = try TypraRuntime.string(value, field: "id") + } + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + if let value = object["arguments"] { + instance.arguments = try TypraRuntime.string(value, field: "arguments") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["id"] = self.id + result["name"] = self.name + result["arguments"] = self.arguments + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ToolCall { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ToolCall"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ToolCall { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ToolCall"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Conversation/tool_result.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Conversation/tool_result.swift new file mode 100644 index 000000000..51f7cc9ad --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Conversation/tool_result.swift @@ -0,0 +1,101 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum ToolResultStatus: String, Codable, CaseIterable { + case success = "success" + case error = "error" + case cancelled = "cancelled" + case timeout = "timeout" + public static func parse(_ value: String) throws -> ToolResultStatus { + switch value { + case "success": return .success + case "error": return .error + case "cancelled": return .cancelled + case "timeout": return .timeout + default: throw TypraRuntimeError.invalidEnum(type: "ToolResultStatus", value: value) + } + } +} + +/// The result of a tool execution. Contains a list of content parts, enabling rich tool results (text, images, files, audio) rather than just strings. Implementations MUST support conversion from a plain string to a ToolResult containing a single TextPart for backward compatibility. +public struct ToolResult: TypraModel { + public static let shorthandProperty: String? = nil + public var parts: [ContentPart] = [] + public var status: ToolResultStatus? = nil + public var errorKind: String? = nil + public var errorMessage: String? = nil + public var durationMs: Double? = nil + + public init(parts: [ContentPart] = [], status: ToolResultStatus? = nil, errorKind: String? = nil, errorMessage: String? = nil, durationMs: Double? = nil) { + self.parts = parts + self.status = status + self.errorKind = errorKind + self.errorMessage = errorMessage + self.durationMs = durationMs + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ToolResult { + let object = try TypraRuntime.object(data, typeName: "ToolResult") + var instance = ToolResult() + if let value = object["parts"] { + instance.parts = try TypraRuntime.array(value, field: "parts").enumerated().map { try ContentPart.load($1, context: context.at("parts").atIndex($0)) } + } + if let value = object["status"] { + instance.status = try ToolResultStatus.parse(try TypraRuntime.string(value, field: "status")) + } + if let value = object["errorKind"] { + instance.errorKind = try TypraRuntime.string(value, field: "errorKind") + } + if let value = object["errorMessage"] { + instance.errorMessage = try TypraRuntime.string(value, field: "errorMessage") + } + if let value = object["durationMs"] { + instance.durationMs = try TypraRuntime.double(value, field: "durationMs") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["parts"] = try self.parts.map { try $0.save(context) } + if let value = self.status { + result["status"] = value.rawValue + } + if let value = self.errorKind { + result["errorKind"] = value + } + if let value = self.errorMessage { + result["errorMessage"] = value + } + if let value = self.durationMs { + result["durationMs"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ToolResult { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ToolResult"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ToolResult { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ToolResult"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } + + public static func text(value: String) -> ToolResult { + return ToolResult(parts: [.textPart(TextPart(kind: "text", value: value))]) + } + + public func text() async throws -> String { + throw TypraRuntimeError.unsupported("text must be implemented by hand-authored code.") + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Core/file_not_found_error.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Core/file_not_found_error.swift new file mode 100644 index 000000000..935ff0800 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Core/file_not_found_error.swift @@ -0,0 +1,51 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Raised when a referenced file cannot be found. This applies to both .prompty files and ${file:path} references in frontmatter. +public struct FileNotFoundError: TypraModel { + public static let shorthandProperty: String? = nil + public var message: String = "" + public var path: String = "" + + public init(message: String = "", path: String = "") { + self.message = message + self.path = path + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> FileNotFoundError { + let object = try TypraRuntime.object(data, typeName: "FileNotFoundError") + var instance = FileNotFoundError() + if let value = object["message"] { + instance.message = try TypraRuntime.string(value, field: "message") + } + if let value = object["path"] { + instance.path = try TypraRuntime.string(value, field: "path") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["message"] = self.message + result["path"] = self.path + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> FileNotFoundError { + return try load(TypraRuntime.jsonObject(from: json, typeName: "FileNotFoundError"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> FileNotFoundError { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "FileNotFoundError"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Core/invoker_error.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Core/invoker_error.swift new file mode 100644 index 000000000..d4ca24196 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Core/invoker_error.swift @@ -0,0 +1,57 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Raised when no invoker implementation is registered for a given component and key. For example, if no renderer is registered for the key "jinja2", an InvokerError is raised. +public struct InvokerError: TypraModel { + public static let shorthandProperty: String? = nil + public var message: String = "" + public var component: String = "" + public var key: String = "" + + public init(message: String = "", component: String = "", key: String = "") { + self.message = message + self.component = component + self.key = key + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> InvokerError { + let object = try TypraRuntime.object(data, typeName: "InvokerError") + var instance = InvokerError() + if let value = object["message"] { + instance.message = try TypraRuntime.string(value, field: "message") + } + if let value = object["component"] { + instance.component = try TypraRuntime.string(value, field: "component") + } + if let value = object["key"] { + instance.key = try TypraRuntime.string(value, field: "key") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["message"] = self.message + result["component"] = self.component + result["key"] = self.key + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> InvokerError { + return try load(TypraRuntime.jsonObject(from: json, typeName: "InvokerError"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> InvokerError { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "InvokerError"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Core/property.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Core/property.swift new file mode 100644 index 000000000..7218caeeb --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Core/property.swift @@ -0,0 +1,438 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public indirect enum Property: TypraModel { + case arrayProperty(ArrayProperty) + case objectProperty(ObjectProperty) + case unionProperty(UnionProperty) + case unknown([String: Any]) + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> Property { + var normalizedData: Any = data + if let scalar = normalizedData as? Bool { + normalizedData = ["kind": "boolean", "example": scalar] + } + if let scalar = normalizedData as? Double { + normalizedData = ["kind": "float", "example": scalar] + } + if let scalar = normalizedData as? Int { + normalizedData = ["kind": "integer", "example": scalar] + } + if let scalar = normalizedData as? String { + normalizedData = ["kind": "string", "example": scalar] + } + let object = try TypraRuntime.object(normalizedData, typeName: "Property") + let discriminator = try TypraRuntime.string(object["kind"] ?? NSNull(), field: "kind") + if discriminator.isEmpty { + throw TypraRuntimeError.invalidField(field: context.at("kind").path, expected: "non-blank string") + } + switch discriminator { + case "array": return .arrayProperty(try ArrayProperty.load(normalizedData, context: context)) + case "object": return .objectProperty(try ObjectProperty.load(normalizedData, context: context)) + case "union": return .unionProperty(try UnionProperty.load(normalizedData, context: context)) + default: return .unknown(object) + } + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + switch self { + case .arrayProperty(let value): return try value.save(context) + case .objectProperty(let value): return try value.save(context) + case .unionProperty(let value): return try value.save(context) + case .unknown(let value): return value + } + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> Property { + return try load(TypraRuntime.jsonObject(from: json, typeName: "Property"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> Property { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "Property"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +/// Represents an array property. This extends the base Property model to represent an array of items. +public struct ArrayProperty: TypraModel { + public static let shorthandProperty: String? = nil + public var name: String = "" + public var kind: String = "array" + public var description: String? = nil + public var `required`: Bool? = nil + public var nullable: Bool? = nil + public var `default`: Any? = nil + public var example: Any? = nil + public var enumValues: [Any]? = nil + public var items: Property = .unknown([:]) + + public init(name: String = "", kind: String = "array", description: String? = nil, `required`: Bool? = nil, nullable: Bool? = nil, `default`: Any? = nil, example: Any? = nil, enumValues: [Any]? = nil, items: Property = .unknown([:])) { + self.name = name + self.kind = kind + self.description = description + self.`required` = `required` + self.nullable = nullable + self.`default` = `default` + self.example = example + self.enumValues = enumValues + self.items = items + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ArrayProperty { + let object = try TypraRuntime.object(data, typeName: "ArrayProperty") + var instance = ArrayProperty() + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + else { + instance.name = "" + } + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "array" + } + if let value = object["description"] { + instance.description = try TypraRuntime.string(value, field: "description") + } + if let value = object["required"] { + instance.`required` = try TypraRuntime.bool(value, field: "required") + } + if let value = object["nullable"] { + instance.nullable = try TypraRuntime.bool(value, field: "nullable") + } + if let value = object["default"] { + instance.`default` = value + } + if let value = object["example"] { + instance.example = value + } + if let value = object["enumValues"] { + instance.enumValues = try TypraRuntime.array(value, field: "enumValues").map { $0 } + } + if object["items"] == nil || object["items"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("items").path + ": missing required field") + } + if let value = object["items"] { + instance.items = try Property.load(value, context: context.at("items")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["name"] = self.name + result["kind"] = self.kind + if let value = self.description { + result["description"] = value + } + if let value = self.`required` { + result["required"] = value + } + if let value = self.nullable { + result["nullable"] = value + } + if let value = self.`default` { + result["default"] = value + } + if let value = self.example { + result["example"] = value + } + if let value = self.enumValues { + result["enumValues"] = value + } + result["items"] = try self.items.save(context) + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ArrayProperty { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ArrayProperty"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ArrayProperty { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ArrayProperty"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +/// Represents an object property. This extends the base Property model to represent a structured object. +public struct ObjectProperty: TypraModel { + public static let shorthandProperty: String? = nil + public var name: String = "" + public var kind: String = "object" + public var description: String? = nil + public var `required`: Bool? = nil + public var nullable: Bool? = nil + public var `default`: Any? = nil + public var example: Any? = nil + public var enumValues: [Any]? = nil + public var properties: [Property] = [] + + public init(name: String = "", kind: String = "object", description: String? = nil, `required`: Bool? = nil, nullable: Bool? = nil, `default`: Any? = nil, example: Any? = nil, enumValues: [Any]? = nil, properties: [Property] = []) { + self.name = name + self.kind = kind + self.description = description + self.`required` = `required` + self.nullable = nullable + self.`default` = `default` + self.example = example + self.enumValues = enumValues + self.properties = properties + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ObjectProperty { + let object = try TypraRuntime.object(data, typeName: "ObjectProperty") + var instance = ObjectProperty() + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + else { + instance.name = "" + } + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "object" + } + if let value = object["description"] { + instance.description = try TypraRuntime.string(value, field: "description") + } + if let value = object["required"] { + instance.`required` = try TypraRuntime.bool(value, field: "required") + } + if let value = object["nullable"] { + instance.nullable = try TypraRuntime.bool(value, field: "nullable") + } + if let value = object["default"] { + instance.`default` = value + } + if let value = object["example"] { + instance.example = value + } + if let value = object["enumValues"] { + instance.enumValues = try TypraRuntime.array(value, field: "enumValues").map { $0 } + } + if let value = object["properties"] { + instance.properties = try loadProperties(value, context: context.at("properties")) + } + return instance + } + + private static func loadProperties(_ data: Any, context: LoadContext) throws -> [Property] { + if let values = data as? [Any] { + return try values.enumerated().map { try Property.load($1, context: context.atIndex($0)) } + } + let values = try TypraRuntime.dictionary(data, field: "properties") + return try values.keys.sorted().map { name in + let value = values[name]! + if value is [Any] { + throw TypraRuntimeError.invalidField(field: context.at(name).path, expected: "non-array named collection entry; received category array") + } + var itemData: [String: Any] + if let object = value as? [String: Any] { + itemData = object + } else { + itemData = try Property.load(value, context: context).save() + } + itemData["name"] = name + return try Property.load(itemData, context: context.at(name)) + } + } + + private static func saveProperties(_ items: [Property], context: SaveContext) throws -> Any { + let serialized = try items.map { try $0.save(context) } + if context.collectionFormat == "array" { + return serialized + } + var names = Set() + for itemData in serialized { + guard let name = itemData["name"] as? String, !name.isEmpty, names.insert(name).inserted else { + return serialized + } + } + var result: [String: Any] = [:] + for (_, originalData) in zip(items, serialized) { + var itemData = originalData + let name = itemData.removeValue(forKey: "name") as! String + result[name] = itemData + } + return result + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["name"] = self.name + result["kind"] = self.kind + if let value = self.description { + result["description"] = value + } + if let value = self.`required` { + result["required"] = value + } + if let value = self.nullable { + result["nullable"] = value + } + if let value = self.`default` { + result["default"] = value + } + if let value = self.example { + result["example"] = value + } + if let value = self.enumValues { + result["enumValues"] = value + } + result["properties"] = try Self.saveProperties(self.properties, context: context) + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ObjectProperty { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ObjectProperty"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ObjectProperty { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ObjectProperty"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +/// Represents a JSON Schema union property. Use `oneOf` when exactly one branch must match, or `anyOf` when one or more branches may match. Exactly one composition field MUST be provided with at least one branch; `oneOf` and `anyOf` MUST NOT both be populated. The alternatives are full Prompty properties so unions remain portable across generated runtimes. +public struct UnionProperty: TypraModel { + public static let shorthandProperty: String? = nil + public var name: String = "" + public var kind: String = "union" + public var description: String? = nil + public var `required`: Bool? = nil + public var nullable: Bool? = nil + public var `default`: Any? = nil + public var example: Any? = nil + public var enumValues: [Any]? = nil + public var oneOf: [Property]? = nil + public var anyOf: [Property]? = nil + + public init(name: String = "", kind: String = "union", description: String? = nil, `required`: Bool? = nil, nullable: Bool? = nil, `default`: Any? = nil, example: Any? = nil, enumValues: [Any]? = nil, oneOf: [Property]? = nil, anyOf: [Property]? = nil) { + self.name = name + self.kind = kind + self.description = description + self.`required` = `required` + self.nullable = nullable + self.`default` = `default` + self.example = example + self.enumValues = enumValues + self.oneOf = oneOf + self.anyOf = anyOf + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> UnionProperty { + let object = try TypraRuntime.object(data, typeName: "UnionProperty") + var instance = UnionProperty() + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + else { + instance.name = "" + } + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "union" + } + if let value = object["description"] { + instance.description = try TypraRuntime.string(value, field: "description") + } + if let value = object["required"] { + instance.`required` = try TypraRuntime.bool(value, field: "required") + } + if let value = object["nullable"] { + instance.nullable = try TypraRuntime.bool(value, field: "nullable") + } + if let value = object["default"] { + instance.`default` = value + } + if let value = object["example"] { + instance.example = value + } + if let value = object["enumValues"] { + instance.enumValues = try TypraRuntime.array(value, field: "enumValues").map { $0 } + } + if let value = object["oneOf"] { + instance.oneOf = try TypraRuntime.array(value, field: "oneOf").enumerated().map { try Property.load($1, context: context.at("oneOf").atIndex($0)) } + } + if let value = object["anyOf"] { + instance.anyOf = try TypraRuntime.array(value, field: "anyOf").enumerated().map { try Property.load($1, context: context.at("anyOf").atIndex($0)) } + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["name"] = self.name + result["kind"] = self.kind + if let value = self.description { + result["description"] = value + } + if let value = self.`required` { + result["required"] = value + } + if let value = self.nullable { + result["nullable"] = value + } + if let value = self.`default` { + result["default"] = value + } + if let value = self.example { + result["example"] = value + } + if let value = self.enumValues { + result["enumValues"] = value + } + if let value = self.oneOf { + result["oneOf"] = try value.map { try $0.save(context) } + } + if let value = self.anyOf { + result["anyOf"] = try value.map { try $0.save(context) } + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> UnionProperty { + return try load(TypraRuntime.jsonObject(from: json, typeName: "UnionProperty"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> UnionProperty { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "UnionProperty"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Core/validation_error.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Core/validation_error.swift new file mode 100644 index 000000000..a952ee972 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Core/validation_error.swift @@ -0,0 +1,57 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Raised when input validation fails. Each ValidationError describes a single property that did not satisfy its constraint. +public struct ValidationError: TypraModel { + public static let shorthandProperty: String? = nil + public var message: String = "" + public var property: String = "" + public var constraint: String = "" + + public init(message: String = "", property: String = "", constraint: String = "") { + self.message = message + self.property = property + self.constraint = constraint + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ValidationError { + let object = try TypraRuntime.object(data, typeName: "ValidationError") + var instance = ValidationError() + if let value = object["message"] { + instance.message = try TypraRuntime.string(value, field: "message") + } + if let value = object["property"] { + instance.property = try TypraRuntime.string(value, field: "property") + } + if let value = object["constraint"] { + instance.constraint = try TypraRuntime.string(value, field: "constraint") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["message"] = self.message + result["property"] = self.property + result["constraint"] = self.constraint + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ValidationError { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ValidationError"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ValidationError { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ValidationError"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Core/validation_result.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Core/validation_result.swift new file mode 100644 index 000000000..439eb8d1f --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Core/validation_result.swift @@ -0,0 +1,51 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// The result of validating inputs against a Prompty's inputs. Returned by `validate_inputs` (§12.2) to indicate whether all required inputs are present and satisfy their constraints. +public struct ValidationResult: TypraModel { + public static let shorthandProperty: String? = nil + public var valid: Bool = false + public var errors: [ValidationError] = [] + + public init(valid: Bool = false, errors: [ValidationError] = []) { + self.valid = valid + self.errors = errors + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ValidationResult { + let object = try TypraRuntime.object(data, typeName: "ValidationResult") + var instance = ValidationResult() + if let value = object["valid"] { + instance.valid = try TypraRuntime.bool(value, field: "valid") + } + if let value = object["errors"] { + instance.errors = try TypraRuntime.array(value, field: "errors").enumerated().map { try ValidationError.load($1, context: context.at("errors").atIndex($0)) } + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["valid"] = self.valid + result["errors"] = try self.errors.map { try $0.save(context) } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ValidationResult { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ValidationResult"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ValidationResult { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ValidationResult"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/checkpoint.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/checkpoint.swift new file mode 100644 index 000000000..3f63e0a49 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/checkpoint.swift @@ -0,0 +1,125 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// A persisted handoff point for a harness session. +public struct Checkpoint: TypraModel { + public static let shorthandProperty: String? = nil + public var id: String? = nil + public var sessionId: String? = nil + public var turnId: String? = nil + public var checkpointNumber: Int32? = nil + public var title: String = "" + public var overview: String? = nil + public var state: [String: Any]? = nil + public var summary: String? = nil + public var metadata: [String: Any]? = nil + public var createdAt: String? = nil + public var redaction: RedactionMetadata? = nil + + public init(id: String? = nil, sessionId: String? = nil, turnId: String? = nil, checkpointNumber: Int32? = nil, title: String = "", overview: String? = nil, state: [String: Any]? = nil, summary: String? = nil, metadata: [String: Any]? = nil, createdAt: String? = nil, redaction: RedactionMetadata? = nil) { + self.id = id + self.sessionId = sessionId + self.turnId = turnId + self.checkpointNumber = checkpointNumber + self.title = title + self.overview = overview + self.state = state + self.summary = summary + self.metadata = metadata + self.createdAt = createdAt + self.redaction = redaction + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> Checkpoint { + let object = try TypraRuntime.object(data, typeName: "Checkpoint") + var instance = Checkpoint() + if let value = object["id"] { + instance.id = try TypraRuntime.string(value, field: "id") + } + if let value = object["sessionId"] { + instance.sessionId = try TypraRuntime.string(value, field: "sessionId") + } + if let value = object["turnId"] { + instance.turnId = try TypraRuntime.string(value, field: "turnId") + } + if let value = object["checkpointNumber"] { + instance.checkpointNumber = try TypraRuntime.int32(value, field: "checkpointNumber") + } + if let value = object["title"] { + instance.title = try TypraRuntime.string(value, field: "title") + } + if let value = object["overview"] { + instance.overview = try TypraRuntime.string(value, field: "overview") + } + if let value = object["state"] { + instance.state = try TypraRuntime.dictionary(value, field: "state") + } + if let value = object["summary"] { + instance.summary = try TypraRuntime.string(value, field: "summary") + } + if let value = object["metadata"] { + instance.metadata = try TypraRuntime.dictionary(value, field: "metadata") + } + if let value = object["createdAt"] { + instance.createdAt = try TypraRuntime.string(value, field: "createdAt") + } + if let value = object["redaction"] { + instance.redaction = try RedactionMetadata.load(value, context: context.at("redaction")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.id { + result["id"] = value + } + if let value = self.sessionId { + result["sessionId"] = value + } + if let value = self.turnId { + result["turnId"] = value + } + if let value = self.checkpointNumber { + result["checkpointNumber"] = value + } + result["title"] = self.title + if let value = self.overview { + result["overview"] = value + } + if let value = self.state { + result["state"] = value + } + if let value = self.summary { + result["summary"] = value + } + if let value = self.metadata { + result["metadata"] = value + } + if let value = self.createdAt { + result["createdAt"] = value + } + if let value = self.redaction { + result["redaction"] = try value.save(context) + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> Checkpoint { + return try load(TypraRuntime.jsonObject(from: json, typeName: "Checkpoint"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> Checkpoint { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "Checkpoint"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/compaction_complete_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/compaction_complete_payload.swift new file mode 100644 index 000000000..f42c13e44 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/compaction_complete_payload.swift @@ -0,0 +1,59 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for "compaction_complete" events — context compaction finished. +public struct CompactionCompletePayload: TypraModel { + public static let shorthandProperty: String? = nil + public var removed: Int32 = 0 + public var remaining: Int32 = 0 + public var summaryLength: Int32? = nil + + public init(removed: Int32 = 0, remaining: Int32 = 0, summaryLength: Int32? = nil) { + self.removed = removed + self.remaining = remaining + self.summaryLength = summaryLength + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> CompactionCompletePayload { + let object = try TypraRuntime.object(data, typeName: "CompactionCompletePayload") + var instance = CompactionCompletePayload() + if let value = object["removed"] { + instance.removed = try TypraRuntime.int32(value, field: "removed") + } + if let value = object["remaining"] { + instance.remaining = try TypraRuntime.int32(value, field: "remaining") + } + if let value = object["summaryLength"] { + instance.summaryLength = try TypraRuntime.int32(value, field: "summaryLength") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["removed"] = self.removed + result["remaining"] = self.remaining + if let value = self.summaryLength { + result["summaryLength"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> CompactionCompletePayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "CompactionCompletePayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> CompactionCompletePayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "CompactionCompletePayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/compaction_failed_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/compaction_failed_payload.swift new file mode 100644 index 000000000..209c35d70 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/compaction_failed_payload.swift @@ -0,0 +1,45 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for "compaction_failed" events — compaction could not be completed. +public struct CompactionFailedPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var message: String = "" + + public init(message: String = "") { + self.message = message + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> CompactionFailedPayload { + let object = try TypraRuntime.object(data, typeName: "CompactionFailedPayload") + var instance = CompactionFailedPayload() + if let value = object["message"] { + instance.message = try TypraRuntime.string(value, field: "message") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["message"] = self.message + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> CompactionFailedPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "CompactionFailedPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> CompactionFailedPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "CompactionFailedPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/compaction_start_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/compaction_start_payload.swift new file mode 100644 index 000000000..4a297e498 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/compaction_start_payload.swift @@ -0,0 +1,45 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for "compaction_start" events — context compaction is beginning. +public struct CompactionStartPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var droppedCount: Int32 = 0 + + public init(droppedCount: Int32 = 0) { + self.droppedCount = droppedCount + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> CompactionStartPayload { + let object = try TypraRuntime.object(data, typeName: "CompactionStartPayload") + var instance = CompactionStartPayload() + if let value = object["droppedCount"] { + instance.droppedCount = try TypraRuntime.int32(value, field: "droppedCount") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["droppedCount"] = self.droppedCount + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> CompactionStartPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "CompactionStartPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> CompactionStartPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "CompactionStartPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/done_event_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/done_event_payload.swift new file mode 100644 index 000000000..77b572bcb --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/done_event_payload.swift @@ -0,0 +1,51 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for "done" events — the agent loop completed successfully. +public struct DoneEventPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var response: Any = NSNull() + public var messages: [Message] = [] + + public init(response: Any = NSNull(), messages: [Message] = []) { + self.response = response + self.messages = messages + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> DoneEventPayload { + let object = try TypraRuntime.object(data, typeName: "DoneEventPayload") + var instance = DoneEventPayload() + if let value = object["response"] { + instance.response = value + } + if let value = object["messages"] { + instance.messages = try TypraRuntime.array(value, field: "messages").enumerated().map { try Message.load($1, context: context.at("messages").atIndex($0)) } + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["response"] = self.response + result["messages"] = try self.messages.map { try $0.save(context) } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> DoneEventPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "DoneEventPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> DoneEventPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "DoneEventPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/error_event_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/error_event_payload.swift new file mode 100644 index 000000000..8f2f98830 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/error_event_payload.swift @@ -0,0 +1,61 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for "error" events — an error occurred during the loop. +public struct ErrorEventPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var message: String = "" + public var errorKind: String? = nil + public var phase: String? = nil + + public init(message: String = "", errorKind: String? = nil, phase: String? = nil) { + self.message = message + self.errorKind = errorKind + self.phase = phase + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ErrorEventPayload { + let object = try TypraRuntime.object(data, typeName: "ErrorEventPayload") + var instance = ErrorEventPayload() + if let value = object["message"] { + instance.message = try TypraRuntime.string(value, field: "message") + } + if let value = object["errorKind"] { + instance.errorKind = try TypraRuntime.string(value, field: "errorKind") + } + if let value = object["phase"] { + instance.phase = try TypraRuntime.string(value, field: "phase") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["message"] = self.message + if let value = self.errorKind { + result["errorKind"] = value + } + if let value = self.phase { + result["phase"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ErrorEventPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ErrorEventPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ErrorEventPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ErrorEventPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/harness_context.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/harness_context.swift new file mode 100644 index 000000000..b5935137d --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/harness_context.swift @@ -0,0 +1,63 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Execution context associated with a harness session. Host-specific environments can store detailed profiles in metadata without making the core contract depend on one source-control provider or workspace shape. +public struct HarnessContext: TypraModel { + public static let shorthandProperty: String? = nil + public var cwd: String? = nil + public var gitRoot: String? = nil + public var metadata: [String: Any]? = nil + + public init(cwd: String? = nil, gitRoot: String? = nil, metadata: [String: Any]? = nil) { + self.cwd = cwd + self.gitRoot = gitRoot + self.metadata = metadata + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> HarnessContext { + let object = try TypraRuntime.object(data, typeName: "HarnessContext") + var instance = HarnessContext() + if let value = object["cwd"] { + instance.cwd = try TypraRuntime.string(value, field: "cwd") + } + if let value = object["gitRoot"] { + instance.gitRoot = try TypraRuntime.string(value, field: "gitRoot") + } + if let value = object["metadata"] { + instance.metadata = try TypraRuntime.dictionary(value, field: "metadata") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.cwd { + result["cwd"] = value + } + if let value = self.gitRoot { + result["gitRoot"] = value + } + if let value = self.metadata { + result["metadata"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> HarnessContext { + return try load(TypraRuntime.jsonObject(from: json, typeName: "HarnessContext"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> HarnessContext { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "HarnessContext"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/hook_end_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/hook_end_payload.swift new file mode 100644 index 000000000..00eaa8b9a --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/hook_end_payload.swift @@ -0,0 +1,109 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum HookEndScope: String, Codable, CaseIterable { + case turn = "turn" + case session = "session" + public static func parse(_ value: String) throws -> HookEndScope { + switch value { + case "turn": return .turn + case "session": return .session + default: throw TypraRuntimeError.invalidEnum(type: "HookEndScope", value: value) + } + } +} + +/// Payload for "hook_end" events — a host lifecycle hook finished. +public struct HookEndPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var hookInvocationId: String = "" + public var hookType: String = "" + public var scope: HookEndScope? = nil + public var success: Bool = false + public var output: [String: Any]? = nil + public var durationMs: Double? = nil + public var error: String? = nil + public var redaction: RedactionMetadata? = nil + + public init(hookInvocationId: String = "", hookType: String = "", scope: HookEndScope? = nil, success: Bool = false, output: [String: Any]? = nil, durationMs: Double? = nil, error: String? = nil, redaction: RedactionMetadata? = nil) { + self.hookInvocationId = hookInvocationId + self.hookType = hookType + self.scope = scope + self.success = success + self.output = output + self.durationMs = durationMs + self.error = error + self.redaction = redaction + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> HookEndPayload { + let object = try TypraRuntime.object(data, typeName: "HookEndPayload") + var instance = HookEndPayload() + if let value = object["hookInvocationId"] { + instance.hookInvocationId = try TypraRuntime.string(value, field: "hookInvocationId") + } + if let value = object["hookType"] { + instance.hookType = try TypraRuntime.string(value, field: "hookType") + } + if let value = object["scope"] { + instance.scope = try HookEndScope.parse(try TypraRuntime.string(value, field: "scope")) + } + if let value = object["success"] { + instance.success = try TypraRuntime.bool(value, field: "success") + } + if let value = object["output"] { + instance.output = try TypraRuntime.dictionary(value, field: "output") + } + if let value = object["durationMs"] { + instance.durationMs = try TypraRuntime.double(value, field: "durationMs") + } + if let value = object["error"] { + instance.error = try TypraRuntime.string(value, field: "error") + } + if let value = object["redaction"] { + instance.redaction = try RedactionMetadata.load(value, context: context.at("redaction")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["hookInvocationId"] = self.hookInvocationId + result["hookType"] = self.hookType + if let value = self.scope { + result["scope"] = value.rawValue + } + result["success"] = self.success + if let value = self.output { + result["output"] = value + } + if let value = self.durationMs { + result["durationMs"] = value + } + if let value = self.error { + result["error"] = value + } + if let value = self.redaction { + result["redaction"] = try value.save(context) + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> HookEndPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "HookEndPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> HookEndPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "HookEndPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/hook_start_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/hook_start_payload.swift new file mode 100644 index 000000000..f0c6c4a04 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/hook_start_payload.swift @@ -0,0 +1,87 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum HookStartScope: String, Codable, CaseIterable { + case turn = "turn" + case session = "session" + public static func parse(_ value: String) throws -> HookStartScope { + switch value { + case "turn": return .turn + case "session": return .session + default: throw TypraRuntimeError.invalidEnum(type: "HookStartScope", value: value) + } + } +} + +/// Payload for "hook_start" events — a host lifecycle hook is beginning. +public struct HookStartPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var hookInvocationId: String = "" + public var hookType: String = "" + public var scope: HookStartScope? = nil + public var input: [String: Any]? = nil + public var redaction: RedactionMetadata? = nil + + public init(hookInvocationId: String = "", hookType: String = "", scope: HookStartScope? = nil, input: [String: Any]? = nil, redaction: RedactionMetadata? = nil) { + self.hookInvocationId = hookInvocationId + self.hookType = hookType + self.scope = scope + self.input = input + self.redaction = redaction + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> HookStartPayload { + let object = try TypraRuntime.object(data, typeName: "HookStartPayload") + var instance = HookStartPayload() + if let value = object["hookInvocationId"] { + instance.hookInvocationId = try TypraRuntime.string(value, field: "hookInvocationId") + } + if let value = object["hookType"] { + instance.hookType = try TypraRuntime.string(value, field: "hookType") + } + if let value = object["scope"] { + instance.scope = try HookStartScope.parse(try TypraRuntime.string(value, field: "scope")) + } + if let value = object["input"] { + instance.input = try TypraRuntime.dictionary(value, field: "input") + } + if let value = object["redaction"] { + instance.redaction = try RedactionMetadata.load(value, context: context.at("redaction")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["hookInvocationId"] = self.hookInvocationId + result["hookType"] = self.hookType + if let value = self.scope { + result["scope"] = value.rawValue + } + if let value = self.input { + result["input"] = value + } + if let value = self.redaction { + result["redaction"] = try value.save(context) + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> HookStartPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "HookStartPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> HookStartPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "HookStartPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/host_tool_request.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/host_tool_request.swift new file mode 100644 index 000000000..c1623d665 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/host_tool_request.swift @@ -0,0 +1,77 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Request passed to a host tool executor after policy and permission checks. +public struct HostToolRequest: TypraModel { + public static let shorthandProperty: String? = nil + public var requestId: String? = nil + public var toolCallId: String? = nil + public var toolName: String = "" + public var arguments: [String: Any]? = nil + public var workingDirectory: String? = nil + + public init(requestId: String? = nil, toolCallId: String? = nil, toolName: String = "", arguments: [String: Any]? = nil, workingDirectory: String? = nil) { + self.requestId = requestId + self.toolCallId = toolCallId + self.toolName = toolName + self.arguments = arguments + self.workingDirectory = workingDirectory + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> HostToolRequest { + let object = try TypraRuntime.object(data, typeName: "HostToolRequest") + var instance = HostToolRequest() + if let value = object["requestId"] { + instance.requestId = try TypraRuntime.string(value, field: "requestId") + } + if let value = object["toolCallId"] { + instance.toolCallId = try TypraRuntime.string(value, field: "toolCallId") + } + if let value = object["toolName"] { + instance.toolName = try TypraRuntime.string(value, field: "toolName") + } + if let value = object["arguments"] { + instance.arguments = try TypraRuntime.dictionary(value, field: "arguments") + } + if let value = object["workingDirectory"] { + instance.workingDirectory = try TypraRuntime.string(value, field: "workingDirectory") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.requestId { + result["requestId"] = value + } + if let value = self.toolCallId { + result["toolCallId"] = value + } + result["toolName"] = self.toolName + if let value = self.arguments { + result["arguments"] = value + } + if let value = self.workingDirectory { + result["workingDirectory"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> HostToolRequest { + return try load(TypraRuntime.jsonObject(from: json, typeName: "HostToolRequest"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> HostToolRequest { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "HostToolRequest"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/host_tool_result.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/host_tool_result.swift new file mode 100644 index 000000000..b0a95ed9a --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/host_tool_result.swift @@ -0,0 +1,107 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Result returned by a host tool executor. +public struct HostToolResult: TypraModel { + public static let shorthandProperty: String? = nil + public var requestId: String? = nil + public var toolCallId: String? = nil + public var toolName: String = "" + public var success: Bool = false + public var result: Any? = nil + public var exitCode: Int32? = nil + public var durationMs: Double? = nil + public var errorKind: String? = nil + public var telemetry: [String: Any]? = nil + + public init(requestId: String? = nil, toolCallId: String? = nil, toolName: String = "", success: Bool = false, result: Any? = nil, exitCode: Int32? = nil, durationMs: Double? = nil, errorKind: String? = nil, telemetry: [String: Any]? = nil) { + self.requestId = requestId + self.toolCallId = toolCallId + self.toolName = toolName + self.success = success + self.result = result + self.exitCode = exitCode + self.durationMs = durationMs + self.errorKind = errorKind + self.telemetry = telemetry + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> HostToolResult { + let object = try TypraRuntime.object(data, typeName: "HostToolResult") + var instance = HostToolResult() + if let value = object["requestId"] { + instance.requestId = try TypraRuntime.string(value, field: "requestId") + } + if let value = object["toolCallId"] { + instance.toolCallId = try TypraRuntime.string(value, field: "toolCallId") + } + if let value = object["toolName"] { + instance.toolName = try TypraRuntime.string(value, field: "toolName") + } + if let value = object["success"] { + instance.success = try TypraRuntime.bool(value, field: "success") + } + if let value = object["result"] { + instance.result = value + } + if let value = object["exitCode"] { + instance.exitCode = try TypraRuntime.int32(value, field: "exitCode") + } + if let value = object["durationMs"] { + instance.durationMs = try TypraRuntime.double(value, field: "durationMs") + } + if let value = object["errorKind"] { + instance.errorKind = try TypraRuntime.string(value, field: "errorKind") + } + if let value = object["telemetry"] { + instance.telemetry = try TypraRuntime.dictionary(value, field: "telemetry") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.requestId { + result["requestId"] = value + } + if let value = self.toolCallId { + result["toolCallId"] = value + } + result["toolName"] = self.toolName + result["success"] = self.success + if let value = self.result { + result["result"] = value + } + if let value = self.exitCode { + result["exitCode"] = value + } + if let value = self.durationMs { + result["durationMs"] = value + } + if let value = self.errorKind { + result["errorKind"] = value + } + if let value = self.telemetry { + result["telemetry"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> HostToolResult { + return try load(TypraRuntime.jsonObject(from: json, typeName: "HostToolResult"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> HostToolResult { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "HostToolResult"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/llm_complete_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/llm_complete_payload.swift new file mode 100644 index 000000000..9c23483a1 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/llm_complete_payload.swift @@ -0,0 +1,71 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for "llm_complete" events — an LLM request completed. +public struct LlmCompletePayload: TypraModel { + public static let shorthandProperty: String? = nil + public var requestId: String? = nil + public var serviceRequestId: String? = nil + public var usage: TokenUsage? = nil + public var durationMs: Double? = nil + + public init(requestId: String? = nil, serviceRequestId: String? = nil, usage: TokenUsage? = nil, durationMs: Double? = nil) { + self.requestId = requestId + self.serviceRequestId = serviceRequestId + self.usage = usage + self.durationMs = durationMs + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> LlmCompletePayload { + let object = try TypraRuntime.object(data, typeName: "LlmCompletePayload") + var instance = LlmCompletePayload() + if let value = object["requestId"] { + instance.requestId = try TypraRuntime.string(value, field: "requestId") + } + if let value = object["serviceRequestId"] { + instance.serviceRequestId = try TypraRuntime.string(value, field: "serviceRequestId") + } + if let value = object["usage"] { + instance.usage = try TokenUsage.load(value, context: context.at("usage")) + } + if let value = object["durationMs"] { + instance.durationMs = try TypraRuntime.double(value, field: "durationMs") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.requestId { + result["requestId"] = value + } + if let value = self.serviceRequestId { + result["serviceRequestId"] = value + } + if let value = self.usage { + result["usage"] = try value.save(context) + } + if let value = self.durationMs { + result["durationMs"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> LlmCompletePayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "LlmCompletePayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> LlmCompletePayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "LlmCompletePayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/llm_start_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/llm_start_payload.swift new file mode 100644 index 000000000..164e02d07 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/llm_start_payload.swift @@ -0,0 +1,71 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for "llm_start" events — an LLM request is about to be sent. +public struct LlmStartPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var provider: String? = nil + public var modelId: String? = nil + public var messageCount: Int32? = nil + public var attempt: Int32? = nil + + public init(provider: String? = nil, modelId: String? = nil, messageCount: Int32? = nil, attempt: Int32? = nil) { + self.provider = provider + self.modelId = modelId + self.messageCount = messageCount + self.attempt = attempt + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> LlmStartPayload { + let object = try TypraRuntime.object(data, typeName: "LlmStartPayload") + var instance = LlmStartPayload() + if let value = object["provider"] { + instance.provider = try TypraRuntime.string(value, field: "provider") + } + if let value = object["modelId"] { + instance.modelId = try TypraRuntime.string(value, field: "modelId") + } + if let value = object["messageCount"] { + instance.messageCount = try TypraRuntime.int32(value, field: "messageCount") + } + if let value = object["attempt"] { + instance.attempt = try TypraRuntime.int32(value, field: "attempt") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.provider { + result["provider"] = value + } + if let value = self.modelId { + result["modelId"] = value + } + if let value = self.messageCount { + result["messageCount"] = value + } + if let value = self.attempt { + result["attempt"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> LlmStartPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "LlmStartPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> LlmStartPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "LlmStartPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/messages_updated_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/messages_updated_payload.swift new file mode 100644 index 000000000..c47b15756 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/messages_updated_payload.swift @@ -0,0 +1,71 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for "messages_updated" events — the conversation state has changed. +public struct MessagesUpdatedPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var messages: [Message]? = nil + public var reason: String? = nil + public var appended: [Message]? = nil + public var removed: Int32? = nil + + public init(messages: [Message]? = nil, reason: String? = nil, appended: [Message]? = nil, removed: Int32? = nil) { + self.messages = messages + self.reason = reason + self.appended = appended + self.removed = removed + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> MessagesUpdatedPayload { + let object = try TypraRuntime.object(data, typeName: "MessagesUpdatedPayload") + var instance = MessagesUpdatedPayload() + if let value = object["messages"] { + instance.messages = try TypraRuntime.array(value, field: "messages").enumerated().map { try Message.load($1, context: context.at("messages").atIndex($0)) } + } + if let value = object["reason"] { + instance.reason = try TypraRuntime.string(value, field: "reason") + } + if let value = object["appended"] { + instance.appended = try TypraRuntime.array(value, field: "appended").enumerated().map { try Message.load($1, context: context.at("appended").atIndex($0)) } + } + if let value = object["removed"] { + instance.removed = try TypraRuntime.int32(value, field: "removed") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.messages { + result["messages"] = try value.map { try $0.save(context) } + } + if let value = self.reason { + result["reason"] = value + } + if let value = self.appended { + result["appended"] = try value.map { try $0.save(context) } + } + if let value = self.removed { + result["removed"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> MessagesUpdatedPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "MessagesUpdatedPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> MessagesUpdatedPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "MessagesUpdatedPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/permission_completed_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/permission_completed_payload.swift new file mode 100644 index 000000000..8909dd86d --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/permission_completed_payload.swift @@ -0,0 +1,91 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for permission completion events — an approval decision was made. +public struct PermissionCompletedPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var requestId: String? = nil + public var toolCallId: String? = nil + public var permission: String = "" + public var approved: Bool = false + public var reason: String? = nil + public var result: [String: Any]? = nil + public var redaction: RedactionMetadata? = nil + + public init(requestId: String? = nil, toolCallId: String? = nil, permission: String = "", approved: Bool = false, reason: String? = nil, result: [String: Any]? = nil, redaction: RedactionMetadata? = nil) { + self.requestId = requestId + self.toolCallId = toolCallId + self.permission = permission + self.approved = approved + self.reason = reason + self.result = result + self.redaction = redaction + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> PermissionCompletedPayload { + let object = try TypraRuntime.object(data, typeName: "PermissionCompletedPayload") + var instance = PermissionCompletedPayload() + if let value = object["requestId"] { + instance.requestId = try TypraRuntime.string(value, field: "requestId") + } + if let value = object["toolCallId"] { + instance.toolCallId = try TypraRuntime.string(value, field: "toolCallId") + } + if let value = object["permission"] { + instance.permission = try TypraRuntime.string(value, field: "permission") + } + if let value = object["approved"] { + instance.approved = try TypraRuntime.bool(value, field: "approved") + } + if let value = object["reason"] { + instance.reason = try TypraRuntime.string(value, field: "reason") + } + if let value = object["result"] { + instance.result = try TypraRuntime.dictionary(value, field: "result") + } + if let value = object["redaction"] { + instance.redaction = try RedactionMetadata.load(value, context: context.at("redaction")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.requestId { + result["requestId"] = value + } + if let value = self.toolCallId { + result["toolCallId"] = value + } + result["permission"] = self.permission + result["approved"] = self.approved + if let value = self.reason { + result["reason"] = value + } + if let value = self.result { + result["result"] = value + } + if let value = self.redaction { + result["redaction"] = try value.save(context) + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> PermissionCompletedPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "PermissionCompletedPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> PermissionCompletedPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "PermissionCompletedPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/permission_decision.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/permission_decision.swift new file mode 100644 index 000000000..213ca86f0 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/permission_decision.swift @@ -0,0 +1,83 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Decision returned by a permission resolver. +public struct PermissionDecision: TypraModel { + public static let shorthandProperty: String? = nil + public var requestId: String? = nil + public var toolCallId: String? = nil + public var permission: String = "" + public var approved: Bool = false + public var reason: String? = nil + public var result: [String: Any]? = nil + + public init(requestId: String? = nil, toolCallId: String? = nil, permission: String = "", approved: Bool = false, reason: String? = nil, result: [String: Any]? = nil) { + self.requestId = requestId + self.toolCallId = toolCallId + self.permission = permission + self.approved = approved + self.reason = reason + self.result = result + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> PermissionDecision { + let object = try TypraRuntime.object(data, typeName: "PermissionDecision") + var instance = PermissionDecision() + if let value = object["requestId"] { + instance.requestId = try TypraRuntime.string(value, field: "requestId") + } + if let value = object["toolCallId"] { + instance.toolCallId = try TypraRuntime.string(value, field: "toolCallId") + } + if let value = object["permission"] { + instance.permission = try TypraRuntime.string(value, field: "permission") + } + if let value = object["approved"] { + instance.approved = try TypraRuntime.bool(value, field: "approved") + } + if let value = object["reason"] { + instance.reason = try TypraRuntime.string(value, field: "reason") + } + if let value = object["result"] { + instance.result = try TypraRuntime.dictionary(value, field: "result") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.requestId { + result["requestId"] = value + } + if let value = self.toolCallId { + result["toolCallId"] = value + } + result["permission"] = self.permission + result["approved"] = self.approved + if let value = self.reason { + result["reason"] = value + } + if let value = self.result { + result["result"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> PermissionDecision { + return try load(TypraRuntime.jsonObject(from: json, typeName: "PermissionDecision"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> PermissionDecision { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "PermissionDecision"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/permission_request.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/permission_request.swift new file mode 100644 index 000000000..f7b0a6018 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/permission_request.swift @@ -0,0 +1,93 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Request passed to a permission resolver. This is the live protocol shape; the event payloads above can include trace-only metadata such as redaction state. +public struct PermissionRequest: TypraModel { + public static let shorthandProperty: String? = nil + public var requestId: String? = nil + public var toolCallId: String? = nil + public var permission: String = "" + public var target: String? = nil + public var details: [String: Any]? = nil + public var promptRequest: String? = nil + public var policy: [String: Any]? = nil + + public init(requestId: String? = nil, toolCallId: String? = nil, permission: String = "", target: String? = nil, details: [String: Any]? = nil, promptRequest: String? = nil, policy: [String: Any]? = nil) { + self.requestId = requestId + self.toolCallId = toolCallId + self.permission = permission + self.target = target + self.details = details + self.promptRequest = promptRequest + self.policy = policy + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> PermissionRequest { + let object = try TypraRuntime.object(data, typeName: "PermissionRequest") + var instance = PermissionRequest() + if let value = object["requestId"] { + instance.requestId = try TypraRuntime.string(value, field: "requestId") + } + if let value = object["toolCallId"] { + instance.toolCallId = try TypraRuntime.string(value, field: "toolCallId") + } + if let value = object["permission"] { + instance.permission = try TypraRuntime.string(value, field: "permission") + } + if let value = object["target"] { + instance.target = try TypraRuntime.string(value, field: "target") + } + if let value = object["details"] { + instance.details = try TypraRuntime.dictionary(value, field: "details") + } + if let value = object["promptRequest"] { + instance.promptRequest = try TypraRuntime.string(value, field: "promptRequest") + } + if let value = object["policy"] { + instance.policy = try TypraRuntime.dictionary(value, field: "policy") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.requestId { + result["requestId"] = value + } + if let value = self.toolCallId { + result["toolCallId"] = value + } + result["permission"] = self.permission + if let value = self.target { + result["target"] = value + } + if let value = self.details { + result["details"] = value + } + if let value = self.promptRequest { + result["promptRequest"] = value + } + if let value = self.policy { + result["policy"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> PermissionRequest { + return try load(TypraRuntime.jsonObject(from: json, typeName: "PermissionRequest"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> PermissionRequest { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "PermissionRequest"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/permission_requested_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/permission_requested_payload.swift new file mode 100644 index 000000000..18c2dcde0 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/permission_requested_payload.swift @@ -0,0 +1,101 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for permission request events — a host is asked to approve an action. +public struct PermissionRequestedPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var requestId: String? = nil + public var toolCallId: String? = nil + public var permission: String = "" + public var target: String? = nil + public var details: [String: Any]? = nil + public var promptRequest: String? = nil + public var policy: [String: Any]? = nil + public var redaction: RedactionMetadata? = nil + + public init(requestId: String? = nil, toolCallId: String? = nil, permission: String = "", target: String? = nil, details: [String: Any]? = nil, promptRequest: String? = nil, policy: [String: Any]? = nil, redaction: RedactionMetadata? = nil) { + self.requestId = requestId + self.toolCallId = toolCallId + self.permission = permission + self.target = target + self.details = details + self.promptRequest = promptRequest + self.policy = policy + self.redaction = redaction + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> PermissionRequestedPayload { + let object = try TypraRuntime.object(data, typeName: "PermissionRequestedPayload") + var instance = PermissionRequestedPayload() + if let value = object["requestId"] { + instance.requestId = try TypraRuntime.string(value, field: "requestId") + } + if let value = object["toolCallId"] { + instance.toolCallId = try TypraRuntime.string(value, field: "toolCallId") + } + if let value = object["permission"] { + instance.permission = try TypraRuntime.string(value, field: "permission") + } + if let value = object["target"] { + instance.target = try TypraRuntime.string(value, field: "target") + } + if let value = object["details"] { + instance.details = try TypraRuntime.dictionary(value, field: "details") + } + if let value = object["promptRequest"] { + instance.promptRequest = try TypraRuntime.string(value, field: "promptRequest") + } + if let value = object["policy"] { + instance.policy = try TypraRuntime.dictionary(value, field: "policy") + } + if let value = object["redaction"] { + instance.redaction = try RedactionMetadata.load(value, context: context.at("redaction")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.requestId { + result["requestId"] = value + } + if let value = self.toolCallId { + result["toolCallId"] = value + } + result["permission"] = self.permission + if let value = self.target { + result["target"] = value + } + if let value = self.details { + result["details"] = value + } + if let value = self.promptRequest { + result["promptRequest"] = value + } + if let value = self.policy { + result["policy"] = value + } + if let value = self.redaction { + result["redaction"] = try value.save(context) + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> PermissionRequestedPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "PermissionRequestedPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> PermissionRequestedPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "PermissionRequestedPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/redacted_field.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/redacted_field.swift new file mode 100644 index 000000000..44f12acf2 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/redacted_field.swift @@ -0,0 +1,77 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum RedactionMode: String, Codable, CaseIterable { + case `none` = "none" + case redacted = "redacted" + case hashed = "hashed" + case summary = "summary" + case reference = "reference" + public static func parse(_ value: String) throws -> RedactionMode { + switch value { + case "none": return .`none` + case "redacted": return .redacted + case "hashed": return .hashed + case "summary": return .summary + case "reference": return .reference + default: throw TypraRuntimeError.invalidEnum(type: "RedactionMode", value: value) + } + } +} + +/// Redaction handling for one JSON-shaped field path. +public struct RedactedField: TypraModel { + public static let shorthandProperty: String? = nil + public var path: String = "" + public var mode: RedactionMode = (try! RedactionMode.parse("none")) + public var reason: String? = nil + + public init(path: String = "", mode: RedactionMode = (try! RedactionMode.parse("none")), reason: String? = nil) { + self.path = path + self.mode = mode + self.reason = reason + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> RedactedField { + let object = try TypraRuntime.object(data, typeName: "RedactedField") + var instance = RedactedField() + if let value = object["path"] { + instance.path = try TypraRuntime.string(value, field: "path") + } + if let value = object["mode"] { + instance.mode = try RedactionMode.parse(try TypraRuntime.string(value, field: "mode")) + } + if let value = object["reason"] { + instance.reason = try TypraRuntime.string(value, field: "reason") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["path"] = self.path + result["mode"] = self.mode.rawValue + if let value = self.reason { + result["reason"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> RedactedField { + return try load(TypraRuntime.jsonObject(from: json, typeName: "RedactedField"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> RedactedField { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "RedactedField"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/redaction_metadata.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/redaction_metadata.swift new file mode 100644 index 000000000..012088e95 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/redaction_metadata.swift @@ -0,0 +1,63 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Metadata describing whether and how a payload was sanitized. +public struct RedactionMetadata: TypraModel { + public static let shorthandProperty: String? = nil + public var sanitized: Bool? = nil + public var fields: [RedactedField]? = nil + public var policy: String? = nil + + public init(sanitized: Bool? = nil, fields: [RedactedField]? = nil, policy: String? = nil) { + self.sanitized = sanitized + self.fields = fields + self.policy = policy + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> RedactionMetadata { + let object = try TypraRuntime.object(data, typeName: "RedactionMetadata") + var instance = RedactionMetadata() + if let value = object["sanitized"] { + instance.sanitized = try TypraRuntime.bool(value, field: "sanitized") + } + if let value = object["fields"] { + instance.fields = try TypraRuntime.array(value, field: "fields").enumerated().map { try RedactedField.load($1, context: context.at("fields").atIndex($0)) } + } + if let value = object["policy"] { + instance.policy = try TypraRuntime.string(value, field: "policy") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.sanitized { + result["sanitized"] = value + } + if let value = self.fields { + result["fields"] = try value.map { try $0.save(context) } + } + if let value = self.policy { + result["policy"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> RedactionMetadata { + return try load(TypraRuntime.jsonObject(from: json, typeName: "RedactionMetadata"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> RedactionMetadata { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "RedactionMetadata"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/retry_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/retry_payload.swift new file mode 100644 index 000000000..31411992c --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/retry_payload.swift @@ -0,0 +1,75 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for "retry" events — a transient operation will be retried. +public struct RetryPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var operation: String = "" + public var attempt: Int32 = 0 + public var maxAttempts: Int32? = nil + public var delayMs: Double? = nil + public var reason: String? = nil + + public init(operation: String = "", attempt: Int32 = 0, maxAttempts: Int32? = nil, delayMs: Double? = nil, reason: String? = nil) { + self.operation = operation + self.attempt = attempt + self.maxAttempts = maxAttempts + self.delayMs = delayMs + self.reason = reason + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> RetryPayload { + let object = try TypraRuntime.object(data, typeName: "RetryPayload") + var instance = RetryPayload() + if let value = object["operation"] { + instance.operation = try TypraRuntime.string(value, field: "operation") + } + if let value = object["attempt"] { + instance.attempt = try TypraRuntime.int32(value, field: "attempt") + } + if let value = object["maxAttempts"] { + instance.maxAttempts = try TypraRuntime.int32(value, field: "maxAttempts") + } + if let value = object["delayMs"] { + instance.delayMs = try TypraRuntime.double(value, field: "delayMs") + } + if let value = object["reason"] { + instance.reason = try TypraRuntime.string(value, field: "reason") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["operation"] = self.operation + result["attempt"] = self.attempt + if let value = self.maxAttempts { + result["maxAttempts"] = value + } + if let value = self.delayMs { + result["delayMs"] = value + } + if let value = self.reason { + result["reason"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> RetryPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "RetryPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> RetryPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "RetryPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_end_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_end_payload.swift new file mode 100644 index 000000000..2a2e309bd --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_end_payload.swift @@ -0,0 +1,87 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum SessionEndStatus: String, Codable, CaseIterable { + case success = "success" + case error = "error" + case cancelled = "cancelled" + case interrupted = "interrupted" + public static func parse(_ value: String) throws -> SessionEndStatus { + switch value { + case "success": return .success + case "error": return .error + case "cancelled": return .cancelled + case "interrupted": return .interrupted + default: throw TypraRuntimeError.invalidEnum(type: "SessionEndStatus", value: value) + } + } +} + +/// Payload for "session_end" events. +public struct SessionEndPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var sessionId: String? = nil + public var status: SessionEndStatus? = nil + public var reason: String? = nil + public var durationMs: Double? = nil + + public init(sessionId: String? = nil, status: SessionEndStatus? = nil, reason: String? = nil, durationMs: Double? = nil) { + self.sessionId = sessionId + self.status = status + self.reason = reason + self.durationMs = durationMs + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> SessionEndPayload { + let object = try TypraRuntime.object(data, typeName: "SessionEndPayload") + var instance = SessionEndPayload() + if let value = object["sessionId"] { + instance.sessionId = try TypraRuntime.string(value, field: "sessionId") + } + if let value = object["status"] { + instance.status = try SessionEndStatus.parse(try TypraRuntime.string(value, field: "status")) + } + if let value = object["reason"] { + instance.reason = try TypraRuntime.string(value, field: "reason") + } + if let value = object["durationMs"] { + instance.durationMs = try TypraRuntime.double(value, field: "durationMs") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.sessionId { + result["sessionId"] = value + } + if let value = self.status { + result["status"] = value.rawValue + } + if let value = self.reason { + result["reason"] = value + } + if let value = self.durationMs { + result["durationMs"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> SessionEndPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "SessionEndPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> SessionEndPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "SessionEndPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_event.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_event.swift new file mode 100644 index 000000000..102c8294e --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_event.swift @@ -0,0 +1,125 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum SessionEventType: String, Codable, CaseIterable { + case sessionStart = "session_start" + case sessionEnd = "session_end" + case sessionWarning = "session_warning" + case sessionHookStart = "session_hook_start" + case sessionHookEnd = "session_hook_end" + case checkpointCreated = "checkpoint_created" + case trajectoryEvent = "trajectory_event" + public static func parse(_ value: String) throws -> SessionEventType { + switch value { + case "session_start": return .sessionStart + case "session_end": return .sessionEnd + case "session_warning": return .sessionWarning + case "session_hook_start": return .sessionHookStart + case "session_hook_end": return .sessionHookEnd + case "checkpoint_created": return .checkpointCreated + case "trajectory_event": return .trajectoryEvent + default: throw TypraRuntimeError.invalidEnum(type: "SessionEventType", value: value) + } + } +} + +/// A canonical event envelope emitted by an outer harness session. +public struct SessionEvent: TypraModel { + public static let shorthandProperty: String? = nil + public var id: String = "" + public var type: SessionEventType = (try! SessionEventType.parse("session_start")) + public var timestamp: String = "" + public var sessionId: String? = nil + public var turnId: String? = nil + public var parentId: String? = nil + public var spanId: String? = nil + public var payload: [String: Any] = [:] + public var redaction: RedactionMetadata? = nil + + public init(id: String = "", type: SessionEventType = (try! SessionEventType.parse("session_start")), timestamp: String = "", sessionId: String? = nil, turnId: String? = nil, parentId: String? = nil, spanId: String? = nil, payload: [String: Any] = [:], redaction: RedactionMetadata? = nil) { + self.id = id + self.type = type + self.timestamp = timestamp + self.sessionId = sessionId + self.turnId = turnId + self.parentId = parentId + self.spanId = spanId + self.payload = payload + self.redaction = redaction + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> SessionEvent { + let object = try TypraRuntime.object(data, typeName: "SessionEvent") + var instance = SessionEvent() + if let value = object["id"] { + instance.id = try TypraRuntime.string(value, field: "id") + } + if let value = object["type"] { + instance.type = try SessionEventType.parse(try TypraRuntime.string(value, field: "type")) + } + if let value = object["timestamp"] { + instance.timestamp = try TypraRuntime.string(value, field: "timestamp") + } + if let value = object["sessionId"] { + instance.sessionId = try TypraRuntime.string(value, field: "sessionId") + } + if let value = object["turnId"] { + instance.turnId = try TypraRuntime.string(value, field: "turnId") + } + if let value = object["parentId"] { + instance.parentId = try TypraRuntime.string(value, field: "parentId") + } + if let value = object["spanId"] { + instance.spanId = try TypraRuntime.string(value, field: "spanId") + } + if let value = object["payload"] { + instance.payload = try TypraRuntime.dictionary(value, field: "payload") + } + if let value = object["redaction"] { + instance.redaction = try RedactionMetadata.load(value, context: context.at("redaction")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["id"] = self.id + result["type"] = self.type.rawValue + result["timestamp"] = self.timestamp + if let value = self.sessionId { + result["sessionId"] = value + } + if let value = self.turnId { + result["turnId"] = value + } + if let value = self.parentId { + result["parentId"] = value + } + if let value = self.spanId { + result["spanId"] = value + } + result["payload"] = self.payload + if let value = self.redaction { + result["redaction"] = try value.save(context) + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> SessionEvent { + return try load(TypraRuntime.jsonObject(from: json, typeName: "SessionEvent"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> SessionEvent { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "SessionEvent"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_file_ref.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_file_ref.swift new file mode 100644 index 000000000..63ecc4d9e --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_file_ref.swift @@ -0,0 +1,77 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// A file observed or touched by a harness session. +public struct SessionFileRef: TypraModel { + public static let shorthandProperty: String? = nil + public var sessionId: String? = nil + public var path: String = "" + public var toolName: String? = nil + public var turnIndex: Int32? = nil + public var firstSeenAt: String? = nil + + public init(sessionId: String? = nil, path: String = "", toolName: String? = nil, turnIndex: Int32? = nil, firstSeenAt: String? = nil) { + self.sessionId = sessionId + self.path = path + self.toolName = toolName + self.turnIndex = turnIndex + self.firstSeenAt = firstSeenAt + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> SessionFileRef { + let object = try TypraRuntime.object(data, typeName: "SessionFileRef") + var instance = SessionFileRef() + if let value = object["sessionId"] { + instance.sessionId = try TypraRuntime.string(value, field: "sessionId") + } + if let value = object["path"] { + instance.path = try TypraRuntime.string(value, field: "path") + } + if let value = object["toolName"] { + instance.toolName = try TypraRuntime.string(value, field: "toolName") + } + if let value = object["turnIndex"] { + instance.turnIndex = try TypraRuntime.int32(value, field: "turnIndex") + } + if let value = object["firstSeenAt"] { + instance.firstSeenAt = try TypraRuntime.string(value, field: "firstSeenAt") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.sessionId { + result["sessionId"] = value + } + result["path"] = self.path + if let value = self.toolName { + result["toolName"] = value + } + if let value = self.turnIndex { + result["turnIndex"] = value + } + if let value = self.firstSeenAt { + result["firstSeenAt"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> SessionFileRef { + return try load(TypraRuntime.jsonObject(from: json, typeName: "SessionFileRef"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> SessionFileRef { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "SessionFileRef"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_ref.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_ref.swift new file mode 100644 index 000000000..e0ecae214 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_ref.swift @@ -0,0 +1,75 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// A non-file reference observed by a harness session. +public struct SessionRef: TypraModel { + public static let shorthandProperty: String? = nil + public var sessionId: String? = nil + public var refType: String = "" + public var refValue: String = "" + public var turnIndex: Int32? = nil + public var createdAt: String? = nil + + public init(sessionId: String? = nil, refType: String = "", refValue: String = "", turnIndex: Int32? = nil, createdAt: String? = nil) { + self.sessionId = sessionId + self.refType = refType + self.refValue = refValue + self.turnIndex = turnIndex + self.createdAt = createdAt + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> SessionRef { + let object = try TypraRuntime.object(data, typeName: "SessionRef") + var instance = SessionRef() + if let value = object["sessionId"] { + instance.sessionId = try TypraRuntime.string(value, field: "sessionId") + } + if let value = object["refType"] { + instance.refType = try TypraRuntime.string(value, field: "refType") + } + if let value = object["refValue"] { + instance.refValue = try TypraRuntime.string(value, field: "refValue") + } + if let value = object["turnIndex"] { + instance.turnIndex = try TypraRuntime.int32(value, field: "turnIndex") + } + if let value = object["createdAt"] { + instance.createdAt = try TypraRuntime.string(value, field: "createdAt") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.sessionId { + result["sessionId"] = value + } + result["refType"] = self.refType + result["refValue"] = self.refValue + if let value = self.turnIndex { + result["turnIndex"] = value + } + if let value = self.createdAt { + result["createdAt"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> SessionRef { + return try load(TypraRuntime.jsonObject(from: json, typeName: "SessionRef"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> SessionRef { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "SessionRef"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_start_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_start_payload.swift new file mode 100644 index 000000000..533aee59d --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_start_payload.swift @@ -0,0 +1,109 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for "session_start" events. +public struct SessionStartPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var sessionId: String = "" + public var schemaVersion: String? = nil + public var producer: String? = nil + public var runtime: String? = nil + public var promptyVersion: String? = nil + public var startTime: String? = nil + public var selectedModel: String? = nil + public var reasoningEffort: String? = nil + public var context: HarnessContext? = nil + + public init(sessionId: String = "", schemaVersion: String? = nil, producer: String? = nil, runtime: String? = nil, promptyVersion: String? = nil, startTime: String? = nil, selectedModel: String? = nil, reasoningEffort: String? = nil, context: HarnessContext? = nil) { + self.sessionId = sessionId + self.schemaVersion = schemaVersion + self.producer = producer + self.runtime = runtime + self.promptyVersion = promptyVersion + self.startTime = startTime + self.selectedModel = selectedModel + self.reasoningEffort = reasoningEffort + self.context = context + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> SessionStartPayload { + let object = try TypraRuntime.object(data, typeName: "SessionStartPayload") + var instance = SessionStartPayload() + if let value = object["sessionId"] { + instance.sessionId = try TypraRuntime.string(value, field: "sessionId") + } + if let value = object["schemaVersion"] { + instance.schemaVersion = try TypraRuntime.string(value, field: "schemaVersion") + } + if let value = object["producer"] { + instance.producer = try TypraRuntime.string(value, field: "producer") + } + if let value = object["runtime"] { + instance.runtime = try TypraRuntime.string(value, field: "runtime") + } + if let value = object["promptyVersion"] { + instance.promptyVersion = try TypraRuntime.string(value, field: "promptyVersion") + } + if let value = object["startTime"] { + instance.startTime = try TypraRuntime.string(value, field: "startTime") + } + if let value = object["selectedModel"] { + instance.selectedModel = try TypraRuntime.string(value, field: "selectedModel") + } + if let value = object["reasoningEffort"] { + instance.reasoningEffort = try TypraRuntime.string(value, field: "reasoningEffort") + } + if let value = object["context"] { + instance.context = try HarnessContext.load(value, context: context.at("context")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["sessionId"] = self.sessionId + if let value = self.schemaVersion { + result["schemaVersion"] = value + } + if let value = self.producer { + result["producer"] = value + } + if let value = self.runtime { + result["runtime"] = value + } + if let value = self.promptyVersion { + result["promptyVersion"] = value + } + if let value = self.startTime { + result["startTime"] = value + } + if let value = self.selectedModel { + result["selectedModel"] = value + } + if let value = self.reasoningEffort { + result["reasoningEffort"] = value + } + if let value = self.context { + result["context"] = try value.save(context) + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> SessionStartPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "SessionStartPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> SessionStartPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "SessionStartPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_summary.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_summary.swift new file mode 100644 index 000000000..716ef6fcd --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_summary.swift @@ -0,0 +1,101 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum SessionSummaryStatus: String, Codable, CaseIterable { + case success = "success" + case error = "error" + case cancelled = "cancelled" + case interrupted = "interrupted" + public static func parse(_ value: String) throws -> SessionSummaryStatus { + switch value { + case "success": return .success + case "error": return .error + case "cancelled": return .cancelled + case "interrupted": return .interrupted + default: throw TypraRuntimeError.invalidEnum(type: "SessionSummaryStatus", value: value) + } + } +} + +/// Summary statistics for a completed session trace. +public struct SessionSummary: TypraModel { + public static let shorthandProperty: String? = nil + public var sessionId: String = "" + public var status: SessionSummaryStatus? = nil + public var turns: Int32? = nil + public var checkpoints: Int32? = nil + public var usage: TokenUsage? = nil + public var durationMs: Double? = nil + + public init(sessionId: String = "", status: SessionSummaryStatus? = nil, turns: Int32? = nil, checkpoints: Int32? = nil, usage: TokenUsage? = nil, durationMs: Double? = nil) { + self.sessionId = sessionId + self.status = status + self.turns = turns + self.checkpoints = checkpoints + self.usage = usage + self.durationMs = durationMs + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> SessionSummary { + let object = try TypraRuntime.object(data, typeName: "SessionSummary") + var instance = SessionSummary() + if let value = object["sessionId"] { + instance.sessionId = try TypraRuntime.string(value, field: "sessionId") + } + if let value = object["status"] { + instance.status = try SessionSummaryStatus.parse(try TypraRuntime.string(value, field: "status")) + } + if let value = object["turns"] { + instance.turns = try TypraRuntime.int32(value, field: "turns") + } + if let value = object["checkpoints"] { + instance.checkpoints = try TypraRuntime.int32(value, field: "checkpoints") + } + if let value = object["usage"] { + instance.usage = try TokenUsage.load(value, context: context.at("usage")) + } + if let value = object["durationMs"] { + instance.durationMs = try TypraRuntime.double(value, field: "durationMs") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["sessionId"] = self.sessionId + if let value = self.status { + result["status"] = value.rawValue + } + if let value = self.turns { + result["turns"] = value + } + if let value = self.checkpoints { + result["checkpoints"] = value + } + if let value = self.usage { + result["usage"] = try value.save(context) + } + if let value = self.durationMs { + result["durationMs"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> SessionSummary { + return try load(TypraRuntime.jsonObject(from: json, typeName: "SessionSummary"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> SessionSummary { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "SessionSummary"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_trace.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_trace.swift new file mode 100644 index 000000000..d38ad1ad1 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_trace.swift @@ -0,0 +1,126 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Portable replay container for an outer harness session. +public struct SessionTrace: TypraModel { + public static let shorthandProperty: String? = nil + public var version: String = "1" + public var runtime: String? = nil + public var promptyVersion: String? = nil + public var sessionId: String? = nil + public var events: [SessionEvent] = [] + public var turns: [TurnTrace]? = nil + public var checkpoints: [Checkpoint]? = nil + public var trajectory: [TrajectoryEvent]? = nil + public var files: [SessionFileRef]? = nil + public var refs: [SessionRef]? = nil + public var summary: SessionSummary? = nil + + public init(version: String = "1", runtime: String? = nil, promptyVersion: String? = nil, sessionId: String? = nil, events: [SessionEvent] = [], turns: [TurnTrace]? = nil, checkpoints: [Checkpoint]? = nil, trajectory: [TrajectoryEvent]? = nil, files: [SessionFileRef]? = nil, refs: [SessionRef]? = nil, summary: SessionSummary? = nil) { + self.version = version + self.runtime = runtime + self.promptyVersion = promptyVersion + self.sessionId = sessionId + self.events = events + self.turns = turns + self.checkpoints = checkpoints + self.trajectory = trajectory + self.files = files + self.refs = refs + self.summary = summary + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> SessionTrace { + let object = try TypraRuntime.object(data, typeName: "SessionTrace") + var instance = SessionTrace() + if let value = object["version"] { + instance.version = try TypraRuntime.string(value, field: "version") + } + else { + instance.version = "1" + } + if let value = object["runtime"] { + instance.runtime = try TypraRuntime.string(value, field: "runtime") + } + if let value = object["promptyVersion"] { + instance.promptyVersion = try TypraRuntime.string(value, field: "promptyVersion") + } + if let value = object["sessionId"] { + instance.sessionId = try TypraRuntime.string(value, field: "sessionId") + } + if let value = object["events"] { + instance.events = try TypraRuntime.array(value, field: "events").enumerated().map { try SessionEvent.load($1, context: context.at("events").atIndex($0)) } + } + if let value = object["turns"] { + instance.turns = try TypraRuntime.array(value, field: "turns").enumerated().map { try TurnTrace.load($1, context: context.at("turns").atIndex($0)) } + } + if let value = object["checkpoints"] { + instance.checkpoints = try TypraRuntime.array(value, field: "checkpoints").enumerated().map { try Checkpoint.load($1, context: context.at("checkpoints").atIndex($0)) } + } + if let value = object["trajectory"] { + instance.trajectory = try TypraRuntime.array(value, field: "trajectory").enumerated().map { try TrajectoryEvent.load($1, context: context.at("trajectory").atIndex($0)) } + } + if let value = object["files"] { + instance.files = try TypraRuntime.array(value, field: "files").enumerated().map { try SessionFileRef.load($1, context: context.at("files").atIndex($0)) } + } + if let value = object["refs"] { + instance.refs = try TypraRuntime.array(value, field: "refs").enumerated().map { try SessionRef.load($1, context: context.at("refs").atIndex($0)) } + } + if let value = object["summary"] { + instance.summary = try SessionSummary.load(value, context: context.at("summary")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["version"] = self.version + if let value = self.runtime { + result["runtime"] = value + } + if let value = self.promptyVersion { + result["promptyVersion"] = value + } + if let value = self.sessionId { + result["sessionId"] = value + } + result["events"] = try self.events.map { try $0.save(context) } + if let value = self.turns { + result["turns"] = try value.map { try $0.save(context) } + } + if let value = self.checkpoints { + result["checkpoints"] = try value.map { try $0.save(context) } + } + if let value = self.trajectory { + result["trajectory"] = try value.map { try $0.save(context) } + } + if let value = self.files { + result["files"] = try value.map { try $0.save(context) } + } + if let value = self.refs { + result["refs"] = try value.map { try $0.save(context) } + } + if let value = self.summary { + result["summary"] = try value.save(context) + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> SessionTrace { + return try load(TypraRuntime.jsonObject(from: json, typeName: "SessionTrace"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> SessionTrace { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "SessionTrace"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_warning_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_warning_payload.swift new file mode 100644 index 000000000..02e256374 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_warning_payload.swift @@ -0,0 +1,59 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for "session_warning" events. +public struct SessionWarningPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var warningType: String = "" + public var message: String = "" + public var details: [String: Any]? = nil + + public init(warningType: String = "", message: String = "", details: [String: Any]? = nil) { + self.warningType = warningType + self.message = message + self.details = details + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> SessionWarningPayload { + let object = try TypraRuntime.object(data, typeName: "SessionWarningPayload") + var instance = SessionWarningPayload() + if let value = object["warningType"] { + instance.warningType = try TypraRuntime.string(value, field: "warningType") + } + if let value = object["message"] { + instance.message = try TypraRuntime.string(value, field: "message") + } + if let value = object["details"] { + instance.details = try TypraRuntime.dictionary(value, field: "details") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["warningType"] = self.warningType + result["message"] = self.message + if let value = self.details { + result["details"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> SessionWarningPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "SessionWarningPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> SessionWarningPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "SessionWarningPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/status_event_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/status_event_payload.swift new file mode 100644 index 000000000..753130ce7 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/status_event_payload.swift @@ -0,0 +1,45 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for "status" events — informational messages about loop progress. +public struct StatusEventPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var message: String = "" + + public init(message: String = "") { + self.message = message + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> StatusEventPayload { + let object = try TypraRuntime.object(data, typeName: "StatusEventPayload") + var instance = StatusEventPayload() + if let value = object["message"] { + instance.message = try TypraRuntime.string(value, field: "message") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["message"] = self.message + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> StatusEventPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "StatusEventPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> StatusEventPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "StatusEventPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/stream_chunk.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/stream_chunk.swift new file mode 100644 index 000000000..3a9fee156 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/stream_chunk.swift @@ -0,0 +1,311 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum StreamChunk: TypraModel { + case textChunk(TextChunk) + case thinkingChunk(ThinkingChunk) + case toolChunk(ToolChunk) + case usageChunk(UsageChunk) + case errorChunk(ErrorChunk) + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> StreamChunk { + let normalizedData: Any = data + let object = try TypraRuntime.object(normalizedData, typeName: "StreamChunk") + let discriminator = try TypraRuntime.string(object["kind"] ?? NSNull(), field: "kind") + if discriminator.isEmpty { + throw TypraRuntimeError.invalidField(field: context.at("kind").path, expected: "non-blank string") + } + switch discriminator { + case "text": return .textChunk(try TextChunk.load(normalizedData, context: context)) + case "thinking": return .thinkingChunk(try ThinkingChunk.load(normalizedData, context: context)) + case "tool": return .toolChunk(try ToolChunk.load(normalizedData, context: context)) + case "usage": return .usageChunk(try UsageChunk.load(normalizedData, context: context)) + case "error": return .errorChunk(try ErrorChunk.load(normalizedData, context: context)) + default: throw TypraRuntimeError.unknownDiscriminator(type: "StreamChunk", field: "kind", value: discriminator) + } + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + switch self { + case .textChunk(let value): return try value.save(context) + case .thinkingChunk(let value): return try value.save(context) + case .toolChunk(let value): return try value.save(context) + case .usageChunk(let value): return try value.save(context) + case .errorChunk(let value): return try value.save(context) + } + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> StreamChunk { + return try load(TypraRuntime.jsonObject(from: json, typeName: "StreamChunk"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> StreamChunk { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "StreamChunk"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +/// A text content chunk from the LLM response stream. +public struct TextChunk: TypraModel { + public static let shorthandProperty: String? = nil + public var kind: String = "text" + public var value: String = "" + + public init(kind: String = "text", value: String = "") { + self.kind = kind + self.value = value + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> TextChunk { + let object = try TypraRuntime.object(data, typeName: "TextChunk") + var instance = TextChunk() + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "text" + } + if let value = object["value"] { + instance.value = try TypraRuntime.string(value, field: "value") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["kind"] = self.kind + result["value"] = self.value + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> TextChunk { + return try load(TypraRuntime.jsonObject(from: json, typeName: "TextChunk"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> TextChunk { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "TextChunk"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +/// A thinking/reasoning content chunk from the LLM response stream. +public struct ThinkingChunk: TypraModel { + public static let shorthandProperty: String? = nil + public var kind: String = "thinking" + public var value: String = "" + + public init(kind: String = "thinking", value: String = "") { + self.kind = kind + self.value = value + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ThinkingChunk { + let object = try TypraRuntime.object(data, typeName: "ThinkingChunk") + var instance = ThinkingChunk() + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "thinking" + } + if let value = object["value"] { + instance.value = try TypraRuntime.string(value, field: "value") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["kind"] = self.kind + result["value"] = self.value + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ThinkingChunk { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ThinkingChunk"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ThinkingChunk { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ThinkingChunk"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +/// A tool call chunk from the LLM response stream. +public struct ToolChunk: TypraModel { + public static let shorthandProperty: String? = nil + public var kind: String = "tool" + public var toolCall: ToolCall = ToolCall() + + public init(kind: String = "tool", toolCall: ToolCall = ToolCall()) { + self.kind = kind + self.toolCall = toolCall + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ToolChunk { + let object = try TypraRuntime.object(data, typeName: "ToolChunk") + var instance = ToolChunk() + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "tool" + } + if object["toolCall"] == nil || object["toolCall"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("toolCall").path + ": missing required field") + } + if let value = object["toolCall"] { + instance.toolCall = try ToolCall.load(value, context: context.at("toolCall")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["kind"] = self.kind + result["toolCall"] = try self.toolCall.save(context) + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ToolChunk { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ToolChunk"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ToolChunk { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ToolChunk"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +/// Cumulative token usage emitted once after provider content and tool chunks. +public struct UsageChunk: TypraModel { + public static let shorthandProperty: String? = nil + public var kind: String = "usage" + public var usage: InvocationUsage = InvocationUsage() + + public init(kind: String = "usage", usage: InvocationUsage = InvocationUsage()) { + self.kind = kind + self.usage = usage + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> UsageChunk { + let object = try TypraRuntime.object(data, typeName: "UsageChunk") + var instance = UsageChunk() + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "usage" + } + if object["usage"] == nil || object["usage"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("usage").path + ": missing required field") + } + if let value = object["usage"] { + instance.usage = try InvocationUsage.load(value, context: context.at("usage")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["kind"] = self.kind + result["usage"] = try self.usage.save(context) + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> UsageChunk { + return try load(TypraRuntime.jsonObject(from: json, typeName: "UsageChunk"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> UsageChunk { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "UsageChunk"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +/// An error chunk from the LLM response stream. +public struct ErrorChunk: TypraModel { + public static let shorthandProperty: String? = nil + public var kind: String = "error" + public var message: String = "" + + public init(kind: String = "error", message: String = "") { + self.kind = kind + self.message = message + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ErrorChunk { + let object = try TypraRuntime.object(data, typeName: "ErrorChunk") + var instance = ErrorChunk() + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "error" + } + if let value = object["message"] { + instance.message = try TypraRuntime.string(value, field: "message") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["kind"] = self.kind + result["message"] = self.message + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ErrorChunk { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ErrorChunk"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ErrorChunk { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ErrorChunk"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/thinking_event_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/thinking_event_payload.swift new file mode 100644 index 000000000..ba635cc0b --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/thinking_event_payload.swift @@ -0,0 +1,45 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for "thinking" events — reasoning/chain-of-thought tokens. +public struct ThinkingEventPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var token: String = "" + + public init(token: String = "") { + self.token = token + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ThinkingEventPayload { + let object = try TypraRuntime.object(data, typeName: "ThinkingEventPayload") + var instance = ThinkingEventPayload() + if let value = object["token"] { + instance.token = try TypraRuntime.string(value, field: "token") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["token"] = self.token + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ThinkingEventPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ThinkingEventPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ThinkingEventPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ThinkingEventPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/token_event_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/token_event_payload.swift new file mode 100644 index 000000000..17a3a736a --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/token_event_payload.swift @@ -0,0 +1,45 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for "token" events — a single text token streamed from the LLM. +public struct TokenEventPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var token: String = "" + + public init(token: String = "") { + self.token = token + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> TokenEventPayload { + let object = try TypraRuntime.object(data, typeName: "TokenEventPayload") + var instance = TokenEventPayload() + if let value = object["token"] { + instance.token = try TypraRuntime.string(value, field: "token") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["token"] = self.token + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> TokenEventPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "TokenEventPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> TokenEventPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "TokenEventPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/tool_call_complete_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/tool_call_complete_payload.swift new file mode 100644 index 000000000..da726fdda --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/tool_call_complete_payload.swift @@ -0,0 +1,83 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for "tool_call_complete" events — a tool dispatch finished. +public struct ToolCallCompletePayload: TypraModel { + public static let shorthandProperty: String? = nil + public var id: String? = nil + public var name: String = "" + public var success: Bool = false + public var result: ToolResult? = nil + public var durationMs: Double? = nil + public var errorKind: String? = nil + + public init(id: String? = nil, name: String = "", success: Bool = false, result: ToolResult? = nil, durationMs: Double? = nil, errorKind: String? = nil) { + self.id = id + self.name = name + self.success = success + self.result = result + self.durationMs = durationMs + self.errorKind = errorKind + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ToolCallCompletePayload { + let object = try TypraRuntime.object(data, typeName: "ToolCallCompletePayload") + var instance = ToolCallCompletePayload() + if let value = object["id"] { + instance.id = try TypraRuntime.string(value, field: "id") + } + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + if let value = object["success"] { + instance.success = try TypraRuntime.bool(value, field: "success") + } + if let value = object["result"] { + instance.result = try ToolResult.load(value, context: context.at("result")) + } + if let value = object["durationMs"] { + instance.durationMs = try TypraRuntime.double(value, field: "durationMs") + } + if let value = object["errorKind"] { + instance.errorKind = try TypraRuntime.string(value, field: "errorKind") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.id { + result["id"] = value + } + result["name"] = self.name + result["success"] = self.success + if let value = self.result { + result["result"] = try value.save(context) + } + if let value = self.durationMs { + result["durationMs"] = value + } + if let value = self.errorKind { + result["errorKind"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ToolCallCompletePayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ToolCallCompletePayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ToolCallCompletePayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ToolCallCompletePayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/tool_call_start_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/tool_call_start_payload.swift new file mode 100644 index 000000000..c6b998790 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/tool_call_start_payload.swift @@ -0,0 +1,59 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for "tool_call_start" events — the LLM has requested a tool call. +public struct ToolCallStartPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var id: String? = nil + public var name: String = "" + public var arguments: String = "" + + public init(id: String? = nil, name: String = "", arguments: String = "") { + self.id = id + self.name = name + self.arguments = arguments + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ToolCallStartPayload { + let object = try TypraRuntime.object(data, typeName: "ToolCallStartPayload") + var instance = ToolCallStartPayload() + if let value = object["id"] { + instance.id = try TypraRuntime.string(value, field: "id") + } + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + if let value = object["arguments"] { + instance.arguments = try TypraRuntime.string(value, field: "arguments") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.id { + result["id"] = value + } + result["name"] = self.name + result["arguments"] = self.arguments + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ToolCallStartPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ToolCallStartPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ToolCallStartPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ToolCallStartPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/tool_execution_complete_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/tool_execution_complete_payload.swift new file mode 100644 index 000000000..b62a9c5ae --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/tool_execution_complete_payload.swift @@ -0,0 +1,115 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for "tool_execution_complete" events — a concrete host tool execution finished. +public struct ToolExecutionCompletePayload: TypraModel { + public static let shorthandProperty: String? = nil + public var requestId: String? = nil + public var toolCallId: String? = nil + public var toolName: String = "" + public var success: Bool = false + public var result: Any? = nil + public var exitCode: Int32? = nil + public var durationMs: Double? = nil + public var errorKind: String? = nil + public var telemetry: [String: Any]? = nil + public var redaction: RedactionMetadata? = nil + + public init(requestId: String? = nil, toolCallId: String? = nil, toolName: String = "", success: Bool = false, result: Any? = nil, exitCode: Int32? = nil, durationMs: Double? = nil, errorKind: String? = nil, telemetry: [String: Any]? = nil, redaction: RedactionMetadata? = nil) { + self.requestId = requestId + self.toolCallId = toolCallId + self.toolName = toolName + self.success = success + self.result = result + self.exitCode = exitCode + self.durationMs = durationMs + self.errorKind = errorKind + self.telemetry = telemetry + self.redaction = redaction + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ToolExecutionCompletePayload { + let object = try TypraRuntime.object(data, typeName: "ToolExecutionCompletePayload") + var instance = ToolExecutionCompletePayload() + if let value = object["requestId"] { + instance.requestId = try TypraRuntime.string(value, field: "requestId") + } + if let value = object["toolCallId"] { + instance.toolCallId = try TypraRuntime.string(value, field: "toolCallId") + } + if let value = object["toolName"] { + instance.toolName = try TypraRuntime.string(value, field: "toolName") + } + if let value = object["success"] { + instance.success = try TypraRuntime.bool(value, field: "success") + } + if let value = object["result"] { + instance.result = value + } + if let value = object["exitCode"] { + instance.exitCode = try TypraRuntime.int32(value, field: "exitCode") + } + if let value = object["durationMs"] { + instance.durationMs = try TypraRuntime.double(value, field: "durationMs") + } + if let value = object["errorKind"] { + instance.errorKind = try TypraRuntime.string(value, field: "errorKind") + } + if let value = object["telemetry"] { + instance.telemetry = try TypraRuntime.dictionary(value, field: "telemetry") + } + if let value = object["redaction"] { + instance.redaction = try RedactionMetadata.load(value, context: context.at("redaction")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.requestId { + result["requestId"] = value + } + if let value = self.toolCallId { + result["toolCallId"] = value + } + result["toolName"] = self.toolName + result["success"] = self.success + if let value = self.result { + result["result"] = value + } + if let value = self.exitCode { + result["exitCode"] = value + } + if let value = self.durationMs { + result["durationMs"] = value + } + if let value = self.errorKind { + result["errorKind"] = value + } + if let value = self.telemetry { + result["telemetry"] = value + } + if let value = self.redaction { + result["redaction"] = try value.save(context) + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ToolExecutionCompletePayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ToolExecutionCompletePayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ToolExecutionCompletePayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ToolExecutionCompletePayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/tool_execution_start_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/tool_execution_start_payload.swift new file mode 100644 index 000000000..16441b911 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/tool_execution_start_payload.swift @@ -0,0 +1,85 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for "tool_execution_start" events — the host is about to execute a concrete tool request. This is distinct from "tool_call_start", which records the model requesting a tool. Tool execution events capture the harness-side action after policy and permission checks. +public struct ToolExecutionStartPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var requestId: String? = nil + public var toolCallId: String? = nil + public var toolName: String = "" + public var arguments: [String: Any]? = nil + public var workingDirectory: String? = nil + public var redaction: RedactionMetadata? = nil + + public init(requestId: String? = nil, toolCallId: String? = nil, toolName: String = "", arguments: [String: Any]? = nil, workingDirectory: String? = nil, redaction: RedactionMetadata? = nil) { + self.requestId = requestId + self.toolCallId = toolCallId + self.toolName = toolName + self.arguments = arguments + self.workingDirectory = workingDirectory + self.redaction = redaction + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ToolExecutionStartPayload { + let object = try TypraRuntime.object(data, typeName: "ToolExecutionStartPayload") + var instance = ToolExecutionStartPayload() + if let value = object["requestId"] { + instance.requestId = try TypraRuntime.string(value, field: "requestId") + } + if let value = object["toolCallId"] { + instance.toolCallId = try TypraRuntime.string(value, field: "toolCallId") + } + if let value = object["toolName"] { + instance.toolName = try TypraRuntime.string(value, field: "toolName") + } + if let value = object["arguments"] { + instance.arguments = try TypraRuntime.dictionary(value, field: "arguments") + } + if let value = object["workingDirectory"] { + instance.workingDirectory = try TypraRuntime.string(value, field: "workingDirectory") + } + if let value = object["redaction"] { + instance.redaction = try RedactionMetadata.load(value, context: context.at("redaction")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.requestId { + result["requestId"] = value + } + if let value = self.toolCallId { + result["toolCallId"] = value + } + result["toolName"] = self.toolName + if let value = self.arguments { + result["arguments"] = value + } + if let value = self.workingDirectory { + result["workingDirectory"] = value + } + if let value = self.redaction { + result["redaction"] = try value.save(context) + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ToolExecutionStartPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ToolExecutionStartPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ToolExecutionStartPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ToolExecutionStartPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/tool_result_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/tool_result_payload.swift new file mode 100644 index 000000000..66549f6ab --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/tool_result_payload.swift @@ -0,0 +1,54 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for "tool_result" events — a tool has returned its result. +public struct ToolResultPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var name: String = "" + public var result: ToolResult = ToolResult() + + public init(name: String = "", result: ToolResult = ToolResult()) { + self.name = name + self.result = result + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ToolResultPayload { + let object = try TypraRuntime.object(data, typeName: "ToolResultPayload") + var instance = ToolResultPayload() + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + if object["result"] == nil || object["result"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("result").path + ": missing required field") + } + if let value = object["result"] { + instance.result = try ToolResult.load(value, context: context.at("result")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["name"] = self.name + result["result"] = try self.result.save(context) + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ToolResultPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ToolResultPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ToolResultPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ToolResultPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/trajectory_event.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/trajectory_event.swift new file mode 100644 index 000000000..d9a463d84 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/trajectory_event.swift @@ -0,0 +1,109 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// A compact, replay-oriented record of one harness-side action or observation. +public struct TrajectoryEvent: TypraModel { + public static let shorthandProperty: String? = nil + public var id: String? = nil + public var sessionId: String? = nil + public var turnId: String? = nil + public var toolCallId: String? = nil + public var turnIndex: Int32? = nil + public var eventType: String = "" + public var data: [String: Any]? = nil + public var createdAt: String? = nil + public var redaction: RedactionMetadata? = nil + + public init(id: String? = nil, sessionId: String? = nil, turnId: String? = nil, toolCallId: String? = nil, turnIndex: Int32? = nil, eventType: String = "", data: [String: Any]? = nil, createdAt: String? = nil, redaction: RedactionMetadata? = nil) { + self.id = id + self.sessionId = sessionId + self.turnId = turnId + self.toolCallId = toolCallId + self.turnIndex = turnIndex + self.eventType = eventType + self.data = data + self.createdAt = createdAt + self.redaction = redaction + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> TrajectoryEvent { + let object = try TypraRuntime.object(data, typeName: "TrajectoryEvent") + var instance = TrajectoryEvent() + if let value = object["id"] { + instance.id = try TypraRuntime.string(value, field: "id") + } + if let value = object["sessionId"] { + instance.sessionId = try TypraRuntime.string(value, field: "sessionId") + } + if let value = object["turnId"] { + instance.turnId = try TypraRuntime.string(value, field: "turnId") + } + if let value = object["toolCallId"] { + instance.toolCallId = try TypraRuntime.string(value, field: "toolCallId") + } + if let value = object["turnIndex"] { + instance.turnIndex = try TypraRuntime.int32(value, field: "turnIndex") + } + if let value = object["eventType"] { + instance.eventType = try TypraRuntime.string(value, field: "eventType") + } + if let value = object["data"] { + instance.data = try TypraRuntime.dictionary(value, field: "data") + } + if let value = object["createdAt"] { + instance.createdAt = try TypraRuntime.string(value, field: "createdAt") + } + if let value = object["redaction"] { + instance.redaction = try RedactionMetadata.load(value, context: context.at("redaction")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.id { + result["id"] = value + } + if let value = self.sessionId { + result["sessionId"] = value + } + if let value = self.turnId { + result["turnId"] = value + } + if let value = self.toolCallId { + result["toolCallId"] = value + } + if let value = self.turnIndex { + result["turnIndex"] = value + } + result["eventType"] = self.eventType + if let value = self.data { + result["data"] = value + } + if let value = self.createdAt { + result["createdAt"] = value + } + if let value = self.redaction { + result["redaction"] = try value.save(context) + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> TrajectoryEvent { + return try load(TypraRuntime.jsonObject(from: json, typeName: "TrajectoryEvent"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> TrajectoryEvent { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "TrajectoryEvent"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/turn_end_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/turn_end_payload.swift new file mode 100644 index 000000000..2979e4ea0 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/turn_end_payload.swift @@ -0,0 +1,85 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum TurnStatus: String, Codable, CaseIterable { + case success = "success" + case error = "error" + case cancelled = "cancelled" + public static func parse(_ value: String) throws -> TurnStatus { + switch value { + case "success": return .success + case "error": return .error + case "cancelled": return .cancelled + default: throw TypraRuntimeError.invalidEnum(type: "TurnStatus", value: value) + } + } +} + +/// Payload for "turn_end" events — a turn has completed. +public struct TurnEndPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var iterations: Int32? = nil + public var status: TurnStatus? = nil + public var response: Any? = nil + public var durationMs: Double? = nil + + public init(iterations: Int32? = nil, status: TurnStatus? = nil, response: Any? = nil, durationMs: Double? = nil) { + self.iterations = iterations + self.status = status + self.response = response + self.durationMs = durationMs + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> TurnEndPayload { + let object = try TypraRuntime.object(data, typeName: "TurnEndPayload") + var instance = TurnEndPayload() + if let value = object["iterations"] { + instance.iterations = try TypraRuntime.int32(value, field: "iterations") + } + if let value = object["status"] { + instance.status = try TurnStatus.parse(try TypraRuntime.string(value, field: "status")) + } + if let value = object["response"] { + instance.response = value + } + if let value = object["durationMs"] { + instance.durationMs = try TypraRuntime.double(value, field: "durationMs") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.iterations { + result["iterations"] = value + } + if let value = self.status { + result["status"] = value.rawValue + } + if let value = self.response { + result["response"] = value + } + if let value = self.durationMs { + result["durationMs"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> TurnEndPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "TurnEndPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> TurnEndPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "TurnEndPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/turn_event.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/turn_event.swift new file mode 100644 index 000000000..2ac755edd --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/turn_event.swift @@ -0,0 +1,151 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum TurnEventType: String, Codable, CaseIterable { + case turnStart = "turn_start" + case turnEnd = "turn_end" + case llmStart = "llm_start" + case llmComplete = "llm_complete" + case retry = "retry" + case permissionRequested = "permission_requested" + case permissionCompleted = "permission_completed" + case token = "token" + case thinking = "thinking" + case toolCallStart = "tool_call_start" + case toolCallComplete = "tool_call_complete" + case toolExecutionStart = "tool_execution_start" + case toolExecutionComplete = "tool_execution_complete" + case toolResult = "tool_result" + case hookStart = "hook_start" + case hookEnd = "hook_end" + case status = "status" + case messagesUpdated = "messages_updated" + case done = "done" + case error = "error" + case cancelled = "cancelled" + case compactionStart = "compaction_start" + case compactionComplete = "compaction_complete" + case compactionFailed = "compaction_failed" + public static func parse(_ value: String) throws -> TurnEventType { + switch value { + case "turn_start": return .turnStart + case "turn_end": return .turnEnd + case "llm_start": return .llmStart + case "llm_complete": return .llmComplete + case "retry": return .retry + case "permission_requested": return .permissionRequested + case "permission_completed": return .permissionCompleted + case "token": return .token + case "thinking": return .thinking + case "tool_call_start": return .toolCallStart + case "tool_call_complete": return .toolCallComplete + case "tool_execution_start": return .toolExecutionStart + case "tool_execution_complete": return .toolExecutionComplete + case "tool_result": return .toolResult + case "hook_start": return .hookStart + case "hook_end": return .hookEnd + case "status": return .status + case "messages_updated": return .messagesUpdated + case "done": return .done + case "error": return .error + case "cancelled": return .cancelled + case "compaction_start": return .compactionStart + case "compaction_complete": return .compactionComplete + case "compaction_failed": return .compactionFailed + default: throw TypraRuntimeError.invalidEnum(type: "TurnEventType", value: value) + } + } +} + +/// A canonical event envelope emitted by the turn harness. The payload is kept JSON-shaped so runtimes can load all events even when newer payload types are added; event-specific typed payload models below define the canonical shapes. +public struct TurnEvent: TypraModel { + public static let shorthandProperty: String? = nil + public var id: String = "" + public var type: TurnEventType = (try! TurnEventType.parse("turn_start")) + public var timestamp: String = "" + public var turnId: String? = nil + public var iteration: Int32? = nil + public var parentId: String? = nil + public var spanId: String? = nil + public var payload: [String: Any] = [:] + + public init(id: String = "", type: TurnEventType = (try! TurnEventType.parse("turn_start")), timestamp: String = "", turnId: String? = nil, iteration: Int32? = nil, parentId: String? = nil, spanId: String? = nil, payload: [String: Any] = [:]) { + self.id = id + self.type = type + self.timestamp = timestamp + self.turnId = turnId + self.iteration = iteration + self.parentId = parentId + self.spanId = spanId + self.payload = payload + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> TurnEvent { + let object = try TypraRuntime.object(data, typeName: "TurnEvent") + var instance = TurnEvent() + if let value = object["id"] { + instance.id = try TypraRuntime.string(value, field: "id") + } + if let value = object["type"] { + instance.type = try TurnEventType.parse(try TypraRuntime.string(value, field: "type")) + } + if let value = object["timestamp"] { + instance.timestamp = try TypraRuntime.string(value, field: "timestamp") + } + if let value = object["turnId"] { + instance.turnId = try TypraRuntime.string(value, field: "turnId") + } + if let value = object["iteration"] { + instance.iteration = try TypraRuntime.int32(value, field: "iteration") + } + if let value = object["parentId"] { + instance.parentId = try TypraRuntime.string(value, field: "parentId") + } + if let value = object["spanId"] { + instance.spanId = try TypraRuntime.string(value, field: "spanId") + } + if let value = object["payload"] { + instance.payload = try TypraRuntime.dictionary(value, field: "payload") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["id"] = self.id + result["type"] = self.type.rawValue + result["timestamp"] = self.timestamp + if let value = self.turnId { + result["turnId"] = value + } + if let value = self.iteration { + result["iteration"] = value + } + if let value = self.parentId { + result["parentId"] = value + } + if let value = self.spanId { + result["spanId"] = value + } + result["payload"] = self.payload + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> TurnEvent { + return try load(TypraRuntime.jsonObject(from: json, typeName: "TurnEvent"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> TurnEvent { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "TurnEvent"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/turn_start_payload.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/turn_start_payload.swift new file mode 100644 index 000000000..d0c154e0e --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/turn_start_payload.swift @@ -0,0 +1,63 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Payload for "turn_start" events — a turn is beginning. +public struct TurnStartPayload: TypraModel { + public static let shorthandProperty: String? = nil + public var agent: String? = nil + public var inputs: [String: Any]? = nil + public var maxIterations: Int32? = nil + + public init(agent: String? = nil, inputs: [String: Any]? = nil, maxIterations: Int32? = nil) { + self.agent = agent + self.inputs = inputs + self.maxIterations = maxIterations + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> TurnStartPayload { + let object = try TypraRuntime.object(data, typeName: "TurnStartPayload") + var instance = TurnStartPayload() + if let value = object["agent"] { + instance.agent = try TypraRuntime.string(value, field: "agent") + } + if let value = object["inputs"] { + instance.inputs = try TypraRuntime.dictionary(value, field: "inputs") + } + if let value = object["maxIterations"] { + instance.maxIterations = try TypraRuntime.int32(value, field: "maxIterations") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.agent { + result["agent"] = value + } + if let value = self.inputs { + result["inputs"] = value + } + if let value = self.maxIterations { + result["maxIterations"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> TurnStartPayload { + return try load(TypraRuntime.jsonObject(from: json, typeName: "TurnStartPayload"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> TurnStartPayload { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "TurnStartPayload"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/turn_summary.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/turn_summary.swift new file mode 100644 index 000000000..e77d7d1a9 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/turn_summary.swift @@ -0,0 +1,97 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Summary statistics for a completed turn trace. +public struct TurnSummary: TypraModel { + public static let shorthandProperty: String? = nil + public var turnId: String = "" + public var status: String = "" + public var iterations: Int32 = 0 + public var llmCalls: Int32? = nil + public var toolCalls: Int32? = nil + public var retries: Int32? = nil + public var usage: TokenUsage? = nil + public var durationMs: Double? = nil + + public init(turnId: String = "", status: String = "", iterations: Int32 = 0, llmCalls: Int32? = nil, toolCalls: Int32? = nil, retries: Int32? = nil, usage: TokenUsage? = nil, durationMs: Double? = nil) { + self.turnId = turnId + self.status = status + self.iterations = iterations + self.llmCalls = llmCalls + self.toolCalls = toolCalls + self.retries = retries + self.usage = usage + self.durationMs = durationMs + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> TurnSummary { + let object = try TypraRuntime.object(data, typeName: "TurnSummary") + var instance = TurnSummary() + if let value = object["turnId"] { + instance.turnId = try TypraRuntime.string(value, field: "turnId") + } + if let value = object["status"] { + instance.status = try TypraRuntime.string(value, field: "status") + } + if let value = object["iterations"] { + instance.iterations = try TypraRuntime.int32(value, field: "iterations") + } + if let value = object["llmCalls"] { + instance.llmCalls = try TypraRuntime.int32(value, field: "llmCalls") + } + if let value = object["toolCalls"] { + instance.toolCalls = try TypraRuntime.int32(value, field: "toolCalls") + } + if let value = object["retries"] { + instance.retries = try TypraRuntime.int32(value, field: "retries") + } + if let value = object["usage"] { + instance.usage = try TokenUsage.load(value, context: context.at("usage")) + } + if let value = object["durationMs"] { + instance.durationMs = try TypraRuntime.double(value, field: "durationMs") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["turnId"] = self.turnId + result["status"] = self.status + result["iterations"] = self.iterations + if let value = self.llmCalls { + result["llmCalls"] = value + } + if let value = self.toolCalls { + result["toolCalls"] = value + } + if let value = self.retries { + result["retries"] = value + } + if let value = self.usage { + result["usage"] = try value.save(context) + } + if let value = self.durationMs { + result["durationMs"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> TurnSummary { + return try load(TypraRuntime.jsonObject(from: json, typeName: "TurnSummary"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> TurnSummary { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "TurnSummary"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/turn_trace.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/turn_trace.swift new file mode 100644 index 000000000..40cc73512 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/turn_trace.swift @@ -0,0 +1,78 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Portable JSONL/replay container for a recorded turn harness run. +public struct TurnTrace: TypraModel { + public static let shorthandProperty: String? = nil + public var version: String = "1" + public var runtime: String? = nil + public var promptyVersion: String? = nil + public var events: [TurnEvent] = [] + public var summary: TurnSummary? = nil + + public init(version: String = "1", runtime: String? = nil, promptyVersion: String? = nil, events: [TurnEvent] = [], summary: TurnSummary? = nil) { + self.version = version + self.runtime = runtime + self.promptyVersion = promptyVersion + self.events = events + self.summary = summary + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> TurnTrace { + let object = try TypraRuntime.object(data, typeName: "TurnTrace") + var instance = TurnTrace() + if let value = object["version"] { + instance.version = try TypraRuntime.string(value, field: "version") + } + else { + instance.version = "1" + } + if let value = object["runtime"] { + instance.runtime = try TypraRuntime.string(value, field: "runtime") + } + if let value = object["promptyVersion"] { + instance.promptyVersion = try TypraRuntime.string(value, field: "promptyVersion") + } + if let value = object["events"] { + instance.events = try TypraRuntime.array(value, field: "events").enumerated().map { try TurnEvent.load($1, context: context.at("events").atIndex($0)) } + } + if let value = object["summary"] { + instance.summary = try TurnSummary.load(value, context: context.at("summary")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["version"] = self.version + if let value = self.runtime { + result["runtime"] = value + } + if let value = self.promptyVersion { + result["promptyVersion"] = value + } + result["events"] = try self.events.map { try $0.save(context) } + if let value = self.summary { + result["summary"] = try value.save(context) + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> TurnTrace { + return try load(TypraRuntime.jsonObject(from: json, typeName: "TurnTrace"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> TurnTrace { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "TurnTrace"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Guardrails/guardrail_result.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Guardrails/guardrail_result.swift new file mode 100644 index 000000000..4aa6b0557 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Guardrails/guardrail_result.swift @@ -0,0 +1,73 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// The result of a guardrail evaluation. Guardrails are safety checks that run at specific phases of the agent loop and can allow, deny, or rewrite content. +public struct GuardrailResult: TypraModel { + public static let shorthandProperty: String? = nil + public var allowed: Bool = false + public var reason: String? = nil + public var rewrite: Any? = nil + + public init(allowed: Bool = false, reason: String? = nil, rewrite: Any? = nil) { + self.allowed = allowed + self.reason = reason + self.rewrite = rewrite + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> GuardrailResult { + let object = try TypraRuntime.object(data, typeName: "GuardrailResult") + var instance = GuardrailResult() + if let value = object["allowed"] { + instance.allowed = try TypraRuntime.bool(value, field: "allowed") + } + if let value = object["reason"] { + instance.reason = try TypraRuntime.string(value, field: "reason") + } + if let value = object["rewrite"] { + instance.rewrite = value + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["allowed"] = self.allowed + if let value = self.reason { + result["reason"] = value + } + if let value = self.rewrite { + result["rewrite"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> GuardrailResult { + return try load(TypraRuntime.jsonObject(from: json, typeName: "GuardrailResult"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> GuardrailResult { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "GuardrailResult"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } + + public static func rewrite(rewrite: Any) -> GuardrailResult { + return GuardrailResult(allowed: true, rewrite: rewrite) + } + + public static func deny(reason: String) -> GuardrailResult { + return GuardrailResult(allowed: false, reason: reason) + } + + public static func allow() -> GuardrailResult { + return GuardrailResult(allowed: true) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Memory/memory_entry.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Memory/memory_entry.swift new file mode 100644 index 000000000..a9b855806 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Memory/memory_entry.swift @@ -0,0 +1,87 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum MemoryCategory: String, Codable, CaseIterable { + case core = "core" + case archival = "archival" + case insight = "insight" + public static func parse(_ value: String) throws -> MemoryCategory { + switch value { + case "core": return .core + case "archival": return .archival + case "insight": return .insight + default: throw TypraRuntimeError.invalidEnum(type: "MemoryCategory", value: value) + } + } +} + +/// A single agent memory — the canonical, host-neutral unit of agent memory. `content` is the memory text, `category` places it in a general tier, `createdAt` records when the memory was formed (intrinsic, portable data), and `tags` are general labels used for keyword recall, grouping, and core deduplication. Host-specific associations (for example a session association) are expressed through the general `tags` field by convention — e.g. a `session:{id}` tag — never as a canonical field. A host needing per-entry bookkeeping, a stable id, or a stored embedding vector layers it in host storage; those are not canonical fields. +public struct MemoryEntry: TypraModel { + public static let shorthandProperty: String? = nil + public var content: String = "" + public var category: MemoryCategory = (try! MemoryCategory.parse("core")) + public var createdAt: String? = nil + public var tags: [String]? = nil + + public init(content: String = "", category: MemoryCategory = (try! MemoryCategory.parse("core")), createdAt: String? = nil, tags: [String]? = nil) { + self.content = content + self.category = category + self.createdAt = createdAt + self.tags = tags + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> MemoryEntry { + let object = try TypraRuntime.object(data, typeName: "MemoryEntry") + var instance = MemoryEntry() + if let value = object["content"] { + instance.content = try TypraRuntime.string(value, field: "content") + } + else { + instance.content = "" + } + if let value = object["category"] { + instance.category = try MemoryCategory.parse(try TypraRuntime.string(value, field: "category")) + } + else { + instance.category = (try! MemoryCategory.parse("core")) + } + if let value = object["createdAt"] { + instance.createdAt = try TypraRuntime.string(value, field: "createdAt") + } + if let value = object["tags"] { + instance.tags = try TypraRuntime.array(value, field: "tags").map { try TypraRuntime.string($0, field: "tags") } + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["content"] = self.content + result["category"] = self.category.rawValue + if let value = self.createdAt { + result["createdAt"] = value + } + if let value = self.tags { + result["tags"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> MemoryEntry { + return try load(TypraRuntime.jsonObject(from: json, typeName: "MemoryEntry"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> MemoryEntry { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "MemoryEntry"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Memory/memory_store.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Memory/memory_store.swift new file mode 100644 index 000000000..ee99f1d69 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Memory/memory_store.swift @@ -0,0 +1,45 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// A whole-store snapshot of agent memory. The canonical persisted shape a host loads and saves as one unit. A host backend implements only load/save of this snapshot; the engine owns the deterministic recall, formatting, tiered injection, eviction, and entry-mutation logic on top of it. +public struct MemoryStore: TypraModel { + public static let shorthandProperty: String? = nil + public var entries: [MemoryEntry] = [] + + public init(entries: [MemoryEntry] = []) { + self.entries = entries + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> MemoryStore { + let object = try TypraRuntime.object(data, typeName: "MemoryStore") + var instance = MemoryStore() + if let value = object["entries"] { + instance.entries = try TypraRuntime.array(value, field: "entries").enumerated().map { try MemoryEntry.load($1, context: context.at("entries").atIndex($0)) } + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["entries"] = try self.entries.map { try $0.save(context) } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> MemoryStore { + return try load(TypraRuntime.jsonObject(from: json, typeName: "MemoryStore"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> MemoryStore { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "MemoryStore"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/ai_resource_info.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/ai_resource_info.swift new file mode 100644 index 000000000..1ed803bcf --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/ai_resource_info.swift @@ -0,0 +1,118 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// An AI service resource that can host models or projects. +public struct AiResourceInfo: TypraModel { + public static let shorthandProperty: String? = nil + public var name: String = "" + public var kind: String = "" + public var endpoint: String = "" + public var location: String = "" + public var resourceGroup: String = "" + public var serviceUrl: String? = nil + + public init(name: String = "", kind: String = "", endpoint: String = "", location: String = "", resourceGroup: String = "", serviceUrl: String? = nil) { + self.name = name + self.kind = kind + self.endpoint = endpoint + self.location = location + self.resourceGroup = resourceGroup + self.serviceUrl = serviceUrl + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> AiResourceInfo { + let object = try TypraRuntime.object(data, typeName: "AiResourceInfo") + var instance = AiResourceInfo() + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + if let value = object["endpoint"] { + instance.endpoint = try TypraRuntime.string(value, field: "endpoint") + } + if let value = object["location"] { + instance.location = try TypraRuntime.string(value, field: "location") + } + if let value = object["resourceGroup"] { + instance.resourceGroup = try TypraRuntime.string(value, field: "resourceGroup") + } + if let value = object["serviceUrl"] { + instance.serviceUrl = try TypraRuntime.string(value, field: "serviceUrl") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["name"] = self.name + result["kind"] = self.kind + result["endpoint"] = self.endpoint + result["location"] = self.location + result["resourceGroup"] = self.resourceGroup + if let value = self.serviceUrl { + result["serviceUrl"] = value + } + return result + } + + public func toWire(_ provider: String, context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + let wireNameName: String? + switch provider { + case "foundry": wireNameName = "name" + default: wireNameName = nil + } + if let wireKey = wireNameName { result[wireKey] = self.name } + let wireNameKind: String? + switch provider { + case "foundry": wireNameKind = "kind" + default: wireNameKind = nil + } + if let wireKey = wireNameKind { result[wireKey] = self.kind } + let wireNameEndpoint: String? + switch provider { + case "foundry": wireNameEndpoint = "endpoint" + default: wireNameEndpoint = nil + } + if let wireKey = wireNameEndpoint { result[wireKey] = self.endpoint } + let wireNameLocation: String? + switch provider { + case "foundry": wireNameLocation = "location" + default: wireNameLocation = nil + } + if let wireKey = wireNameLocation { result[wireKey] = self.location } + let wireNameResourceGroup: String? + switch provider { + case "foundry": wireNameResourceGroup = "resource_group" + default: wireNameResourceGroup = nil + } + if let wireKey = wireNameResourceGroup { result[wireKey] = self.resourceGroup } + let wireNameServiceUrl: String? + switch provider { + case "foundry": wireNameServiceUrl = "foundry_url" + default: wireNameServiceUrl = nil + } + if let wireKey = wireNameServiceUrl, let value = self.serviceUrl { result[wireKey] = value } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> AiResourceInfo { + return try load(TypraRuntime.jsonObject(from: json, typeName: "AiResourceInfo"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> AiResourceInfo { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "AiResourceInfo"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/invocation_usage.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/invocation_usage.swift new file mode 100644 index 000000000..5550e090b --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/invocation_usage.swift @@ -0,0 +1,82 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Complete cumulative token usage for one completed model invocation. Providers emit this value at most once per invocation. `totalTokens` uses the provider total when available; otherwise the provider adapter computes it as `inputTokens + outputTokens`. +public struct InvocationUsage: TypraModel { + public static let shorthandProperty: String? = nil + public var inputTokens: Int64 = 0 + public var outputTokens: Int64 = 0 + public var totalTokens: Int64 = 0 + + public init(inputTokens: Int64 = 0, outputTokens: Int64 = 0, totalTokens: Int64 = 0) { + self.inputTokens = inputTokens + self.outputTokens = outputTokens + self.totalTokens = totalTokens + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> InvocationUsage { + let object = try TypraRuntime.object(data, typeName: "InvocationUsage") + var instance = InvocationUsage() + if let value = object["inputTokens"] { + instance.inputTokens = try TypraRuntime.int64(value, field: "inputTokens") + } + if let value = object["outputTokens"] { + instance.outputTokens = try TypraRuntime.int64(value, field: "outputTokens") + } + if let value = object["totalTokens"] { + instance.totalTokens = try TypraRuntime.int64(value, field: "totalTokens") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["inputTokens"] = self.inputTokens + result["outputTokens"] = self.outputTokens + result["totalTokens"] = self.totalTokens + return result + } + + public func toWire(_ provider: String, context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + let wireNameInputTokens: String? + switch provider { + case "openai": wireNameInputTokens = "prompt_tokens" + case "anthropic": wireNameInputTokens = "input_tokens" + default: wireNameInputTokens = nil + } + if let wireKey = wireNameInputTokens { result[wireKey] = self.inputTokens } + let wireNameOutputTokens: String? + switch provider { + case "openai": wireNameOutputTokens = "completion_tokens" + case "anthropic": wireNameOutputTokens = "output_tokens" + default: wireNameOutputTokens = nil + } + if let wireKey = wireNameOutputTokens { result[wireKey] = self.outputTokens } + let wireNameTotalTokens: String? + switch provider { + case "openai": wireNameTotalTokens = "total_tokens" + default: wireNameTotalTokens = nil + } + if let wireKey = wireNameTotalTokens { result[wireKey] = self.totalTokens } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> InvocationUsage { + return try load(TypraRuntime.jsonObject(from: json, typeName: "InvocationUsage"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> InvocationUsage { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "InvocationUsage"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/model.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/model.swift new file mode 100644 index 000000000..798a0ed3d --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/model.swift @@ -0,0 +1,103 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public struct ApiType: RawRepresentable, Equatable, Hashable, Codable { + public let rawValue: String + public init(rawValue: String) { self.rawValue = rawValue } + public static let chat = ApiType(rawValue: "chat") + public static let embedding = ApiType(rawValue: "embedding") + public static let image = ApiType(rawValue: "image") + public static let responses = ApiType(rawValue: "responses") + public static func parse(_ value: String) throws -> ApiType { + switch value { + case "chat": return .chat + case "embedding": return .embedding + case "image": return .image + case "responses": return .responses + default: return ApiType(rawValue: value) + } + } +} + +/// Model for defining the structure and behavior of AI agents. This model includes properties for specifying the model's provider, connection details, and various options. It allows for flexible configuration of AI models to suit different use cases and requirements. +public struct Model: TypraModel { + public static let shorthandProperty: String? = "id" + public var id: String = "" + public var provider: String? = nil + public var apiType: ApiType? = nil + public var connection: Connection? = nil + public var options: ModelOptions? = nil + + public init(id: String = "", provider: String? = nil, apiType: ApiType? = nil, connection: Connection? = nil, options: ModelOptions? = nil) { + self.id = id + self.provider = provider + self.apiType = apiType + self.connection = connection + self.options = options + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> Model { + if let scalar = data as? String { + var instance = Model() + instance.id = try TypraRuntime.string(scalar, field: "id") + return instance + } + let object = try TypraRuntime.object(data, typeName: "Model") + var instance = Model() + if let value = object["id"] { + instance.id = try TypraRuntime.string(value, field: "id") + } + else { + instance.id = "" + } + if let value = object["provider"] { + instance.provider = try TypraRuntime.string(value, field: "provider") + } + if let value = object["apiType"] { + instance.apiType = try ApiType.parse(try TypraRuntime.string(value, field: "apiType")) + } + if let value = object["connection"] { + instance.connection = try Connection.load(value, context: context.at("connection")) + } + if let value = object["options"] { + instance.options = try ModelOptions.load(value, context: context.at("options")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["id"] = self.id + if let value = self.provider { + result["provider"] = value + } + if let value = self.apiType { + result["apiType"] = value.rawValue + } + if let value = self.connection { + result["connection"] = try value.save(context) + } + if let value = self.options { + result["options"] = try value.save(context) + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> Model { + return try load(TypraRuntime.jsonObject(from: json, typeName: "Model"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> Model { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "Model"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/model_info.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/model_info.swift new file mode 100644 index 000000000..1c7c17a14 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/model_info.swift @@ -0,0 +1,135 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Information about a model available from a provider. Used by provider-level model discovery to report which models are available and their capabilities. Not all providers return all fields — implementations SHOULD populate as many fields as the provider's API supports and MAY enrich sparse results from a built-in lookup table of known models. +public struct ModelInfo: TypraModel { + public static let shorthandProperty: String? = nil + public var id: String = "" + public var displayName: String? = nil + public var ownedBy: String? = nil + public var contextWindow: Int32? = nil + public var inputModalities: [String]? = nil + public var outputModalities: [String]? = nil + public var additionalProperties: [String: Any]? = nil + + public init(id: String = "", displayName: String? = nil, ownedBy: String? = nil, contextWindow: Int32? = nil, inputModalities: [String]? = nil, outputModalities: [String]? = nil, additionalProperties: [String: Any]? = nil) { + self.id = id + self.displayName = displayName + self.ownedBy = ownedBy + self.contextWindow = contextWindow + self.inputModalities = inputModalities + self.outputModalities = outputModalities + self.additionalProperties = additionalProperties + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ModelInfo { + let object = try TypraRuntime.object(data, typeName: "ModelInfo") + var instance = ModelInfo() + if let value = object["id"] { + instance.id = try TypraRuntime.string(value, field: "id") + } + if let value = object["displayName"] { + instance.displayName = try TypraRuntime.string(value, field: "displayName") + } + if let value = object["ownedBy"] { + instance.ownedBy = try TypraRuntime.string(value, field: "ownedBy") + } + if let value = object["contextWindow"] { + instance.contextWindow = try TypraRuntime.int32(value, field: "contextWindow") + } + if let value = object["inputModalities"] { + instance.inputModalities = try TypraRuntime.array(value, field: "inputModalities").map { try TypraRuntime.string($0, field: "inputModalities") } + } + if let value = object["outputModalities"] { + instance.outputModalities = try TypraRuntime.array(value, field: "outputModalities").map { try TypraRuntime.string($0, field: "outputModalities") } + } + if let value = object["additionalProperties"] { + instance.additionalProperties = try TypraRuntime.dictionary(value, field: "additionalProperties") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["id"] = self.id + if let value = self.displayName { + result["displayName"] = value + } + if let value = self.ownedBy { + result["ownedBy"] = value + } + if let value = self.contextWindow { + result["contextWindow"] = value + } + if let value = self.inputModalities { + result["inputModalities"] = value + } + if let value = self.outputModalities { + result["outputModalities"] = value + } + if let value = self.additionalProperties { + result["additionalProperties"] = value + } + return result + } + + public func toWire(_ provider: String, context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + let wireNameId: String? + switch provider { + case "openai": wireNameId = "id" + case "anthropic": wireNameId = "id" + default: wireNameId = nil + } + if let wireKey = wireNameId { result[wireKey] = self.id } + let wireNameDisplayName: String? + switch provider { + case "anthropic": wireNameDisplayName = "display_name" + default: wireNameDisplayName = nil + } + if let wireKey = wireNameDisplayName, let value = self.displayName { result[wireKey] = value } + let wireNameOwnedBy: String? + switch provider { + case "openai": wireNameOwnedBy = "owned_by" + default: wireNameOwnedBy = nil + } + if let wireKey = wireNameOwnedBy, let value = self.ownedBy { result[wireKey] = value } + let wireNameContextWindow: String? + switch provider { + case "anthropic": wireNameContextWindow = "context_length" + default: wireNameContextWindow = nil + } + if let wireKey = wireNameContextWindow, let value = self.contextWindow { result[wireKey] = value } + let wireNameInputModalities: String? + switch provider { + case "anthropic": wireNameInputModalities = "input_modalities" + default: wireNameInputModalities = nil + } + if let wireKey = wireNameInputModalities, let value = self.inputModalities { result[wireKey] = value } + let wireNameOutputModalities: String? + switch provider { + case "anthropic": wireNameOutputModalities = "output_modalities" + default: wireNameOutputModalities = nil + } + if let wireKey = wireNameOutputModalities, let value = self.outputModalities { result[wireKey] = value } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ModelInfo { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ModelInfo"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ModelInfo { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ModelInfo"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/model_options.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/model_options.swift new file mode 100644 index 000000000..b6434cacd --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/model_options.swift @@ -0,0 +1,186 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Options for configuring the behavior of the AI model. +public struct ModelOptions: TypraModel { + public static let shorthandProperty: String? = nil + public var frequencyPenalty: Float? = nil + public var maxOutputTokens: Int32? = nil + public var presencePenalty: Float? = nil + public var seed: Int32? = nil + public var temperature: Float? = nil + public var topK: Int32? = nil + public var topP: Float? = nil + public var stopSequences: [String]? = nil + public var allowMultipleToolCalls: Bool? = nil + public var additionalProperties: [String: Any]? = nil + + public init(frequencyPenalty: Float? = nil, maxOutputTokens: Int32? = nil, presencePenalty: Float? = nil, seed: Int32? = nil, temperature: Float? = nil, topK: Int32? = nil, topP: Float? = nil, stopSequences: [String]? = nil, allowMultipleToolCalls: Bool? = nil, additionalProperties: [String: Any]? = nil) { + self.frequencyPenalty = frequencyPenalty + self.maxOutputTokens = maxOutputTokens + self.presencePenalty = presencePenalty + self.seed = seed + self.temperature = temperature + self.topK = topK + self.topP = topP + self.stopSequences = stopSequences + self.allowMultipleToolCalls = allowMultipleToolCalls + self.additionalProperties = additionalProperties + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ModelOptions { + let object = try TypraRuntime.object(data, typeName: "ModelOptions") + var instance = ModelOptions() + if let value = object["frequencyPenalty"] { + instance.frequencyPenalty = try TypraRuntime.float(value, field: "frequencyPenalty") + } + if let value = object["maxOutputTokens"] { + instance.maxOutputTokens = try TypraRuntime.int32(value, field: "maxOutputTokens") + } + if let value = object["presencePenalty"] { + instance.presencePenalty = try TypraRuntime.float(value, field: "presencePenalty") + } + if let value = object["seed"] { + instance.seed = try TypraRuntime.int32(value, field: "seed") + } + if let value = object["temperature"] { + instance.temperature = try TypraRuntime.float(value, field: "temperature") + } + if let value = object["topK"] { + instance.topK = try TypraRuntime.int32(value, field: "topK") + } + if let value = object["topP"] { + instance.topP = try TypraRuntime.float(value, field: "topP") + } + if let value = object["stopSequences"] { + instance.stopSequences = try TypraRuntime.array(value, field: "stopSequences").map { try TypraRuntime.string($0, field: "stopSequences") } + } + if let value = object["allowMultipleToolCalls"] { + instance.allowMultipleToolCalls = try TypraRuntime.bool(value, field: "allowMultipleToolCalls") + } + if let value = object["additionalProperties"] { + instance.additionalProperties = try TypraRuntime.dictionary(value, field: "additionalProperties") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.frequencyPenalty { + result["frequencyPenalty"] = value + } + if let value = self.maxOutputTokens { + result["maxOutputTokens"] = value + } + if let value = self.presencePenalty { + result["presencePenalty"] = value + } + if let value = self.seed { + result["seed"] = value + } + if let value = self.temperature { + result["temperature"] = value + } + if let value = self.topK { + result["topK"] = value + } + if let value = self.topP { + result["topP"] = value + } + if let value = self.stopSequences { + result["stopSequences"] = value + } + if let value = self.allowMultipleToolCalls { + result["allowMultipleToolCalls"] = value + } + if let value = self.additionalProperties { + result["additionalProperties"] = value + } + return result + } + + public func toWire(_ provider: String, context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + let wireNameFrequencyPenalty: String? + switch provider { + case "openai": wireNameFrequencyPenalty = "frequency_penalty" + default: wireNameFrequencyPenalty = nil + } + if let wireKey = wireNameFrequencyPenalty, let value = self.frequencyPenalty { result[wireKey] = value } + let wireNameMaxOutputTokens: String? + switch provider { + case "openai": wireNameMaxOutputTokens = "max_completion_tokens" + case "responses": wireNameMaxOutputTokens = "max_output_tokens" + case "anthropic": wireNameMaxOutputTokens = "max_tokens" + default: wireNameMaxOutputTokens = nil + } + if let wireKey = wireNameMaxOutputTokens, let value = self.maxOutputTokens { result[wireKey] = value } + let wireNamePresencePenalty: String? + switch provider { + case "openai": wireNamePresencePenalty = "presence_penalty" + default: wireNamePresencePenalty = nil + } + if let wireKey = wireNamePresencePenalty, let value = self.presencePenalty { result[wireKey] = value } + let wireNameSeed: String? + switch provider { + case "openai": wireNameSeed = "seed" + default: wireNameSeed = nil + } + if let wireKey = wireNameSeed, let value = self.seed { result[wireKey] = value } + let wireNameTemperature: String? + switch provider { + case "openai": wireNameTemperature = "temperature" + case "responses": wireNameTemperature = "temperature" + case "anthropic": wireNameTemperature = "temperature" + default: wireNameTemperature = nil + } + if let wireKey = wireNameTemperature, let value = self.temperature { result[wireKey] = value } + let wireNameTopK: String? + switch provider { + case "openai": wireNameTopK = "top_k" + case "anthropic": wireNameTopK = "top_k" + default: wireNameTopK = nil + } + if let wireKey = wireNameTopK, let value = self.topK { result[wireKey] = value } + let wireNameTopP: String? + switch provider { + case "openai": wireNameTopP = "top_p" + case "responses": wireNameTopP = "top_p" + case "anthropic": wireNameTopP = "top_p" + default: wireNameTopP = nil + } + if let wireKey = wireNameTopP, let value = self.topP { result[wireKey] = value } + let wireNameStopSequences: String? + switch provider { + case "openai": wireNameStopSequences = "stop" + case "anthropic": wireNameStopSequences = "stop_sequences" + default: wireNameStopSequences = nil + } + if let wireKey = wireNameStopSequences, let value = self.stopSequences { result[wireKey] = value } + let wireNameAllowMultipleToolCalls: String? + switch provider { + case "openai": wireNameAllowMultipleToolCalls = "parallel_tool_calls" + default: wireNameAllowMultipleToolCalls = nil + } + if let wireKey = wireNameAllowMultipleToolCalls, let value = self.allowMultipleToolCalls { result[wireKey] = value } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ModelOptions { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ModelOptions"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ModelOptions { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ModelOptions"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/project_info.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/project_info.swift new file mode 100644 index 000000000..523ebd2b3 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/project_info.swift @@ -0,0 +1,80 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// A project hosted by an AI service resource. +public struct ProjectInfo: TypraModel { + public static let shorthandProperty: String? = nil + public var name: String = "" + public var displayName: String = "" + public var endpoint: String = "" + + public init(name: String = "", displayName: String = "", endpoint: String = "") { + self.name = name + self.displayName = displayName + self.endpoint = endpoint + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ProjectInfo { + let object = try TypraRuntime.object(data, typeName: "ProjectInfo") + var instance = ProjectInfo() + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + if let value = object["displayName"] { + instance.displayName = try TypraRuntime.string(value, field: "displayName") + } + if let value = object["endpoint"] { + instance.endpoint = try TypraRuntime.string(value, field: "endpoint") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["name"] = self.name + result["displayName"] = self.displayName + result["endpoint"] = self.endpoint + return result + } + + public func toWire(_ provider: String, context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + let wireNameName: String? + switch provider { + case "foundry": wireNameName = "name" + default: wireNameName = nil + } + if let wireKey = wireNameName { result[wireKey] = self.name } + let wireNameDisplayName: String? + switch provider { + case "foundry": wireNameDisplayName = "display_name" + default: wireNameDisplayName = nil + } + if let wireKey = wireNameDisplayName { result[wireKey] = self.displayName } + let wireNameEndpoint: String? + switch provider { + case "foundry": wireNameEndpoint = "endpoint" + default: wireNameEndpoint = nil + } + if let wireKey = wireNameEndpoint { result[wireKey] = self.endpoint } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ProjectInfo { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ProjectInfo"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ProjectInfo { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ProjectInfo"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/subscription_info.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/subscription_info.swift new file mode 100644 index 000000000..72be34054 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/subscription_info.swift @@ -0,0 +1,80 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// A cloud subscription or account boundary available to the authenticated user. +public struct SubscriptionInfo: TypraModel { + public static let shorthandProperty: String? = nil + public var subscriptionId: String = "" + public var displayName: String = "" + public var state: String = "" + + public init(subscriptionId: String = "", displayName: String = "", state: String = "") { + self.subscriptionId = subscriptionId + self.displayName = displayName + self.state = state + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> SubscriptionInfo { + let object = try TypraRuntime.object(data, typeName: "SubscriptionInfo") + var instance = SubscriptionInfo() + if let value = object["subscriptionId"] { + instance.subscriptionId = try TypraRuntime.string(value, field: "subscriptionId") + } + if let value = object["displayName"] { + instance.displayName = try TypraRuntime.string(value, field: "displayName") + } + if let value = object["state"] { + instance.state = try TypraRuntime.string(value, field: "state") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["subscriptionId"] = self.subscriptionId + result["displayName"] = self.displayName + result["state"] = self.state + return result + } + + public func toWire(_ provider: String, context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + let wireNameSubscriptionId: String? + switch provider { + case "foundry": wireNameSubscriptionId = "subscription_id" + default: wireNameSubscriptionId = nil + } + if let wireKey = wireNameSubscriptionId { result[wireKey] = self.subscriptionId } + let wireNameDisplayName: String? + switch provider { + case "foundry": wireNameDisplayName = "display_name" + default: wireNameDisplayName = nil + } + if let wireKey = wireNameDisplayName { result[wireKey] = self.displayName } + let wireNameState: String? + switch provider { + case "foundry": wireNameState = "state" + default: wireNameState = nil + } + if let wireKey = wireNameState { result[wireKey] = self.state } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> SubscriptionInfo { + return try load(TypraRuntime.jsonObject(from: json, typeName: "SubscriptionInfo"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> SubscriptionInfo { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "SubscriptionInfo"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/token_usage.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/token_usage.swift new file mode 100644 index 000000000..68dfd35d8 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/token_usage.swift @@ -0,0 +1,88 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Tracks token consumption for a single LLM call. Provider-specific field names (e.g., OpenAI's `prompt_tokens` vs Anthropic's `input_tokens`) are mapped via `knownAs` augments in the wire directory. +public struct TokenUsage: TypraModel { + public static let shorthandProperty: String? = nil + public var promptTokens: Int32? = nil + public var completionTokens: Int32? = nil + public var totalTokens: Int32? = nil + + public init(promptTokens: Int32? = nil, completionTokens: Int32? = nil, totalTokens: Int32? = nil) { + self.promptTokens = promptTokens + self.completionTokens = completionTokens + self.totalTokens = totalTokens + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> TokenUsage { + let object = try TypraRuntime.object(data, typeName: "TokenUsage") + var instance = TokenUsage() + if let value = object["promptTokens"] { + instance.promptTokens = try TypraRuntime.int32(value, field: "promptTokens") + } + if let value = object["completionTokens"] { + instance.completionTokens = try TypraRuntime.int32(value, field: "completionTokens") + } + if let value = object["totalTokens"] { + instance.totalTokens = try TypraRuntime.int32(value, field: "totalTokens") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.promptTokens { + result["promptTokens"] = value + } + if let value = self.completionTokens { + result["completionTokens"] = value + } + if let value = self.totalTokens { + result["totalTokens"] = value + } + return result + } + + public func toWire(_ provider: String, context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + let wireNamePromptTokens: String? + switch provider { + case "openai": wireNamePromptTokens = "prompt_tokens" + case "anthropic": wireNamePromptTokens = "input_tokens" + default: wireNamePromptTokens = nil + } + if let wireKey = wireNamePromptTokens, let value = self.promptTokens { result[wireKey] = value } + let wireNameCompletionTokens: String? + switch provider { + case "openai": wireNameCompletionTokens = "completion_tokens" + case "anthropic": wireNameCompletionTokens = "output_tokens" + default: wireNameCompletionTokens = nil + } + if let wireKey = wireNameCompletionTokens, let value = self.completionTokens { result[wireKey] = value } + let wireNameTotalTokens: String? + switch provider { + case "openai": wireNameTotalTokens = "total_tokens" + default: wireNameTotalTokens = nil + } + if let wireKey = wireNameTotalTokens, let value = self.totalTokens { result[wireKey] = value } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> TokenUsage { + return try load(TypraRuntime.jsonObject(from: json, typeName: "TokenUsage"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> TokenUsage { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "TokenUsage"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/compaction_config.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/compaction_config.swift new file mode 100644 index 000000000..1cc870af0 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/compaction_config.swift @@ -0,0 +1,63 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Configuration for context window compaction. When the message history exceeds the context budget, the compaction strategy is applied to reduce the message list while preserving essential information. +public struct CompactionConfig: TypraModel { + public static let shorthandProperty: String? = nil + public var strategy: String? = nil + public var budget: Int32? = nil + public var options: [String: Any]? = nil + + public init(strategy: String? = nil, budget: Int32? = nil, options: [String: Any]? = nil) { + self.strategy = strategy + self.budget = budget + self.options = options + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> CompactionConfig { + let object = try TypraRuntime.object(data, typeName: "CompactionConfig") + var instance = CompactionConfig() + if let value = object["strategy"] { + instance.strategy = try TypraRuntime.string(value, field: "strategy") + } + if let value = object["budget"] { + instance.budget = try TypraRuntime.int32(value, field: "budget") + } + if let value = object["options"] { + instance.options = try TypraRuntime.dictionary(value, field: "options") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.strategy { + result["strategy"] = value + } + if let value = self.budget { + result["budget"] = value + } + if let value = self.options { + result["options"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> CompactionConfig { + return try load(TypraRuntime.jsonObject(from: json, typeName: "CompactionConfig"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> CompactionConfig { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "CompactionConfig"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/context_candidate.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/context_candidate.swift new file mode 100644 index 000000000..0c998d81c --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/context_candidate.swift @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// A context contribution before filtering, ranking, and packing. +public struct ContextCandidate: TypraModel { + public static let shorthandProperty: String? = nil + public var id: String = "" + public var source: String = "" + public var messages: [Message] = [] + public var metadata: [String: Any]? = nil + + public init(id: String = "", source: String = "", messages: [Message] = [], metadata: [String: Any]? = nil) { + self.id = id + self.source = source + self.messages = messages + self.metadata = metadata + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ContextCandidate { + let object = try TypraRuntime.object(data, typeName: "ContextCandidate") + var instance = ContextCandidate() + if let value = object["id"] { + instance.id = try TypraRuntime.string(value, field: "id") + } + if let value = object["source"] { + instance.source = try TypraRuntime.string(value, field: "source") + } + if let value = object["messages"] { + instance.messages = try TypraRuntime.array(value, field: "messages").enumerated().map { try Message.load($1, context: context.at("messages").atIndex($0)) } + } + if let value = object["metadata"] { + instance.metadata = try TypraRuntime.dictionary(value, field: "metadata") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["id"] = self.id + result["source"] = self.source + result["messages"] = try self.messages.map { try $0.save(context) } + if let value = self.metadata { + result["metadata"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ContextCandidate { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ContextCandidate"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ContextCandidate { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ContextCandidate"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/context_request.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/context_request.swift new file mode 100644 index 000000000..52110d0d2 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/context_request.swift @@ -0,0 +1,95 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Planning input handed to the context pipeline before one model invocation. +public struct ContextRequest: TypraModel { + public static let shorthandProperty: String? = nil + public var sessionId: String = "" + public var turnId: String = "" + public var invocationId: String = "" + public var iteration: Int32 = 0 + public var messages: [Message] = [] + public var stablePrefixMessages: Int32 = 0 + public var contextState: InvocationContextState = InvocationContextState() + public var inputs: Any? = nil + + public init(sessionId: String = "", turnId: String = "", invocationId: String = "", iteration: Int32 = 0, messages: [Message] = [], stablePrefixMessages: Int32 = 0, contextState: InvocationContextState = InvocationContextState(), inputs: Any? = nil) { + self.sessionId = sessionId + self.turnId = turnId + self.invocationId = invocationId + self.iteration = iteration + self.messages = messages + self.stablePrefixMessages = stablePrefixMessages + self.contextState = contextState + self.inputs = inputs + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ContextRequest { + let object = try TypraRuntime.object(data, typeName: "ContextRequest") + var instance = ContextRequest() + if let value = object["sessionId"] { + instance.sessionId = try TypraRuntime.string(value, field: "sessionId") + } + if let value = object["turnId"] { + instance.turnId = try TypraRuntime.string(value, field: "turnId") + } + if let value = object["invocationId"] { + instance.invocationId = try TypraRuntime.string(value, field: "invocationId") + } + if let value = object["iteration"] { + instance.iteration = try TypraRuntime.int32(value, field: "iteration") + } + if let value = object["messages"] { + instance.messages = try TypraRuntime.array(value, field: "messages").enumerated().map { try Message.load($1, context: context.at("messages").atIndex($0)) } + } + if let value = object["stablePrefixMessages"] { + instance.stablePrefixMessages = try TypraRuntime.int32(value, field: "stablePrefixMessages") + } + else { + instance.stablePrefixMessages = 0 + } + if object["contextState"] == nil || object["contextState"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("contextState").path + ": missing required field") + } + if let value = object["contextState"] { + instance.contextState = try InvocationContextState.load(value, context: context.at("contextState")) + } + if let value = object["inputs"] { + instance.inputs = value + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["sessionId"] = self.sessionId + result["turnId"] = self.turnId + result["invocationId"] = self.invocationId + result["iteration"] = self.iteration + result["messages"] = try self.messages.map { try $0.save(context) } + result["stablePrefixMessages"] = self.stablePrefixMessages + result["contextState"] = try self.contextState.save(context) + if let value = self.inputs { + result["inputs"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ContextRequest { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ContextRequest"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ContextRequest { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ContextRequest"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/delegated_state_reference.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/delegated_state_reference.swift new file mode 100644 index 000000000..e04df8927 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/delegated_state_reference.swift @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// A reference to model-visible state retained by a provider. +public struct DelegatedStateReference: TypraModel { + public static let shorthandProperty: String? = nil + public var provider: String = "" + public var kind: String = "" + public var id: String = "" + public var metadata: [String: Any]? = nil + + public init(provider: String = "", kind: String = "", id: String = "", metadata: [String: Any]? = nil) { + self.provider = provider + self.kind = kind + self.id = id + self.metadata = metadata + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> DelegatedStateReference { + let object = try TypraRuntime.object(data, typeName: "DelegatedStateReference") + var instance = DelegatedStateReference() + if let value = object["provider"] { + instance.provider = try TypraRuntime.string(value, field: "provider") + } + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + if let value = object["id"] { + instance.id = try TypraRuntime.string(value, field: "id") + } + if let value = object["metadata"] { + instance.metadata = try TypraRuntime.dictionary(value, field: "metadata") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["provider"] = self.provider + result["kind"] = self.kind + result["id"] = self.id + if let value = self.metadata { + result["metadata"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> DelegatedStateReference { + return try load(TypraRuntime.jsonObject(from: json, typeName: "DelegatedStateReference"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> DelegatedStateReference { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "DelegatedStateReference"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/engine_checkpoint.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/engine_checkpoint.swift new file mode 100644 index 000000000..9fb75a5f4 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/engine_checkpoint.swift @@ -0,0 +1,225 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Portable durable checkpoint emitted after a committed model/tool round. A resumed run rebuilt from a checkpoint MUST NOT duplicate a model or tool effect that the checkpoint already records as committed. Run identity is carried so delegated runs checkpoint independently under their own run. +public struct EngineCheckpoint: TypraModel { + public static let shorthandProperty: String? = nil + public var id: String = "" + public var sessionId: String = "" + public var turnId: String = "" + public var runId: String = "" + public var parentRunId: String? = nil + public var delegationDepth: Int32 = 0 + public var iteration: Int32 = 0 + public var lastSequence: Int64 = 0 + public var messages: [Message] = [] + public var stablePrefixMessages: Int32 = 0 + public var inputs: Any? = nil + public var activeInvocationId: String? = nil + public var pendingToolRequests: [ModelToolRequest]? = nil + public var completedToolResults: [ModelToolResult]? = nil + public var completedModelIterations: Int32 = 0 + public var reconciliationRequired: Bool = false + public var modelReconciliation: ModelReconciliationState? = nil + public var pendingOutput: Any? = nil + public var finalOutputReady: Bool = false + public var pendingModelResponse: ModelInvocationResponse? = nil + public var resumeSameIteration: Bool = false + public var policyAppliedForIteration: Bool = false + public var contextState: InvocationContextState = InvocationContextState() + public var metadata: [String: Any]? = nil + + public init(id: String = "", sessionId: String = "", turnId: String = "", runId: String = "", parentRunId: String? = nil, delegationDepth: Int32 = 0, iteration: Int32 = 0, lastSequence: Int64 = 0, messages: [Message] = [], stablePrefixMessages: Int32 = 0, inputs: Any? = nil, activeInvocationId: String? = nil, pendingToolRequests: [ModelToolRequest]? = nil, completedToolResults: [ModelToolResult]? = nil, completedModelIterations: Int32 = 0, reconciliationRequired: Bool = false, modelReconciliation: ModelReconciliationState? = nil, pendingOutput: Any? = nil, finalOutputReady: Bool = false, pendingModelResponse: ModelInvocationResponse? = nil, resumeSameIteration: Bool = false, policyAppliedForIteration: Bool = false, contextState: InvocationContextState = InvocationContextState(), metadata: [String: Any]? = nil) { + self.id = id + self.sessionId = sessionId + self.turnId = turnId + self.runId = runId + self.parentRunId = parentRunId + self.delegationDepth = delegationDepth + self.iteration = iteration + self.lastSequence = lastSequence + self.messages = messages + self.stablePrefixMessages = stablePrefixMessages + self.inputs = inputs + self.activeInvocationId = activeInvocationId + self.pendingToolRequests = pendingToolRequests + self.completedToolResults = completedToolResults + self.completedModelIterations = completedModelIterations + self.reconciliationRequired = reconciliationRequired + self.modelReconciliation = modelReconciliation + self.pendingOutput = pendingOutput + self.finalOutputReady = finalOutputReady + self.pendingModelResponse = pendingModelResponse + self.resumeSameIteration = resumeSameIteration + self.policyAppliedForIteration = policyAppliedForIteration + self.contextState = contextState + self.metadata = metadata + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> EngineCheckpoint { + let object = try TypraRuntime.object(data, typeName: "EngineCheckpoint") + var instance = EngineCheckpoint() + if let value = object["id"] { + instance.id = try TypraRuntime.string(value, field: "id") + } + if let value = object["sessionId"] { + instance.sessionId = try TypraRuntime.string(value, field: "sessionId") + } + if let value = object["turnId"] { + instance.turnId = try TypraRuntime.string(value, field: "turnId") + } + if let value = object["runId"] { + instance.runId = try TypraRuntime.string(value, field: "runId") + } + if let value = object["parentRunId"] { + instance.parentRunId = try TypraRuntime.string(value, field: "parentRunId") + } + if let value = object["delegationDepth"] { + instance.delegationDepth = try TypraRuntime.int32(value, field: "delegationDepth") + } + else { + instance.delegationDepth = 0 + } + if let value = object["iteration"] { + instance.iteration = try TypraRuntime.int32(value, field: "iteration") + } + if let value = object["lastSequence"] { + instance.lastSequence = try TypraRuntime.int64(value, field: "lastSequence") + } + if let value = object["messages"] { + instance.messages = try TypraRuntime.array(value, field: "messages").enumerated().map { try Message.load($1, context: context.at("messages").atIndex($0)) } + } + if let value = object["stablePrefixMessages"] { + instance.stablePrefixMessages = try TypraRuntime.int32(value, field: "stablePrefixMessages") + } + else { + instance.stablePrefixMessages = 0 + } + if let value = object["inputs"] { + instance.inputs = value + } + if let value = object["activeInvocationId"] { + instance.activeInvocationId = try TypraRuntime.string(value, field: "activeInvocationId") + } + if let value = object["pendingToolRequests"] { + instance.pendingToolRequests = try TypraRuntime.array(value, field: "pendingToolRequests").enumerated().map { try ModelToolRequest.load($1, context: context.at("pendingToolRequests").atIndex($0)) } + } + if let value = object["completedToolResults"] { + instance.completedToolResults = try TypraRuntime.array(value, field: "completedToolResults").enumerated().map { try ModelToolResult.load($1, context: context.at("completedToolResults").atIndex($0)) } + } + if let value = object["completedModelIterations"] { + instance.completedModelIterations = try TypraRuntime.int32(value, field: "completedModelIterations") + } + else { + instance.completedModelIterations = 0 + } + if let value = object["reconciliationRequired"] { + instance.reconciliationRequired = try TypraRuntime.bool(value, field: "reconciliationRequired") + } + else { + instance.reconciliationRequired = false + } + if let value = object["modelReconciliation"] { + instance.modelReconciliation = try ModelReconciliationState.load(value, context: context.at("modelReconciliation")) + } + if let value = object["pendingOutput"] { + instance.pendingOutput = value + } + if let value = object["finalOutputReady"] { + instance.finalOutputReady = try TypraRuntime.bool(value, field: "finalOutputReady") + } + else { + instance.finalOutputReady = false + } + if let value = object["pendingModelResponse"] { + instance.pendingModelResponse = try ModelInvocationResponse.load(value, context: context.at("pendingModelResponse")) + } + if let value = object["resumeSameIteration"] { + instance.resumeSameIteration = try TypraRuntime.bool(value, field: "resumeSameIteration") + } + else { + instance.resumeSameIteration = false + } + if let value = object["policyAppliedForIteration"] { + instance.policyAppliedForIteration = try TypraRuntime.bool(value, field: "policyAppliedForIteration") + } + else { + instance.policyAppliedForIteration = false + } + if object["contextState"] == nil || object["contextState"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("contextState").path + ": missing required field") + } + if let value = object["contextState"] { + instance.contextState = try InvocationContextState.load(value, context: context.at("contextState")) + } + if let value = object["metadata"] { + instance.metadata = try TypraRuntime.dictionary(value, field: "metadata") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["id"] = self.id + result["sessionId"] = self.sessionId + result["turnId"] = self.turnId + result["runId"] = self.runId + if let value = self.parentRunId { + result["parentRunId"] = value + } + result["delegationDepth"] = self.delegationDepth + result["iteration"] = self.iteration + result["lastSequence"] = self.lastSequence + result["messages"] = try self.messages.map { try $0.save(context) } + result["stablePrefixMessages"] = self.stablePrefixMessages + if let value = self.inputs { + result["inputs"] = value + } + if let value = self.activeInvocationId { + result["activeInvocationId"] = value + } + if let value = self.pendingToolRequests { + result["pendingToolRequests"] = try value.map { try $0.save(context) } + } + if let value = self.completedToolResults { + result["completedToolResults"] = try value.map { try $0.save(context) } + } + result["completedModelIterations"] = self.completedModelIterations + result["reconciliationRequired"] = self.reconciliationRequired + if let value = self.modelReconciliation { + result["modelReconciliation"] = try value.save(context) + } + if let value = self.pendingOutput { + result["pendingOutput"] = value + } + result["finalOutputReady"] = self.finalOutputReady + if let value = self.pendingModelResponse { + result["pendingModelResponse"] = try value.save(context) + } + result["resumeSameIteration"] = self.resumeSameIteration + result["policyAppliedForIteration"] = self.policyAppliedForIteration + result["contextState"] = try self.contextState.save(context) + if let value = self.metadata { + result["metadata"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> EngineCheckpoint { + return try load(TypraRuntime.jsonObject(from: json, typeName: "EngineCheckpoint"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> EngineCheckpoint { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "EngineCheckpoint"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/engine_event.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/engine_event.swift new file mode 100644 index 000000000..bf177f63f --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/engine_event.swift @@ -0,0 +1,176 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum EngineEventKind: String, Codable, CaseIterable { + case turnStarted = "turn_started" + case policyApplied = "policy_applied" + case contextPrepared = "context_prepared" + case modelInvocationStarted = "model_invocation_started" + case modelInvocationCompleted = "model_invocation_completed" + case modelInvocationFailed = "model_invocation_failed" + case modelReconciliationRequired = "model_reconciliation_required" + case modelInvocationReconciled = "model_invocation_reconciled" + case permissionRequested = "permission_requested" + case permissionResolved = "permission_resolved" + case toolExecutionStarted = "tool_execution_started" + case toolExecutionCompleted = "tool_execution_completed" + case toolResultCommitted = "tool_result_committed" + case toolResultReconciled = "tool_result_reconciled" + case conversationUpdated = "conversation_updated" + case checkpointCreated = "checkpoint_created" + case turnCommitted = "turn_committed" + case turnCancelled = "turn_cancelled" + case turnFailed = "turn_failed" + case turnReconciliationRequired = "turn_reconciliation_required" + case postCommitStarted = "post_commit_started" + case postCommitCompleted = "post_commit_completed" + case postCommitFailed = "post_commit_failed" + public static func parse(_ value: String) throws -> EngineEventKind { + switch value { + case "turn_started": return .turnStarted + case "policy_applied": return .policyApplied + case "context_prepared": return .contextPrepared + case "model_invocation_started": return .modelInvocationStarted + case "model_invocation_completed": return .modelInvocationCompleted + case "model_invocation_failed": return .modelInvocationFailed + case "model_reconciliation_required": return .modelReconciliationRequired + case "model_invocation_reconciled": return .modelInvocationReconciled + case "permission_requested": return .permissionRequested + case "permission_resolved": return .permissionResolved + case "tool_execution_started": return .toolExecutionStarted + case "tool_execution_completed": return .toolExecutionCompleted + case "tool_result_committed": return .toolResultCommitted + case "tool_result_reconciled": return .toolResultReconciled + case "conversation_updated": return .conversationUpdated + case "checkpoint_created": return .checkpointCreated + case "turn_committed": return .turnCommitted + case "turn_cancelled": return .turnCancelled + case "turn_failed": return .turnFailed + case "turn_reconciliation_required": return .turnReconciliationRequired + case "post_commit_started": return .postCommitStarted + case "post_commit_completed": return .postCommitCompleted + case "post_commit_failed": return .postCommitFailed + default: throw TypraRuntimeError.invalidEnum(type: "EngineEventKind", value: value) + } + } +} + +/// One event in the monotonic semantic event stream of a turn. Events are the durable, replayable record a DurabilityPort persists. Run identity links delegated (nested) engine runs to their parent for cross-run observability. +public struct EngineEvent: TypraModel { + public static let shorthandProperty: String? = nil + public var sequence: Int64 = 0 + public var id: String = "" + public var timestamp: String = "" + public var sessionId: String = "" + public var turnId: String = "" + public var runId: String = "" + public var parentRunId: String? = nil + public var delegationDepth: Int32 = 0 + public var invocationId: String? = nil + public var iteration: Int32? = nil + public var kind: EngineEventKind = (try! EngineEventKind.parse("turn_started")) + public var payload: Any? = nil + + public init(sequence: Int64 = 0, id: String = "", timestamp: String = "", sessionId: String = "", turnId: String = "", runId: String = "", parentRunId: String? = nil, delegationDepth: Int32 = 0, invocationId: String? = nil, iteration: Int32? = nil, kind: EngineEventKind = (try! EngineEventKind.parse("turn_started")), payload: Any? = nil) { + self.sequence = sequence + self.id = id + self.timestamp = timestamp + self.sessionId = sessionId + self.turnId = turnId + self.runId = runId + self.parentRunId = parentRunId + self.delegationDepth = delegationDepth + self.invocationId = invocationId + self.iteration = iteration + self.kind = kind + self.payload = payload + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> EngineEvent { + let object = try TypraRuntime.object(data, typeName: "EngineEvent") + var instance = EngineEvent() + if let value = object["sequence"] { + instance.sequence = try TypraRuntime.int64(value, field: "sequence") + } + if let value = object["id"] { + instance.id = try TypraRuntime.string(value, field: "id") + } + if let value = object["timestamp"] { + instance.timestamp = try TypraRuntime.string(value, field: "timestamp") + } + if let value = object["sessionId"] { + instance.sessionId = try TypraRuntime.string(value, field: "sessionId") + } + if let value = object["turnId"] { + instance.turnId = try TypraRuntime.string(value, field: "turnId") + } + if let value = object["runId"] { + instance.runId = try TypraRuntime.string(value, field: "runId") + } + if let value = object["parentRunId"] { + instance.parentRunId = try TypraRuntime.string(value, field: "parentRunId") + } + if let value = object["delegationDepth"] { + instance.delegationDepth = try TypraRuntime.int32(value, field: "delegationDepth") + } + else { + instance.delegationDepth = 0 + } + if let value = object["invocationId"] { + instance.invocationId = try TypraRuntime.string(value, field: "invocationId") + } + if let value = object["iteration"] { + instance.iteration = try TypraRuntime.int32(value, field: "iteration") + } + if let value = object["kind"] { + instance.kind = try EngineEventKind.parse(try TypraRuntime.string(value, field: "kind")) + } + if let value = object["payload"] { + instance.payload = value + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["sequence"] = self.sequence + result["id"] = self.id + result["timestamp"] = self.timestamp + result["sessionId"] = self.sessionId + result["turnId"] = self.turnId + result["runId"] = self.runId + if let value = self.parentRunId { + result["parentRunId"] = value + } + result["delegationDepth"] = self.delegationDepth + if let value = self.invocationId { + result["invocationId"] = value + } + if let value = self.iteration { + result["iteration"] = value + } + result["kind"] = self.kind.rawValue + if let value = self.payload { + result["payload"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> EngineEvent { + return try load(TypraRuntime.jsonObject(from: json, typeName: "EngineEvent"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> EngineEvent { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "EngineEvent"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/engine_permission_decision.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/engine_permission_decision.swift new file mode 100644 index 000000000..7579693d9 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/engine_permission_decision.swift @@ -0,0 +1,61 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// A permission decision for one tool request. +public struct EnginePermissionDecision: TypraModel { + public static let shorthandProperty: String? = nil + public var approved: Bool = false + public var reason: String? = nil + public var metadata: [String: Any]? = nil + + public init(approved: Bool = false, reason: String? = nil, metadata: [String: Any]? = nil) { + self.approved = approved + self.reason = reason + self.metadata = metadata + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> EnginePermissionDecision { + let object = try TypraRuntime.object(data, typeName: "EnginePermissionDecision") + var instance = EnginePermissionDecision() + if let value = object["approved"] { + instance.approved = try TypraRuntime.bool(value, field: "approved") + } + if let value = object["reason"] { + instance.reason = try TypraRuntime.string(value, field: "reason") + } + if let value = object["metadata"] { + instance.metadata = try TypraRuntime.dictionary(value, field: "metadata") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["approved"] = self.approved + if let value = self.reason { + result["reason"] = value + } + if let value = self.metadata { + result["metadata"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> EnginePermissionDecision { + return try load(TypraRuntime.jsonObject(from: json, typeName: "EnginePermissionDecision"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> EnginePermissionDecision { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "EnginePermissionDecision"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/final_output_policy_request.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/final_output_policy_request.swift new file mode 100644 index 000000000..11c1ac8b1 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/final_output_policy_request.swift @@ -0,0 +1,79 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Final output supplied to the host policy immediately before a success commit. +public struct FinalOutputPolicyRequest: TypraModel { + public static let shorthandProperty: String? = nil + public var sessionId: String = "" + public var turnId: String = "" + public var iteration: Int32 = 0 + public var messages: [Message] = [] + public var output: Any? = nil + public var inputs: Any? = nil + + public init(sessionId: String = "", turnId: String = "", iteration: Int32 = 0, messages: [Message] = [], output: Any? = nil, inputs: Any? = nil) { + self.sessionId = sessionId + self.turnId = turnId + self.iteration = iteration + self.messages = messages + self.output = output + self.inputs = inputs + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> FinalOutputPolicyRequest { + let object = try TypraRuntime.object(data, typeName: "FinalOutputPolicyRequest") + var instance = FinalOutputPolicyRequest() + if let value = object["sessionId"] { + instance.sessionId = try TypraRuntime.string(value, field: "sessionId") + } + if let value = object["turnId"] { + instance.turnId = try TypraRuntime.string(value, field: "turnId") + } + if let value = object["iteration"] { + instance.iteration = try TypraRuntime.int32(value, field: "iteration") + } + if let value = object["messages"] { + instance.messages = try TypraRuntime.array(value, field: "messages").enumerated().map { try Message.load($1, context: context.at("messages").atIndex($0)) } + } + if let value = object["output"] { + instance.output = value + } + if let value = object["inputs"] { + instance.inputs = value + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["sessionId"] = self.sessionId + result["turnId"] = self.turnId + result["iteration"] = self.iteration + result["messages"] = try self.messages.map { try $0.save(context) } + if let value = self.output { + result["output"] = value + } + if let value = self.inputs { + result["inputs"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> FinalOutputPolicyRequest { + return try load(TypraRuntime.jsonObject(from: json, typeName: "FinalOutputPolicyRequest"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> FinalOutputPolicyRequest { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "FinalOutputPolicyRequest"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/final_output_policy_result.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/final_output_policy_result.swift new file mode 100644 index 000000000..89c7be9e8 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/final_output_policy_result.swift @@ -0,0 +1,55 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Final output rewrite produced by the host policy before commit. +public struct FinalOutputPolicyResult: TypraModel { + public static let shorthandProperty: String? = nil + public var output: Any? = nil + public var metadata: [String: Any]? = nil + + public init(output: Any? = nil, metadata: [String: Any]? = nil) { + self.output = output + self.metadata = metadata + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> FinalOutputPolicyResult { + let object = try TypraRuntime.object(data, typeName: "FinalOutputPolicyResult") + var instance = FinalOutputPolicyResult() + if let value = object["output"] { + instance.output = value + } + if let value = object["metadata"] { + instance.metadata = try TypraRuntime.dictionary(value, field: "metadata") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.output { + result["output"] = value + } + if let value = self.metadata { + result["metadata"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> FinalOutputPolicyResult { + return try load(TypraRuntime.jsonObject(from: json, typeName: "FinalOutputPolicyResult"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> FinalOutputPolicyResult { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "FinalOutputPolicyResult"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/host_policy_request.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/host_policy_request.swift new file mode 100644 index 000000000..284ba050f --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/host_policy_request.swift @@ -0,0 +1,80 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Host-owned deterministic state supplied before one model invocation. +public struct HostPolicyRequest: TypraModel { + public static let shorthandProperty: String? = nil + public var sessionId: String = "" + public var turnId: String = "" + public var iteration: Int32 = 0 + public var messages: [Message] = [] + public var stablePrefixMessages: Int32 = 0 + public var inputs: Any? = nil + + public init(sessionId: String = "", turnId: String = "", iteration: Int32 = 0, messages: [Message] = [], stablePrefixMessages: Int32 = 0, inputs: Any? = nil) { + self.sessionId = sessionId + self.turnId = turnId + self.iteration = iteration + self.messages = messages + self.stablePrefixMessages = stablePrefixMessages + self.inputs = inputs + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> HostPolicyRequest { + let object = try TypraRuntime.object(data, typeName: "HostPolicyRequest") + var instance = HostPolicyRequest() + if let value = object["sessionId"] { + instance.sessionId = try TypraRuntime.string(value, field: "sessionId") + } + if let value = object["turnId"] { + instance.turnId = try TypraRuntime.string(value, field: "turnId") + } + if let value = object["iteration"] { + instance.iteration = try TypraRuntime.int32(value, field: "iteration") + } + if let value = object["messages"] { + instance.messages = try TypraRuntime.array(value, field: "messages").enumerated().map { try Message.load($1, context: context.at("messages").atIndex($0)) } + } + if let value = object["stablePrefixMessages"] { + instance.stablePrefixMessages = try TypraRuntime.int32(value, field: "stablePrefixMessages") + } + else { + instance.stablePrefixMessages = 0 + } + if let value = object["inputs"] { + instance.inputs = value + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["sessionId"] = self.sessionId + result["turnId"] = self.turnId + result["iteration"] = self.iteration + result["messages"] = try self.messages.map { try $0.save(context) } + result["stablePrefixMessages"] = self.stablePrefixMessages + if let value = self.inputs { + result["inputs"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> HostPolicyRequest { + return try load(TypraRuntime.jsonObject(from: json, typeName: "HostPolicyRequest"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> HostPolicyRequest { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "HostPolicyRequest"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/host_policy_result.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/host_policy_result.swift new file mode 100644 index 000000000..3cf826418 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/host_policy_result.swift @@ -0,0 +1,62 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// State rewrite produced by the host policy before a model invocation. +public struct HostPolicyResult: TypraModel { + public static let shorthandProperty: String? = nil + public var messages: [Message] = [] + public var stablePrefixMessages: Int32 = 0 + public var metadata: [String: Any]? = nil + + public init(messages: [Message] = [], stablePrefixMessages: Int32 = 0, metadata: [String: Any]? = nil) { + self.messages = messages + self.stablePrefixMessages = stablePrefixMessages + self.metadata = metadata + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> HostPolicyResult { + let object = try TypraRuntime.object(data, typeName: "HostPolicyResult") + var instance = HostPolicyResult() + if let value = object["messages"] { + instance.messages = try TypraRuntime.array(value, field: "messages").enumerated().map { try Message.load($1, context: context.at("messages").atIndex($0)) } + } + if let value = object["stablePrefixMessages"] { + instance.stablePrefixMessages = try TypraRuntime.int32(value, field: "stablePrefixMessages") + } + else { + instance.stablePrefixMessages = 0 + } + if let value = object["metadata"] { + instance.metadata = try TypraRuntime.dictionary(value, field: "metadata") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["messages"] = try self.messages.map { try $0.save(context) } + result["stablePrefixMessages"] = self.stablePrefixMessages + if let value = self.metadata { + result["metadata"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> HostPolicyResult { + return try load(TypraRuntime.jsonObject(from: json, typeName: "HostPolicyResult"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> HostPolicyResult { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "HostPolicyResult"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/invocation_context_decision.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/invocation_context_decision.swift new file mode 100644 index 000000000..96e9259d0 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/invocation_context_decision.swift @@ -0,0 +1,93 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum InvocationContextDisposition: String, Codable, CaseIterable { + case included = "included" + case excluded = "excluded" + public static func parse(_ value: String) throws -> InvocationContextDisposition { + switch value { + case "included": return .included + case "excluded": return .excluded + default: throw TypraRuntimeError.invalidEnum(type: "InvocationContextDisposition", value: value) + } + } +} + +/// An auditable decision made while preparing model-visible context. +public struct InvocationContextDecision: TypraModel { + public static let shorthandProperty: String? = nil + public var candidateId: String = "" + public var disposition: InvocationContextDisposition = (try! InvocationContextDisposition.parse("included")) + public var reason: String = "" + public var rank: Int32? = nil + public var estimatedTokens: Int32? = nil + public var metadata: [String: Any]? = nil + + public init(candidateId: String = "", disposition: InvocationContextDisposition = (try! InvocationContextDisposition.parse("included")), reason: String = "", rank: Int32? = nil, estimatedTokens: Int32? = nil, metadata: [String: Any]? = nil) { + self.candidateId = candidateId + self.disposition = disposition + self.reason = reason + self.rank = rank + self.estimatedTokens = estimatedTokens + self.metadata = metadata + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> InvocationContextDecision { + let object = try TypraRuntime.object(data, typeName: "InvocationContextDecision") + var instance = InvocationContextDecision() + if let value = object["candidateId"] { + instance.candidateId = try TypraRuntime.string(value, field: "candidateId") + } + if let value = object["disposition"] { + instance.disposition = try InvocationContextDisposition.parse(try TypraRuntime.string(value, field: "disposition")) + } + if let value = object["reason"] { + instance.reason = try TypraRuntime.string(value, field: "reason") + } + if let value = object["rank"] { + instance.rank = try TypraRuntime.int32(value, field: "rank") + } + if let value = object["estimatedTokens"] { + instance.estimatedTokens = try TypraRuntime.int32(value, field: "estimatedTokens") + } + if let value = object["metadata"] { + instance.metadata = try TypraRuntime.dictionary(value, field: "metadata") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["candidateId"] = self.candidateId + result["disposition"] = self.disposition.rawValue + result["reason"] = self.reason + if let value = self.rank { + result["rank"] = value + } + if let value = self.estimatedTokens { + result["estimatedTokens"] = value + } + if let value = self.metadata { + result["metadata"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> InvocationContextDecision { + return try load(TypraRuntime.jsonObject(from: json, typeName: "InvocationContextDecision"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> InvocationContextDecision { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "InvocationContextDecision"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/invocation_context_state.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/invocation_context_state.swift new file mode 100644 index 000000000..ccda60728 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/invocation_context_state.swift @@ -0,0 +1,70 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum InvocationContextPortability: String, Codable, CaseIterable { + case portable = "portable" + case delegated = "delegated" + case opaque = "opaque" + public static func parse(_ value: String) throws -> InvocationContextPortability { + switch value { + case "portable": return .portable + case "delegated": return .delegated + case "opaque": return .opaque + default: throw TypraRuntimeError.invalidEnum(type: "InvocationContextPortability", value: value) + } + } +} + +/// Provider-context state carried into or out of an invocation. +public struct InvocationContextState: TypraModel { + public static let shorthandProperty: String? = nil + public var portability: InvocationContextPortability = (try! InvocationContextPortability.parse("portable")) + public var delegatedState: [DelegatedStateReference]? = nil + + public init(portability: InvocationContextPortability = (try! InvocationContextPortability.parse("portable")), delegatedState: [DelegatedStateReference]? = nil) { + self.portability = portability + self.delegatedState = delegatedState + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> InvocationContextState { + let object = try TypraRuntime.object(data, typeName: "InvocationContextState") + var instance = InvocationContextState() + if let value = object["portability"] { + instance.portability = try InvocationContextPortability.parse(try TypraRuntime.string(value, field: "portability")) + } + else { + instance.portability = (try! InvocationContextPortability.parse("portable")) + } + if let value = object["delegatedState"] { + instance.delegatedState = try TypraRuntime.array(value, field: "delegatedState").enumerated().map { try DelegatedStateReference.load($1, context: context.at("delegatedState").atIndex($0)) } + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["portability"] = self.portability.rawValue + if let value = self.delegatedState { + result["delegatedState"] = try value.map { try $0.save(context) } + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> InvocationContextState { + return try load(TypraRuntime.jsonObject(from: json, typeName: "InvocationContextState"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> InvocationContextState { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "InvocationContextState"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_invocation_context_snapshot.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_invocation_context_snapshot.swift new file mode 100644 index 000000000..a45bfb04d --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_invocation_context_snapshot.swift @@ -0,0 +1,109 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Immutable model-visible context for a single provider invocation. Retries of the same invocation MUST reuse the same snapshot. +public struct ModelInvocationContextSnapshot: TypraModel { + public static let shorthandProperty: String? = nil + public var id: String = "" + public var sessionId: String = "" + public var turnId: String = "" + public var invocationId: String = "" + public var iteration: Int32 = 0 + public var messages: [Message] = [] + public var decisions: [InvocationContextDecision]? = nil + public var stablePrefixMessages: Int32 = 0 + public var contextState: InvocationContextState = InvocationContextState() + public var metadata: [String: Any]? = nil + + public init(id: String = "", sessionId: String = "", turnId: String = "", invocationId: String = "", iteration: Int32 = 0, messages: [Message] = [], decisions: [InvocationContextDecision]? = nil, stablePrefixMessages: Int32 = 0, contextState: InvocationContextState = InvocationContextState(), metadata: [String: Any]? = nil) { + self.id = id + self.sessionId = sessionId + self.turnId = turnId + self.invocationId = invocationId + self.iteration = iteration + self.messages = messages + self.decisions = decisions + self.stablePrefixMessages = stablePrefixMessages + self.contextState = contextState + self.metadata = metadata + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ModelInvocationContextSnapshot { + let object = try TypraRuntime.object(data, typeName: "ModelInvocationContextSnapshot") + var instance = ModelInvocationContextSnapshot() + if let value = object["id"] { + instance.id = try TypraRuntime.string(value, field: "id") + } + if let value = object["sessionId"] { + instance.sessionId = try TypraRuntime.string(value, field: "sessionId") + } + if let value = object["turnId"] { + instance.turnId = try TypraRuntime.string(value, field: "turnId") + } + if let value = object["invocationId"] { + instance.invocationId = try TypraRuntime.string(value, field: "invocationId") + } + if let value = object["iteration"] { + instance.iteration = try TypraRuntime.int32(value, field: "iteration") + } + if let value = object["messages"] { + instance.messages = try TypraRuntime.array(value, field: "messages").enumerated().map { try Message.load($1, context: context.at("messages").atIndex($0)) } + } + if let value = object["decisions"] { + instance.decisions = try TypraRuntime.array(value, field: "decisions").enumerated().map { try InvocationContextDecision.load($1, context: context.at("decisions").atIndex($0)) } + } + if let value = object["stablePrefixMessages"] { + instance.stablePrefixMessages = try TypraRuntime.int32(value, field: "stablePrefixMessages") + } + else { + instance.stablePrefixMessages = 0 + } + if object["contextState"] == nil || object["contextState"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("contextState").path + ": missing required field") + } + if let value = object["contextState"] { + instance.contextState = try InvocationContextState.load(value, context: context.at("contextState")) + } + if let value = object["metadata"] { + instance.metadata = try TypraRuntime.dictionary(value, field: "metadata") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["id"] = self.id + result["sessionId"] = self.sessionId + result["turnId"] = self.turnId + result["invocationId"] = self.invocationId + result["iteration"] = self.iteration + result["messages"] = try self.messages.map { try $0.save(context) } + if let value = self.decisions { + result["decisions"] = try value.map { try $0.save(context) } + } + result["stablePrefixMessages"] = self.stablePrefixMessages + result["contextState"] = try self.contextState.save(context) + if let value = self.metadata { + result["metadata"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ModelInvocationContextSnapshot { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ModelInvocationContextSnapshot"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ModelInvocationContextSnapshot { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ModelInvocationContextSnapshot"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_invocation_request.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_invocation_request.swift new file mode 100644 index 000000000..336dd0abd --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_invocation_request.swift @@ -0,0 +1,48 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Normalized request for a single model-provider invocation. +public struct ModelInvocationRequest: TypraModel { + public static let shorthandProperty: String? = nil + public var context: ModelInvocationContextSnapshot = ModelInvocationContextSnapshot() + + public init(context: ModelInvocationContextSnapshot = ModelInvocationContextSnapshot()) { + self.context = context + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ModelInvocationRequest { + let object = try TypraRuntime.object(data, typeName: "ModelInvocationRequest") + var instance = ModelInvocationRequest() + if object["context"] == nil || object["context"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("context").path + ": missing required field") + } + if let value = object["context"] { + instance.context = try ModelInvocationContextSnapshot.load(value, context: context.at("context")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["context"] = try self.context.save(context) + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ModelInvocationRequest { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ModelInvocationRequest"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ModelInvocationRequest { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ModelInvocationRequest"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_invocation_response.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_invocation_response.swift new file mode 100644 index 000000000..b45c879fb --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_invocation_response.swift @@ -0,0 +1,87 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Provider-neutral result of a single model invocation. This is the live provider boundary. It is intentionally distinct from TurnModelResponse, which is the deterministic reference-runner callback. +public struct ModelInvocationResponse: TypraModel { + public static let shorthandProperty: String? = nil + public var output: Any? = nil + public var usage: InvocationUsage? = nil + public var assistantMessages: [Message]? = nil + public var toolRequests: [ModelToolRequest]? = nil + public var nextContextState: InvocationContextState? = nil + public var metadata: [String: Any]? = nil + + public init(output: Any? = nil, usage: InvocationUsage? = nil, assistantMessages: [Message]? = nil, toolRequests: [ModelToolRequest]? = nil, nextContextState: InvocationContextState? = nil, metadata: [String: Any]? = nil) { + self.output = output + self.usage = usage + self.assistantMessages = assistantMessages + self.toolRequests = toolRequests + self.nextContextState = nextContextState + self.metadata = metadata + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ModelInvocationResponse { + let object = try TypraRuntime.object(data, typeName: "ModelInvocationResponse") + var instance = ModelInvocationResponse() + if let value = object["output"] { + instance.output = value + } + if let value = object["usage"] { + instance.usage = try InvocationUsage.load(value, context: context.at("usage")) + } + if let value = object["assistantMessages"] { + instance.assistantMessages = try TypraRuntime.array(value, field: "assistantMessages").enumerated().map { try Message.load($1, context: context.at("assistantMessages").atIndex($0)) } + } + if let value = object["toolRequests"] { + instance.toolRequests = try TypraRuntime.array(value, field: "toolRequests").enumerated().map { try ModelToolRequest.load($1, context: context.at("toolRequests").atIndex($0)) } + } + if let value = object["nextContextState"] { + instance.nextContextState = try InvocationContextState.load(value, context: context.at("nextContextState")) + } + if let value = object["metadata"] { + instance.metadata = try TypraRuntime.dictionary(value, field: "metadata") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.output { + result["output"] = value + } + if let value = self.usage { + result["usage"] = try value.save(context) + } + if let value = self.assistantMessages { + result["assistantMessages"] = try value.map { try $0.save(context) } + } + if let value = self.toolRequests { + result["toolRequests"] = try value.map { try $0.save(context) } + } + if let value = self.nextContextState { + result["nextContextState"] = try value.save(context) + } + if let value = self.metadata { + result["metadata"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ModelInvocationResponse { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ModelInvocationResponse"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ModelInvocationResponse { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ModelInvocationResponse"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_reconciliation_state.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_reconciliation_state.swift new file mode 100644 index 000000000..9a5444a54 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_reconciliation_state.swift @@ -0,0 +1,74 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Durable state required to reconcile one indeterminate model invocation. Retained when a model effect completed with an unknown outcome so a resumed run can determine success or failure without re-invoking the provider. +public struct ModelReconciliationState: TypraModel { + public static let shorthandProperty: String? = nil + public var invocationId: String = "" + public var request: ModelInvocationRequest = ModelInvocationRequest() + public var failedAttempt: Int32 = 0 + public var message: String = "" + public var metadata: [String: Any]? = nil + + public init(invocationId: String = "", request: ModelInvocationRequest = ModelInvocationRequest(), failedAttempt: Int32 = 0, message: String = "", metadata: [String: Any]? = nil) { + self.invocationId = invocationId + self.request = request + self.failedAttempt = failedAttempt + self.message = message + self.metadata = metadata + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ModelReconciliationState { + let object = try TypraRuntime.object(data, typeName: "ModelReconciliationState") + var instance = ModelReconciliationState() + if let value = object["invocationId"] { + instance.invocationId = try TypraRuntime.string(value, field: "invocationId") + } + if object["request"] == nil || object["request"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("request").path + ": missing required field") + } + if let value = object["request"] { + instance.request = try ModelInvocationRequest.load(value, context: context.at("request")) + } + if let value = object["failedAttempt"] { + instance.failedAttempt = try TypraRuntime.int32(value, field: "failedAttempt") + } + if let value = object["message"] { + instance.message = try TypraRuntime.string(value, field: "message") + } + if let value = object["metadata"] { + instance.metadata = try TypraRuntime.dictionary(value, field: "metadata") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["invocationId"] = self.invocationId + result["request"] = try self.request.save(context) + result["failedAttempt"] = self.failedAttempt + result["message"] = self.message + if let value = self.metadata { + result["metadata"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ModelReconciliationState { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ModelReconciliationState"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ModelReconciliationState { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ModelReconciliationState"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_tool_request.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_tool_request.swift new file mode 100644 index 000000000..4992ec800 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_tool_request.swift @@ -0,0 +1,67 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// A normalized tool request returned by a model provider. Arguments remain JSON-shaped because providers may return a parsed object or a serialized JSON value before the host's tool binding is selected. +public struct ModelToolRequest: TypraModel { + public static let shorthandProperty: String? = nil + public var id: String = "" + public var name: String = "" + public var arguments: Any? = nil + public var metadata: [String: Any]? = nil + + public init(id: String = "", name: String = "", arguments: Any? = nil, metadata: [String: Any]? = nil) { + self.id = id + self.name = name + self.arguments = arguments + self.metadata = metadata + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ModelToolRequest { + let object = try TypraRuntime.object(data, typeName: "ModelToolRequest") + var instance = ModelToolRequest() + if let value = object["id"] { + instance.id = try TypraRuntime.string(value, field: "id") + } + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + if let value = object["arguments"] { + instance.arguments = value + } + if let value = object["metadata"] { + instance.metadata = try TypraRuntime.dictionary(value, field: "metadata") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["id"] = self.id + result["name"] = self.name + if let value = self.arguments { + result["arguments"] = value + } + if let value = self.metadata { + result["metadata"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ModelToolRequest { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ModelToolRequest"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ModelToolRequest { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ModelToolRequest"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_tool_result.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_tool_result.swift new file mode 100644 index 000000000..d171bbce9 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_tool_result.swift @@ -0,0 +1,95 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum ModelToolOutcome: String, Codable, CaseIterable { + case success = "success" + case failed = "failed" + case indeterminate = "indeterminate" + public static func parse(_ value: String) throws -> ModelToolOutcome { + switch value { + case "success": return .success + case "failed": return .failed + case "indeterminate": return .indeterminate + default: throw TypraRuntimeError.invalidEnum(type: "ModelToolOutcome", value: value) + } + } +} + +/// Normalized result of executing one model-requested tool. +public struct ModelToolResult: TypraModel { + public static let shorthandProperty: String? = nil + public var requestId: String = "" + public var name: String = "" + public var outcome: ModelToolOutcome = (try! ModelToolOutcome.parse("success")) + public var output: Any? = nil + public var errorKind: String? = nil + public var metadata: [String: Any]? = nil + + public init(requestId: String = "", name: String = "", outcome: ModelToolOutcome = (try! ModelToolOutcome.parse("success")), output: Any? = nil, errorKind: String? = nil, metadata: [String: Any]? = nil) { + self.requestId = requestId + self.name = name + self.outcome = outcome + self.output = output + self.errorKind = errorKind + self.metadata = metadata + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ModelToolResult { + let object = try TypraRuntime.object(data, typeName: "ModelToolResult") + var instance = ModelToolResult() + if let value = object["requestId"] { + instance.requestId = try TypraRuntime.string(value, field: "requestId") + } + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + if let value = object["outcome"] { + instance.outcome = try ModelToolOutcome.parse(try TypraRuntime.string(value, field: "outcome")) + } + if let value = object["output"] { + instance.output = value + } + if let value = object["errorKind"] { + instance.errorKind = try TypraRuntime.string(value, field: "errorKind") + } + if let value = object["metadata"] { + instance.metadata = try TypraRuntime.dictionary(value, field: "metadata") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["requestId"] = self.requestId + result["name"] = self.name + result["outcome"] = self.outcome.rawValue + if let value = self.output { + result["output"] = value + } + if let value = self.errorKind { + result["errorKind"] = value + } + if let value = self.metadata { + result["metadata"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ModelToolResult { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ModelToolResult"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ModelToolResult { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ModelToolResult"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/replay_journal_record.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/replay_journal_record.swift new file mode 100644 index 000000000..72e2b647e --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/replay_journal_record.swift @@ -0,0 +1,161 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum ReplayRecordKind: String, Codable, CaseIterable { + case session = "session" + case turn = "turn" + case summary = "summary" + public static func parse(_ value: String) throws -> ReplayRecordKind { + switch value { + case "session": return .session + case "turn": return .turn + case "summary": return .summary + default: throw TypraRuntimeError.invalidEnum(type: "ReplayRecordKind", value: value) + } + } +} + +public enum ReplayRecordStatus: String, Codable, CaseIterable { + case success = "success" + case error = "error" + case cancelled = "cancelled" + public static func parse(_ value: String) throws -> ReplayRecordStatus { + switch value { + case "success": return .success + case "error": return .error + case "cancelled": return .cancelled + default: throw TypraRuntimeError.invalidEnum(type: "ReplayRecordStatus", value: value) + } + } +} + +/// Stable, replay-comparable projection of a journal record. Runtime journal records may carry additional payload fields, durations, telemetry, or provider-specific data. Replay verification compares this normalized shape so deterministic orchestration semantics are mechanically shared across runtimes. +public struct ReplayJournalRecord: TypraModel { + public static let shorthandProperty: String? = nil + public var kind: ReplayRecordKind = (try! ReplayRecordKind.parse("session")) + public var type: String? = nil + public var sessionId: String? = nil + public var turnId: String? = nil + public var iteration: Int32? = nil + public var status: ReplayRecordStatus? = nil + public var requestId: String? = nil + public var toolName: String? = nil + public var success: Bool? = nil + public var errorKind: String? = nil + public var turns: Int32? = nil + public var checkpoints: Int32? = nil + + public init(kind: ReplayRecordKind = (try! ReplayRecordKind.parse("session")), type: String? = nil, sessionId: String? = nil, turnId: String? = nil, iteration: Int32? = nil, status: ReplayRecordStatus? = nil, requestId: String? = nil, toolName: String? = nil, success: Bool? = nil, errorKind: String? = nil, turns: Int32? = nil, checkpoints: Int32? = nil) { + self.kind = kind + self.type = type + self.sessionId = sessionId + self.turnId = turnId + self.iteration = iteration + self.status = status + self.requestId = requestId + self.toolName = toolName + self.success = success + self.errorKind = errorKind + self.turns = turns + self.checkpoints = checkpoints + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ReplayJournalRecord { + let object = try TypraRuntime.object(data, typeName: "ReplayJournalRecord") + var instance = ReplayJournalRecord() + if let value = object["kind"] { + instance.kind = try ReplayRecordKind.parse(try TypraRuntime.string(value, field: "kind")) + } + if let value = object["type"] { + instance.type = try TypraRuntime.string(value, field: "type") + } + if let value = object["sessionId"] { + instance.sessionId = try TypraRuntime.string(value, field: "sessionId") + } + if let value = object["turnId"] { + instance.turnId = try TypraRuntime.string(value, field: "turnId") + } + if let value = object["iteration"] { + instance.iteration = try TypraRuntime.int32(value, field: "iteration") + } + if let value = object["status"] { + instance.status = try ReplayRecordStatus.parse(try TypraRuntime.string(value, field: "status")) + } + if let value = object["requestId"] { + instance.requestId = try TypraRuntime.string(value, field: "requestId") + } + if let value = object["toolName"] { + instance.toolName = try TypraRuntime.string(value, field: "toolName") + } + if let value = object["success"] { + instance.success = try TypraRuntime.bool(value, field: "success") + } + if let value = object["errorKind"] { + instance.errorKind = try TypraRuntime.string(value, field: "errorKind") + } + if let value = object["turns"] { + instance.turns = try TypraRuntime.int32(value, field: "turns") + } + if let value = object["checkpoints"] { + instance.checkpoints = try TypraRuntime.int32(value, field: "checkpoints") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["kind"] = self.kind.rawValue + if let value = self.type { + result["type"] = value + } + if let value = self.sessionId { + result["sessionId"] = value + } + if let value = self.turnId { + result["turnId"] = value + } + if let value = self.iteration { + result["iteration"] = value + } + if let value = self.status { + result["status"] = value.rawValue + } + if let value = self.requestId { + result["requestId"] = value + } + if let value = self.toolName { + result["toolName"] = value + } + if let value = self.success { + result["success"] = value + } + if let value = self.errorKind { + result["errorKind"] = value + } + if let value = self.turns { + result["turns"] = value + } + if let value = self.checkpoints { + result["checkpoints"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ReplayJournalRecord { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ReplayJournalRecord"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ReplayJournalRecord { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ReplayJournalRecord"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/replay_mismatch.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/replay_mismatch.swift new file mode 100644 index 000000000..a7930ba12 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/replay_mismatch.swift @@ -0,0 +1,67 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// A single mismatch produced by replay verification. +public struct ReplayMismatch: TypraModel { + public static let shorthandProperty: String? = nil + public var index: Int32 = 0 + public var expected: ReplayJournalRecord? = nil + public var actual: ReplayJournalRecord? = nil + public var message: String = "" + + public init(index: Int32 = 0, expected: ReplayJournalRecord? = nil, actual: ReplayJournalRecord? = nil, message: String = "") { + self.index = index + self.expected = expected + self.actual = actual + self.message = message + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ReplayMismatch { + let object = try TypraRuntime.object(data, typeName: "ReplayMismatch") + var instance = ReplayMismatch() + if let value = object["index"] { + instance.index = try TypraRuntime.int32(value, field: "index") + } + if let value = object["expected"] { + instance.expected = try ReplayJournalRecord.load(value, context: context.at("expected")) + } + if let value = object["actual"] { + instance.actual = try ReplayJournalRecord.load(value, context: context.at("actual")) + } + if let value = object["message"] { + instance.message = try TypraRuntime.string(value, field: "message") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["index"] = self.index + if let value = self.expected { + result["expected"] = try value.save(context) + } + if let value = self.actual { + result["actual"] = try value.save(context) + } + result["message"] = self.message + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ReplayMismatch { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ReplayMismatch"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ReplayMismatch { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ReplayMismatch"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/replay_verification_request.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/replay_verification_request.swift new file mode 100644 index 000000000..43ea69419 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/replay_verification_request.swift @@ -0,0 +1,51 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Request accepted by a replay verifier implementation. +public struct ReplayVerificationRequest: TypraModel { + public static let shorthandProperty: String? = nil + public var expected: [ReplayJournalRecord] = [] + public var actual: [ReplayJournalRecord] = [] + + public init(expected: [ReplayJournalRecord] = [], actual: [ReplayJournalRecord] = []) { + self.expected = expected + self.actual = actual + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ReplayVerificationRequest { + let object = try TypraRuntime.object(data, typeName: "ReplayVerificationRequest") + var instance = ReplayVerificationRequest() + if let value = object["expected"] { + instance.expected = try TypraRuntime.array(value, field: "expected").enumerated().map { try ReplayJournalRecord.load($1, context: context.at("expected").atIndex($0)) } + } + if let value = object["actual"] { + instance.actual = try TypraRuntime.array(value, field: "actual").enumerated().map { try ReplayJournalRecord.load($1, context: context.at("actual").atIndex($0)) } + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["expected"] = try self.expected.map { try $0.save(context) } + result["actual"] = try self.actual.map { try $0.save(context) } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ReplayVerificationRequest { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ReplayVerificationRequest"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ReplayVerificationRequest { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ReplayVerificationRequest"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/replay_verification_result.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/replay_verification_result.swift new file mode 100644 index 000000000..874f06b95 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/replay_verification_result.swift @@ -0,0 +1,77 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum ReplayVerificationStatus: String, Codable, CaseIterable { + case passed = "passed" + case failed = "failed" + public static func parse(_ value: String) throws -> ReplayVerificationStatus { + switch value { + case "passed": return .passed + case "failed": return .failed + default: throw TypraRuntimeError.invalidEnum(type: "ReplayVerificationStatus", value: value) + } + } +} + +/// Result returned by a replay verifier implementation. +public struct ReplayVerificationResult: TypraModel { + public static let shorthandProperty: String? = nil + public var status: ReplayVerificationStatus = (try! ReplayVerificationStatus.parse("passed")) + public var mismatches: [ReplayMismatch]? = nil + public var expectedCount: Int32 = 0 + public var actualCount: Int32 = 0 + + public init(status: ReplayVerificationStatus = (try! ReplayVerificationStatus.parse("passed")), mismatches: [ReplayMismatch]? = nil, expectedCount: Int32 = 0, actualCount: Int32 = 0) { + self.status = status + self.mismatches = mismatches + self.expectedCount = expectedCount + self.actualCount = actualCount + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ReplayVerificationResult { + let object = try TypraRuntime.object(data, typeName: "ReplayVerificationResult") + var instance = ReplayVerificationResult() + if let value = object["status"] { + instance.status = try ReplayVerificationStatus.parse(try TypraRuntime.string(value, field: "status")) + } + if let value = object["mismatches"] { + instance.mismatches = try TypraRuntime.array(value, field: "mismatches").enumerated().map { try ReplayMismatch.load($1, context: context.at("mismatches").atIndex($0)) } + } + if let value = object["expectedCount"] { + instance.expectedCount = try TypraRuntime.int32(value, field: "expectedCount") + } + if let value = object["actualCount"] { + instance.actualCount = try TypraRuntime.int32(value, field: "actualCount") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["status"] = self.status.rawValue + if let value = self.mismatches { + result["mismatches"] = try value.map { try $0.save(context) } + } + result["expectedCount"] = self.expectedCount + result["actualCount"] = self.actualCount + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ReplayVerificationResult { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ReplayVerificationResult"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ReplayVerificationResult { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ReplayVerificationResult"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/resume_context.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/resume_context.swift new file mode 100644 index 000000000..43f9df48f --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/resume_context.swift @@ -0,0 +1,77 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Input that drives resuming a turn from a durable checkpoint. A host supplies this to restart an interrupted turn without duplicating a committed model or tool effect. +public struct ResumeContext: TypraModel { + public static let shorthandProperty: String? = nil + public var checkpoint: EngineCheckpoint = EngineCheckpoint() + public var maxIterations: Int32 = 0 + public var maxModelAttempts: Int32 = 0 + public var lastJournalSequence: Int64 = 0 + public var metadata: [String: Any]? = nil + + public init(checkpoint: EngineCheckpoint = EngineCheckpoint(), maxIterations: Int32 = 0, maxModelAttempts: Int32 = 0, lastJournalSequence: Int64 = 0, metadata: [String: Any]? = nil) { + self.checkpoint = checkpoint + self.maxIterations = maxIterations + self.maxModelAttempts = maxModelAttempts + self.lastJournalSequence = lastJournalSequence + self.metadata = metadata + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ResumeContext { + let object = try TypraRuntime.object(data, typeName: "ResumeContext") + var instance = ResumeContext() + if object["checkpoint"] == nil || object["checkpoint"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("checkpoint").path + ": missing required field") + } + if let value = object["checkpoint"] { + instance.checkpoint = try EngineCheckpoint.load(value, context: context.at("checkpoint")) + } + if let value = object["maxIterations"] { + instance.maxIterations = try TypraRuntime.int32(value, field: "maxIterations") + } + if let value = object["maxModelAttempts"] { + instance.maxModelAttempts = try TypraRuntime.int32(value, field: "maxModelAttempts") + } + if let value = object["lastJournalSequence"] { + instance.lastJournalSequence = try TypraRuntime.int64(value, field: "lastJournalSequence") + } + else { + instance.lastJournalSequence = 0 + } + if let value = object["metadata"] { + instance.metadata = try TypraRuntime.dictionary(value, field: "metadata") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["checkpoint"] = try self.checkpoint.save(context) + result["maxIterations"] = self.maxIterations + result["maxModelAttempts"] = self.maxModelAttempts + result["lastJournalSequence"] = self.lastJournalSequence + if let value = self.metadata { + result["metadata"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ResumeContext { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ResumeContext"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ResumeContext { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ResumeContext"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/retry_policy_request.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/retry_policy_request.swift new file mode 100644 index 000000000..c9bb8aa3f --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/retry_policy_request.swift @@ -0,0 +1,63 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Context supplied to the retry policy after a retryable model failure. +public struct RetryPolicyRequest: TypraModel { + public static let shorthandProperty: String? = nil + public var failedAttempts: Int32 = 0 + public var nextAttempt: Int32 = 0 + public var maxAttempts: Int32 = 0 + public var reason: String = "" + + public init(failedAttempts: Int32 = 0, nextAttempt: Int32 = 0, maxAttempts: Int32 = 0, reason: String = "") { + self.failedAttempts = failedAttempts + self.nextAttempt = nextAttempt + self.maxAttempts = maxAttempts + self.reason = reason + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> RetryPolicyRequest { + let object = try TypraRuntime.object(data, typeName: "RetryPolicyRequest") + var instance = RetryPolicyRequest() + if let value = object["failedAttempts"] { + instance.failedAttempts = try TypraRuntime.int32(value, field: "failedAttempts") + } + if let value = object["nextAttempt"] { + instance.nextAttempt = try TypraRuntime.int32(value, field: "nextAttempt") + } + if let value = object["maxAttempts"] { + instance.maxAttempts = try TypraRuntime.int32(value, field: "maxAttempts") + } + if let value = object["reason"] { + instance.reason = try TypraRuntime.string(value, field: "reason") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["failedAttempts"] = self.failedAttempts + result["nextAttempt"] = self.nextAttempt + result["maxAttempts"] = self.maxAttempts + result["reason"] = self.reason + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> RetryPolicyRequest { + return try load(TypraRuntime.jsonObject(from: json, typeName: "RetryPolicyRequest"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> RetryPolicyRequest { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "RetryPolicyRequest"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/run_turn_request.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/run_turn_request.swift new file mode 100644 index 000000000..ff17d9441 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/run_turn_request.swift @@ -0,0 +1,67 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Request accepted by a reference turn runner implementation. +public struct RunTurnRequest: TypraModel { + public static let shorthandProperty: String? = nil + public var sessionId: String = "" + public var turnId: String = "" + public var inputs: [String: Any]? = nil + public var options: TurnOptions? = nil + + public init(sessionId: String = "", turnId: String = "", inputs: [String: Any]? = nil, options: TurnOptions? = nil) { + self.sessionId = sessionId + self.turnId = turnId + self.inputs = inputs + self.options = options + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> RunTurnRequest { + let object = try TypraRuntime.object(data, typeName: "RunTurnRequest") + var instance = RunTurnRequest() + if let value = object["sessionId"] { + instance.sessionId = try TypraRuntime.string(value, field: "sessionId") + } + if let value = object["turnId"] { + instance.turnId = try TypraRuntime.string(value, field: "turnId") + } + if let value = object["inputs"] { + instance.inputs = try TypraRuntime.dictionary(value, field: "inputs") + } + if let value = object["options"] { + instance.options = try TurnOptions.load(value, context: context.at("options")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["sessionId"] = self.sessionId + result["turnId"] = self.turnId + if let value = self.inputs { + result["inputs"] = value + } + if let value = self.options { + result["options"] = try value.save(context) + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> RunTurnRequest { + return try load(TypraRuntime.jsonObject(from: json, typeName: "RunTurnRequest"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> RunTurnRequest { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "RunTurnRequest"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/run_turn_result.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/run_turn_result.swift new file mode 100644 index 000000000..9b478fc54 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/run_turn_result.swift @@ -0,0 +1,101 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum RunTurnStatus: String, Codable, CaseIterable { + case success = "success" + case error = "error" + case cancelled = "cancelled" + public static func parse(_ value: String) throws -> RunTurnStatus { + switch value { + case "success": return .success + case "error": return .error + case "cancelled": return .cancelled + default: throw TypraRuntimeError.invalidEnum(type: "RunTurnStatus", value: value) + } + } +} + +/// Result returned by a reference turn runner implementation. +public struct RunTurnResult: TypraModel { + public static let shorthandProperty: String? = nil + public var sessionId: String = "" + public var turnId: String = "" + public var status: RunTurnStatus = (try! RunTurnStatus.parse("success")) + public var output: Any? = nil + public var iterations: Int32 = 0 + public var toolResults: [HostToolResult]? = nil + public var checkpoints: [Checkpoint]? = nil + + public init(sessionId: String = "", turnId: String = "", status: RunTurnStatus = (try! RunTurnStatus.parse("success")), output: Any? = nil, iterations: Int32 = 0, toolResults: [HostToolResult]? = nil, checkpoints: [Checkpoint]? = nil) { + self.sessionId = sessionId + self.turnId = turnId + self.status = status + self.output = output + self.iterations = iterations + self.toolResults = toolResults + self.checkpoints = checkpoints + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> RunTurnResult { + let object = try TypraRuntime.object(data, typeName: "RunTurnResult") + var instance = RunTurnResult() + if let value = object["sessionId"] { + instance.sessionId = try TypraRuntime.string(value, field: "sessionId") + } + if let value = object["turnId"] { + instance.turnId = try TypraRuntime.string(value, field: "turnId") + } + if let value = object["status"] { + instance.status = try RunTurnStatus.parse(try TypraRuntime.string(value, field: "status")) + } + if let value = object["output"] { + instance.output = value + } + if let value = object["iterations"] { + instance.iterations = try TypraRuntime.int32(value, field: "iterations") + } + if let value = object["toolResults"] { + instance.toolResults = try TypraRuntime.array(value, field: "toolResults").enumerated().map { try HostToolResult.load($1, context: context.at("toolResults").atIndex($0)) } + } + if let value = object["checkpoints"] { + instance.checkpoints = try TypraRuntime.array(value, field: "checkpoints").enumerated().map { try Checkpoint.load($1, context: context.at("checkpoints").atIndex($0)) } + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["sessionId"] = self.sessionId + result["turnId"] = self.turnId + result["status"] = self.status.rawValue + if let value = self.output { + result["output"] = value + } + result["iterations"] = self.iterations + if let value = self.toolResults { + result["toolResults"] = try value.map { try $0.save(context) } + } + if let value = self.checkpoints { + result["checkpoints"] = try value.map { try $0.save(context) } + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> RunTurnResult { + return try load(TypraRuntime.jsonObject(from: json, typeName: "RunTurnResult"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> RunTurnResult { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "RunTurnResult"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/turn_commit.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/turn_commit.swift new file mode 100644 index 000000000..bc6609f2f --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/turn_commit.swift @@ -0,0 +1,116 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum EngineTurnStatus: String, Codable, CaseIterable { + case success = "success" + case failed = "failed" + case cancelled = "cancelled" + case reconciliationRequired = "reconciliation_required" + public static func parse(_ value: String) throws -> EngineTurnStatus { + switch value { + case "success": return .success + case "failed": return .failed + case "cancelled": return .cancelled + case "reconciliation_required": return .reconciliationRequired + default: throw TypraRuntimeError.invalidEnum(type: "EngineTurnStatus", value: value) + } + } +} + +/// The committed outcome of a turn handed to post-commit consumers. +public struct TurnCommit: TypraModel { + public static let shorthandProperty: String? = nil + public var sessionId: String = "" + public var turnId: String = "" + public var status: EngineTurnStatus = (try! EngineTurnStatus.parse("success")) + public var output: Any? = nil + public var messages: [Message] = [] + public var iterations: Int32 = 0 + public var lastSequence: Int64 = 0 + public var contextState: InvocationContextState = InvocationContextState() + public var modelReconciliation: ModelReconciliationState? = nil + + public init(sessionId: String = "", turnId: String = "", status: EngineTurnStatus = (try! EngineTurnStatus.parse("success")), output: Any? = nil, messages: [Message] = [], iterations: Int32 = 0, lastSequence: Int64 = 0, contextState: InvocationContextState = InvocationContextState(), modelReconciliation: ModelReconciliationState? = nil) { + self.sessionId = sessionId + self.turnId = turnId + self.status = status + self.output = output + self.messages = messages + self.iterations = iterations + self.lastSequence = lastSequence + self.contextState = contextState + self.modelReconciliation = modelReconciliation + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> TurnCommit { + let object = try TypraRuntime.object(data, typeName: "TurnCommit") + var instance = TurnCommit() + if let value = object["sessionId"] { + instance.sessionId = try TypraRuntime.string(value, field: "sessionId") + } + if let value = object["turnId"] { + instance.turnId = try TypraRuntime.string(value, field: "turnId") + } + if let value = object["status"] { + instance.status = try EngineTurnStatus.parse(try TypraRuntime.string(value, field: "status")) + } + if let value = object["output"] { + instance.output = value + } + if let value = object["messages"] { + instance.messages = try TypraRuntime.array(value, field: "messages").enumerated().map { try Message.load($1, context: context.at("messages").atIndex($0)) } + } + if let value = object["iterations"] { + instance.iterations = try TypraRuntime.int32(value, field: "iterations") + } + if let value = object["lastSequence"] { + instance.lastSequence = try TypraRuntime.int64(value, field: "lastSequence") + } + if object["contextState"] == nil || object["contextState"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("contextState").path + ": missing required field") + } + if let value = object["contextState"] { + instance.contextState = try InvocationContextState.load(value, context: context.at("contextState")) + } + if let value = object["modelReconciliation"] { + instance.modelReconciliation = try ModelReconciliationState.load(value, context: context.at("modelReconciliation")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["sessionId"] = self.sessionId + result["turnId"] = self.turnId + result["status"] = self.status.rawValue + if let value = self.output { + result["output"] = value + } + result["messages"] = try self.messages.map { try $0.save(context) } + result["iterations"] = self.iterations + result["lastSequence"] = self.lastSequence + result["contextState"] = try self.contextState.save(context) + if let value = self.modelReconciliation { + result["modelReconciliation"] = try value.save(context) + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> TurnCommit { + return try load(TypraRuntime.jsonObject(from: json, typeName: "TurnCommit"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> TurnCommit { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "TurnCommit"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/turn_engine_result.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/turn_engine_result.swift new file mode 100644 index 000000000..f442ac42e --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/turn_engine_result.swift @@ -0,0 +1,72 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// The result returned by the live turn engine. +public struct TurnEngineResult: TypraModel { + public static let shorthandProperty: String? = nil + public var commit: TurnCommit = TurnCommit() + public var snapshots: [ModelInvocationContextSnapshot]? = nil + public var toolResults: [ModelToolResult]? = nil + public var postCommitError: String? = nil + + public init(commit: TurnCommit = TurnCommit(), snapshots: [ModelInvocationContextSnapshot]? = nil, toolResults: [ModelToolResult]? = nil, postCommitError: String? = nil) { + self.commit = commit + self.snapshots = snapshots + self.toolResults = toolResults + self.postCommitError = postCommitError + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> TurnEngineResult { + let object = try TypraRuntime.object(data, typeName: "TurnEngineResult") + var instance = TurnEngineResult() + if object["commit"] == nil || object["commit"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("commit").path + ": missing required field") + } + if let value = object["commit"] { + instance.commit = try TurnCommit.load(value, context: context.at("commit")) + } + if let value = object["snapshots"] { + instance.snapshots = try TypraRuntime.array(value, field: "snapshots").enumerated().map { try ModelInvocationContextSnapshot.load($1, context: context.at("snapshots").atIndex($0)) } + } + if let value = object["toolResults"] { + instance.toolResults = try TypraRuntime.array(value, field: "toolResults").enumerated().map { try ModelToolResult.load($1, context: context.at("toolResults").atIndex($0)) } + } + if let value = object["postCommitError"] { + instance.postCommitError = try TypraRuntime.string(value, field: "postCommitError") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["commit"] = try self.commit.save(context) + if let value = self.snapshots { + result["snapshots"] = try value.map { try $0.save(context) } + } + if let value = self.toolResults { + result["toolResults"] = try value.map { try $0.save(context) } + } + if let value = self.postCommitError { + result["postCommitError"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> TurnEngineResult { + return try load(TypraRuntime.jsonObject(from: json, typeName: "TurnEngineResult"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> TurnEngineResult { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "TurnEngineResult"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/turn_model_request.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/turn_model_request.swift new file mode 100644 index 000000000..bb0cdf308 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/turn_model_request.swift @@ -0,0 +1,81 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Request passed by the reference turn runner to the injected model callback. The runner owns deterministic orchestration semantics; model/provider-specific execution stays behind this callback boundary. +public struct TurnModelRequest: TypraModel { + public static let shorthandProperty: String? = nil + public var sessionId: String = "" + public var turnId: String = "" + public var iteration: Int32 = 0 + public var inputs: [String: Any]? = nil + public var options: TurnOptions? = nil + public var toolResults: [HostToolResult]? = nil + + public init(sessionId: String = "", turnId: String = "", iteration: Int32 = 0, inputs: [String: Any]? = nil, options: TurnOptions? = nil, toolResults: [HostToolResult]? = nil) { + self.sessionId = sessionId + self.turnId = turnId + self.iteration = iteration + self.inputs = inputs + self.options = options + self.toolResults = toolResults + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> TurnModelRequest { + let object = try TypraRuntime.object(data, typeName: "TurnModelRequest") + var instance = TurnModelRequest() + if let value = object["sessionId"] { + instance.sessionId = try TypraRuntime.string(value, field: "sessionId") + } + if let value = object["turnId"] { + instance.turnId = try TypraRuntime.string(value, field: "turnId") + } + if let value = object["iteration"] { + instance.iteration = try TypraRuntime.int32(value, field: "iteration") + } + if let value = object["inputs"] { + instance.inputs = try TypraRuntime.dictionary(value, field: "inputs") + } + if let value = object["options"] { + instance.options = try TurnOptions.load(value, context: context.at("options")) + } + if let value = object["toolResults"] { + instance.toolResults = try TypraRuntime.array(value, field: "toolResults").enumerated().map { try HostToolResult.load($1, context: context.at("toolResults").atIndex($0)) } + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["sessionId"] = self.sessionId + result["turnId"] = self.turnId + result["iteration"] = self.iteration + if let value = self.inputs { + result["inputs"] = value + } + if let value = self.options { + result["options"] = try value.save(context) + } + if let value = self.toolResults { + result["toolResults"] = try value.map { try $0.save(context) } + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> TurnModelRequest { + return try load(TypraRuntime.jsonObject(from: json, typeName: "TurnModelRequest"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> TurnModelRequest { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "TurnModelRequest"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/turn_model_response.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/turn_model_response.swift new file mode 100644 index 000000000..b6091414f --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/turn_model_response.swift @@ -0,0 +1,71 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Response returned by the injected model callback to the reference turn runner. +public struct TurnModelResponse: TypraModel { + public static let shorthandProperty: String? = nil + public var output: Any? = nil + public var usage: InvocationUsage? = nil + public var toolRequests: [HostToolRequest]? = nil + public var checkpointState: [String: Any]? = nil + + public init(output: Any? = nil, usage: InvocationUsage? = nil, toolRequests: [HostToolRequest]? = nil, checkpointState: [String: Any]? = nil) { + self.output = output + self.usage = usage + self.toolRequests = toolRequests + self.checkpointState = checkpointState + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> TurnModelResponse { + let object = try TypraRuntime.object(data, typeName: "TurnModelResponse") + var instance = TurnModelResponse() + if let value = object["output"] { + instance.output = value + } + if let value = object["usage"] { + instance.usage = try InvocationUsage.load(value, context: context.at("usage")) + } + if let value = object["toolRequests"] { + instance.toolRequests = try TypraRuntime.array(value, field: "toolRequests").enumerated().map { try HostToolRequest.load($1, context: context.at("toolRequests").atIndex($0)) } + } + if let value = object["checkpointState"] { + instance.checkpointState = try TypraRuntime.dictionary(value, field: "checkpointState") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.output { + result["output"] = value + } + if let value = self.usage { + result["usage"] = try value.save(context) + } + if let value = self.toolRequests { + result["toolRequests"] = try value.map { try $0.save(context) } + } + if let value = self.checkpointState { + result["checkpointState"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> TurnModelResponse { + return try load(TypraRuntime.jsonObject(from: json, typeName: "TurnModelResponse"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> TurnModelResponse { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "TurnModelResponse"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/turn_options.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/turn_options.swift new file mode 100644 index 000000000..977eef770 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/turn_options.swift @@ -0,0 +1,95 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Configuration for the agent loop's turn() function. Controls iteration limits, retry policy, context management, and execution behavior. Runtimes accept these as either a TurnOptions object or individual keyword/named parameters — the TypeSpec model defines the canonical field set. +public struct TurnOptions: TypraModel { + public static let shorthandProperty: String? = nil + public var maxIterations: Int32? = nil + public var maxLlmRetries: Int32? = nil + public var contextBudget: Int32? = nil + public var parallelToolCalls: Bool? = nil + public var raw: Bool? = nil + public var turn: Int32? = nil + public var compaction: CompactionConfig? = nil + + public init(maxIterations: Int32? = nil, maxLlmRetries: Int32? = nil, contextBudget: Int32? = nil, parallelToolCalls: Bool? = nil, raw: Bool? = nil, turn: Int32? = nil, compaction: CompactionConfig? = nil) { + self.maxIterations = maxIterations + self.maxLlmRetries = maxLlmRetries + self.contextBudget = contextBudget + self.parallelToolCalls = parallelToolCalls + self.raw = raw + self.turn = turn + self.compaction = compaction + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> TurnOptions { + let object = try TypraRuntime.object(data, typeName: "TurnOptions") + var instance = TurnOptions() + if let value = object["maxIterations"] { + instance.maxIterations = try TypraRuntime.int32(value, field: "maxIterations") + } + if let value = object["maxLlmRetries"] { + instance.maxLlmRetries = try TypraRuntime.int32(value, field: "maxLlmRetries") + } + if let value = object["contextBudget"] { + instance.contextBudget = try TypraRuntime.int32(value, field: "contextBudget") + } + if let value = object["parallelToolCalls"] { + instance.parallelToolCalls = try TypraRuntime.bool(value, field: "parallelToolCalls") + } + if let value = object["raw"] { + instance.raw = try TypraRuntime.bool(value, field: "raw") + } + if let value = object["turn"] { + instance.turn = try TypraRuntime.int32(value, field: "turn") + } + if let value = object["compaction"] { + instance.compaction = try CompactionConfig.load(value, context: context.at("compaction")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.maxIterations { + result["maxIterations"] = value + } + if let value = self.maxLlmRetries { + result["maxLlmRetries"] = value + } + if let value = self.contextBudget { + result["contextBudget"] = value + } + if let value = self.parallelToolCalls { + result["parallelToolCalls"] = value + } + if let value = self.raw { + result["raw"] = value + } + if let value = self.turn { + result["turn"] = value + } + if let value = self.compaction { + result["compaction"] = try value.save(context) + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> TurnOptions { + return try load(TypraRuntime.jsonObject(from: json, typeName: "TurnOptions"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> TurnOptions { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "TurnOptions"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Streaming/stream_options.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Streaming/stream_options.swift new file mode 100644 index 000000000..43542e6f5 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Streaming/stream_options.swift @@ -0,0 +1,47 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Options controlling streaming behavior for LLM API calls. Passed alongside the model options when streaming is enabled. +public struct StreamOptions: TypraModel { + public static let shorthandProperty: String? = nil + public var includeUsage: Bool? = nil + + public init(includeUsage: Bool? = nil) { + self.includeUsage = includeUsage + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> StreamOptions { + let object = try TypraRuntime.object(data, typeName: "StreamOptions") + var instance = StreamOptions() + if let value = object["includeUsage"] { + instance.includeUsage = try TypraRuntime.bool(value, field: "includeUsage") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + if let value = self.includeUsage { + result["includeUsage"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> StreamOptions { + return try load(TypraRuntime.jsonObject(from: json, typeName: "StreamOptions"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> StreamOptions { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "StreamOptions"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Templates/format_config.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Templates/format_config.swift new file mode 100644 index 000000000..95986e41e --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Templates/format_config.swift @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Template format definition +public struct FormatConfig: TypraModel { + public static let shorthandProperty: String? = "kind" + public var kind: String = "*" + public var strict: Bool? = nil + public var options: [String: Any]? = nil + + public init(kind: String = "*", strict: Bool? = nil, options: [String: Any]? = nil) { + self.kind = kind + self.strict = strict + self.options = options + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> FormatConfig { + if let scalar = data as? String { + var instance = FormatConfig() + instance.kind = try TypraRuntime.string(scalar, field: "kind") + return instance + } + let object = try TypraRuntime.object(data, typeName: "FormatConfig") + var instance = FormatConfig() + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "*" + } + if let value = object["strict"] { + instance.strict = try TypraRuntime.bool(value, field: "strict") + } + if let value = object["options"] { + instance.options = try TypraRuntime.dictionary(value, field: "options") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["kind"] = self.kind + if let value = self.strict { + result["strict"] = value + } + if let value = self.options { + result["options"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> FormatConfig { + return try load(TypraRuntime.jsonObject(from: json, typeName: "FormatConfig"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> FormatConfig { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "FormatConfig"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Templates/parser_config.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Templates/parser_config.swift new file mode 100644 index 000000000..356a11cd4 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Templates/parser_config.swift @@ -0,0 +1,61 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Template parser definition +public struct ParserConfig: TypraModel { + public static let shorthandProperty: String? = "kind" + public var kind: String = "*" + public var options: [String: Any]? = nil + + public init(kind: String = "*", options: [String: Any]? = nil) { + self.kind = kind + self.options = options + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ParserConfig { + if let scalar = data as? String { + var instance = ParserConfig() + instance.kind = try TypraRuntime.string(scalar, field: "kind") + return instance + } + let object = try TypraRuntime.object(data, typeName: "ParserConfig") + var instance = ParserConfig() + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "*" + } + if let value = object["options"] { + instance.options = try TypraRuntime.dictionary(value, field: "options") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["kind"] = self.kind + if let value = self.options { + result["options"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ParserConfig { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ParserConfig"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ParserConfig { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ParserConfig"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Templates/template.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Templates/template.swift new file mode 100644 index 000000000..5ff052b63 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Templates/template.swift @@ -0,0 +1,57 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Template model for defining prompt templates. This model specifies the rendering engine used for slot filling prompts, the parser used to process the rendered template into API-compatible format, and additional options for the template engine. It allows for the creation of reusable templates that can be filled with dynamic data and processed to generate prompts for AI models. +public struct Template: TypraModel { + public static let shorthandProperty: String? = nil + public var format: FormatConfig = FormatConfig() + public var parser: ParserConfig = ParserConfig() + + public init(format: FormatConfig = FormatConfig(), parser: ParserConfig = ParserConfig()) { + self.format = format + self.parser = parser + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> Template { + let object = try TypraRuntime.object(data, typeName: "Template") + var instance = Template() + if object["format"] == nil || object["format"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("format").path + ": missing required field") + } + if let value = object["format"] { + instance.format = try FormatConfig.load(value, context: context.at("format")) + } + if object["parser"] == nil || object["parser"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("parser").path + ": missing required field") + } + if let value = object["parser"] { + instance.parser = try ParserConfig.load(value, context: context.at("parser")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["format"] = try self.format.save(context) + result["parser"] = try self.parser.save(context) + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> Template { + return try load(TypraRuntime.jsonObject(from: json, typeName: "Template"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> Template { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "Template"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tooling/binding.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tooling/binding.swift new file mode 100644 index 000000000..034c0a922 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tooling/binding.swift @@ -0,0 +1,59 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Represents a binding between an input property and a tool parameter. +public struct Binding: TypraModel { + public static let shorthandProperty: String? = "input" + public var name: String = "" + public var input: String = "" + + public init(name: String = "", input: String = "") { + self.name = name + self.input = input + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> Binding { + if let scalar = data as? String { + var instance = Binding() + instance.input = try TypraRuntime.string(scalar, field: "input") + return instance + } + let object = try TypraRuntime.object(data, typeName: "Binding") + var instance = Binding() + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + else { + instance.name = "" + } + if let value = object["input"] { + instance.input = try TypraRuntime.string(value, field: "input") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["name"] = self.name + result["input"] = self.input + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> Binding { + return try load(TypraRuntime.jsonObject(from: json, typeName: "Binding"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> Binding { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "Binding"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tooling/mcp_approval_mode.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tooling/mcp_approval_mode.swift new file mode 100644 index 000000000..ae2306119 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tooling/mcp_approval_mode.swift @@ -0,0 +1,80 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum McpApprovalModeKind: String, Codable, CaseIterable { + case always = "always" + case never = "never" + case specify = "specify" + public static func parse(_ value: String) throws -> McpApprovalModeKind { + switch value { + case "always": return .always + case "never": return .never + case "specify": return .specify + default: throw TypraRuntimeError.invalidEnum(type: "McpApprovalModeKind", value: value) + } + } +} + +/// The approval mode for MCP server tools. When kind is "specify", use alwaysRequireApprovalTools and neverRequireApprovalTools to control per-tool approval. For "always" and "never", those fields are ignored. +public struct McpApprovalMode: TypraModel { + public static let shorthandProperty: String? = "kind" + public var kind: McpApprovalModeKind = (try! McpApprovalModeKind.parse("always")) + public var alwaysRequireApprovalTools: [String]? = nil + public var neverRequireApprovalTools: [String]? = nil + + public init(kind: McpApprovalModeKind = (try! McpApprovalModeKind.parse("always")), alwaysRequireApprovalTools: [String]? = nil, neverRequireApprovalTools: [String]? = nil) { + self.kind = kind + self.alwaysRequireApprovalTools = alwaysRequireApprovalTools + self.neverRequireApprovalTools = neverRequireApprovalTools + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> McpApprovalMode { + if let scalar = data as? String { + var instance = McpApprovalMode() + instance.kind = try McpApprovalModeKind.parse(try TypraRuntime.string(scalar, field: "kind")) + return instance + } + let object = try TypraRuntime.object(data, typeName: "McpApprovalMode") + var instance = McpApprovalMode() + if let value = object["kind"] { + instance.kind = try McpApprovalModeKind.parse(try TypraRuntime.string(value, field: "kind")) + } + if let value = object["alwaysRequireApprovalTools"] { + instance.alwaysRequireApprovalTools = try TypraRuntime.array(value, field: "alwaysRequireApprovalTools").map { try TypraRuntime.string($0, field: "alwaysRequireApprovalTools") } + } + if let value = object["neverRequireApprovalTools"] { + instance.neverRequireApprovalTools = try TypraRuntime.array(value, field: "neverRequireApprovalTools").map { try TypraRuntime.string($0, field: "neverRequireApprovalTools") } + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["kind"] = self.kind.rawValue + if let value = self.alwaysRequireApprovalTools { + result["alwaysRequireApprovalTools"] = value + } + if let value = self.neverRequireApprovalTools { + result["neverRequireApprovalTools"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> McpApprovalMode { + return try load(TypraRuntime.jsonObject(from: json, typeName: "McpApprovalMode"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> McpApprovalMode { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "McpApprovalMode"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tooling/tool.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tooling/tool.swift new file mode 100644 index 000000000..dd4a80bfc --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tooling/tool.swift @@ -0,0 +1,767 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public enum Tool: TypraModel { + case functionTool(FunctionTool) + case mcpTool(McpTool) + case openApiTool(OpenApiTool) + case promptyTool(PromptyTool) + case customTool(CustomTool, [String: Any]) + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> Tool { + let normalizedData: Any = data + let object = try TypraRuntime.object(normalizedData, typeName: "Tool") + let discriminator = try TypraRuntime.string(object["kind"] ?? NSNull(), field: "kind") + if discriminator.isEmpty { + throw TypraRuntimeError.invalidField(field: context.at("kind").path, expected: "non-blank string") + } + switch discriminator { + case "function": return .functionTool(try FunctionTool.load(normalizedData, context: context)) + case "mcp": return .mcpTool(try McpTool.load(normalizedData, context: context)) + case "openapi": return .openApiTool(try OpenApiTool.load(normalizedData, context: context)) + case "prompty": return .promptyTool(try PromptyTool.load(normalizedData, context: context)) + default: return .customTool(try CustomTool.load(normalizedData, context: context), object) + } + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + switch self { + case .functionTool(let value): return try value.save(context) + case .mcpTool(let value): return try value.save(context) + case .openApiTool(let value): return try value.save(context) + case .promptyTool(let value): return try value.save(context) + case .customTool(let value, let raw): + var result = raw + for (key, item) in try value.save(context) { + result[key] = item + } + return result + } + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> Tool { + return try load(TypraRuntime.jsonObject(from: json, typeName: "Tool"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> Tool { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "Tool"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +/// Represents a local function tool. +public struct FunctionTool: TypraModel { + public static let shorthandProperty: String? = nil + public var name: String = "" + public var kind: String = "function" + public var description: String? = nil + public var bindings: [Binding]? = nil + public var parameters: [Property] = [] + public var strict: Bool? = nil + + public init(name: String = "", kind: String = "function", description: String? = nil, bindings: [Binding]? = nil, parameters: [Property] = [], strict: Bool? = nil) { + self.name = name + self.kind = kind + self.description = description + self.bindings = bindings + self.parameters = parameters + self.strict = strict + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> FunctionTool { + let object = try TypraRuntime.object(data, typeName: "FunctionTool") + var instance = FunctionTool() + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + else { + instance.name = "" + } + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "function" + } + if let value = object["description"] { + instance.description = try TypraRuntime.string(value, field: "description") + } + if let value = object["bindings"] { + instance.bindings = try loadBindings(value, context: context.at("bindings")) + } + if let value = object["parameters"] { + instance.parameters = try loadParameters(value, context: context.at("parameters")) + } + if let value = object["strict"] { + instance.strict = try TypraRuntime.bool(value, field: "strict") + } + return instance + } + + private static func loadBindings(_ data: Any, context: LoadContext) throws -> [Binding] { + if let values = data as? [Any] { + return try values.enumerated().map { try Binding.load($1, context: context.atIndex($0)) } + } + let values = try TypraRuntime.dictionary(data, field: "bindings") + return try values.keys.sorted().map { name in + let value = values[name]! + if value is [Any] { + throw TypraRuntimeError.invalidField(field: context.at(name).path, expected: "non-array named collection entry; received category array") + } + var itemData: [String: Any] + if let object = value as? [String: Any] { + itemData = object + } else { + itemData = try Binding.load(value, context: context).save() + } + itemData["name"] = name + return try Binding.load(itemData, context: context.at(name)) + } + } + + private static func saveBindings(_ items: [Binding], context: SaveContext) throws -> Any { + let serialized = try items.map { try $0.save(context) } + if context.collectionFormat == "array" { + return serialized + } + var names = Set() + for itemData in serialized { + guard let name = itemData["name"] as? String, !name.isEmpty, names.insert(name).inserted else { + return serialized + } + } + var result: [String: Any] = [:] + for (_, originalData) in zip(items, serialized) { + var itemData = originalData + let name = itemData.removeValue(forKey: "name") as! String + if context.useShorthand, itemData.count == 1, let value = itemData["input"] { + result[name] = value + } else { + result[name] = itemData + } + } + return result + } + + private static func loadParameters(_ data: Any, context: LoadContext) throws -> [Property] { + if let values = data as? [Any] { + return try values.enumerated().map { try Property.load($1, context: context.atIndex($0)) } + } + let values = try TypraRuntime.dictionary(data, field: "parameters") + return try values.keys.sorted().map { name in + let value = values[name]! + if value is [Any] { + throw TypraRuntimeError.invalidField(field: context.at(name).path, expected: "non-array named collection entry; received category array") + } + var itemData: [String: Any] + if let object = value as? [String: Any] { + itemData = object + } else { + itemData = try Property.load(value, context: context).save() + } + itemData["name"] = name + return try Property.load(itemData, context: context.at(name)) + } + } + + private static func saveParameters(_ items: [Property], context: SaveContext) throws -> Any { + let serialized = try items.map { try $0.save(context) } + if context.collectionFormat == "array" { + return serialized + } + var names = Set() + for itemData in serialized { + guard let name = itemData["name"] as? String, !name.isEmpty, names.insert(name).inserted else { + return serialized + } + } + var result: [String: Any] = [:] + for (_, originalData) in zip(items, serialized) { + var itemData = originalData + let name = itemData.removeValue(forKey: "name") as! String + result[name] = itemData + } + return result + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["name"] = self.name + result["kind"] = self.kind + if let value = self.description { + result["description"] = value + } + if let value = self.bindings { + result["bindings"] = try Self.saveBindings(value, context: context) + } + result["parameters"] = try Self.saveParameters(self.parameters, context: context) + if let value = self.strict { + result["strict"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> FunctionTool { + return try load(TypraRuntime.jsonObject(from: json, typeName: "FunctionTool"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> FunctionTool { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "FunctionTool"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +/// Represents a generic server tool that runs on a server This tool kind is designed for operations that require server-side execution It may include features such as authentication, data storage, and long-running processes This tool kind is ideal for tasks that involve complex computations or access to secure resources Server tools can be used to offload heavy processing from client applications +public struct CustomTool: TypraModel { + public static let shorthandProperty: String? = nil + public var name: String = "" + public var kind: String = "*" + public var description: String? = nil + public var bindings: [Binding]? = nil + public var connection: Connection = .unknown([:]) + public var options: [String: Any] = [:] + + public init(name: String = "", kind: String = "*", description: String? = nil, bindings: [Binding]? = nil, connection: Connection = .unknown([:]), options: [String: Any] = [:]) { + self.name = name + self.kind = kind + self.description = description + self.bindings = bindings + self.connection = connection + self.options = options + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> CustomTool { + let object = try TypraRuntime.object(data, typeName: "CustomTool") + var instance = CustomTool() + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + else { + instance.name = "" + } + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "*" + } + if let value = object["description"] { + instance.description = try TypraRuntime.string(value, field: "description") + } + if let value = object["bindings"] { + instance.bindings = try loadBindings(value, context: context.at("bindings")) + } + if object["connection"] == nil || object["connection"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("connection").path + ": missing required field") + } + if let value = object["connection"] { + instance.connection = try Connection.load(value, context: context.at("connection")) + } + if let value = object["options"] { + instance.options = try TypraRuntime.dictionary(value, field: "options") + } + return instance + } + + private static func loadBindings(_ data: Any, context: LoadContext) throws -> [Binding] { + if let values = data as? [Any] { + return try values.enumerated().map { try Binding.load($1, context: context.atIndex($0)) } + } + let values = try TypraRuntime.dictionary(data, field: "bindings") + return try values.keys.sorted().map { name in + let value = values[name]! + if value is [Any] { + throw TypraRuntimeError.invalidField(field: context.at(name).path, expected: "non-array named collection entry; received category array") + } + var itemData: [String: Any] + if let object = value as? [String: Any] { + itemData = object + } else { + itemData = try Binding.load(value, context: context).save() + } + itemData["name"] = name + return try Binding.load(itemData, context: context.at(name)) + } + } + + private static func saveBindings(_ items: [Binding], context: SaveContext) throws -> Any { + let serialized = try items.map { try $0.save(context) } + if context.collectionFormat == "array" { + return serialized + } + var names = Set() + for itemData in serialized { + guard let name = itemData["name"] as? String, !name.isEmpty, names.insert(name).inserted else { + return serialized + } + } + var result: [String: Any] = [:] + for (_, originalData) in zip(items, serialized) { + var itemData = originalData + let name = itemData.removeValue(forKey: "name") as! String + if context.useShorthand, itemData.count == 1, let value = itemData["input"] { + result[name] = value + } else { + result[name] = itemData + } + } + return result + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["name"] = self.name + result["kind"] = self.kind + if let value = self.description { + result["description"] = value + } + if let value = self.bindings { + result["bindings"] = try Self.saveBindings(value, context: context) + } + result["connection"] = try self.connection.save(context) + result["options"] = self.options + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> CustomTool { + return try load(TypraRuntime.jsonObject(from: json, typeName: "CustomTool"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> CustomTool { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "CustomTool"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +/// The MCP Server tool. +public struct McpTool: TypraModel { + public static let shorthandProperty: String? = nil + public var name: String = "" + public var kind: String = "mcp" + public var description: String? = nil + public var bindings: [Binding]? = nil + public var connection: Connection = .unknown([:]) + public var serverName: String = "" + public var serverDescription: String? = nil + public var approvalMode: McpApprovalMode? = nil + public var allowedTools: [String]? = nil + + public init(name: String = "", kind: String = "mcp", description: String? = nil, bindings: [Binding]? = nil, connection: Connection = .unknown([:]), serverName: String = "", serverDescription: String? = nil, approvalMode: McpApprovalMode? = nil, allowedTools: [String]? = nil) { + self.name = name + self.kind = kind + self.description = description + self.bindings = bindings + self.connection = connection + self.serverName = serverName + self.serverDescription = serverDescription + self.approvalMode = approvalMode + self.allowedTools = allowedTools + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> McpTool { + let object = try TypraRuntime.object(data, typeName: "McpTool") + var instance = McpTool() + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + else { + instance.name = "" + } + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "mcp" + } + if let value = object["description"] { + instance.description = try TypraRuntime.string(value, field: "description") + } + if let value = object["bindings"] { + instance.bindings = try loadBindings(value, context: context.at("bindings")) + } + if object["connection"] == nil || object["connection"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("connection").path + ": missing required field") + } + if let value = object["connection"] { + instance.connection = try Connection.load(value, context: context.at("connection")) + } + if let value = object["serverName"] { + instance.serverName = try TypraRuntime.string(value, field: "serverName") + } + if let value = object["serverDescription"] { + instance.serverDescription = try TypraRuntime.string(value, field: "serverDescription") + } + if let value = object["approvalMode"] { + instance.approvalMode = try McpApprovalMode.load(value, context: context.at("approvalMode")) + } + if let value = object["allowedTools"] { + instance.allowedTools = try TypraRuntime.array(value, field: "allowedTools").map { try TypraRuntime.string($0, field: "allowedTools") } + } + return instance + } + + private static func loadBindings(_ data: Any, context: LoadContext) throws -> [Binding] { + if let values = data as? [Any] { + return try values.enumerated().map { try Binding.load($1, context: context.atIndex($0)) } + } + let values = try TypraRuntime.dictionary(data, field: "bindings") + return try values.keys.sorted().map { name in + let value = values[name]! + if value is [Any] { + throw TypraRuntimeError.invalidField(field: context.at(name).path, expected: "non-array named collection entry; received category array") + } + var itemData: [String: Any] + if let object = value as? [String: Any] { + itemData = object + } else { + itemData = try Binding.load(value, context: context).save() + } + itemData["name"] = name + return try Binding.load(itemData, context: context.at(name)) + } + } + + private static func saveBindings(_ items: [Binding], context: SaveContext) throws -> Any { + let serialized = try items.map { try $0.save(context) } + if context.collectionFormat == "array" { + return serialized + } + var names = Set() + for itemData in serialized { + guard let name = itemData["name"] as? String, !name.isEmpty, names.insert(name).inserted else { + return serialized + } + } + var result: [String: Any] = [:] + for (_, originalData) in zip(items, serialized) { + var itemData = originalData + let name = itemData.removeValue(forKey: "name") as! String + if context.useShorthand, itemData.count == 1, let value = itemData["input"] { + result[name] = value + } else { + result[name] = itemData + } + } + return result + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["name"] = self.name + result["kind"] = self.kind + if let value = self.description { + result["description"] = value + } + if let value = self.bindings { + result["bindings"] = try Self.saveBindings(value, context: context) + } + result["connection"] = try self.connection.save(context) + result["serverName"] = self.serverName + if let value = self.serverDescription { + result["serverDescription"] = value + } + if let value = self.approvalMode { + result["approvalMode"] = try value.save(context) + } + if let value = self.allowedTools { + result["allowedTools"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> McpTool { + return try load(TypraRuntime.jsonObject(from: json, typeName: "McpTool"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> McpTool { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "McpTool"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +public struct OpenApiTool: TypraModel { + public static let shorthandProperty: String? = nil + public var name: String = "" + public var kind: String = "openapi" + public var description: String? = nil + public var bindings: [Binding]? = nil + public var connection: Connection = .unknown([:]) + public var specification: String = "" + + public init(name: String = "", kind: String = "openapi", description: String? = nil, bindings: [Binding]? = nil, connection: Connection = .unknown([:]), specification: String = "") { + self.name = name + self.kind = kind + self.description = description + self.bindings = bindings + self.connection = connection + self.specification = specification + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> OpenApiTool { + let object = try TypraRuntime.object(data, typeName: "OpenApiTool") + var instance = OpenApiTool() + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + else { + instance.name = "" + } + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "openapi" + } + if let value = object["description"] { + instance.description = try TypraRuntime.string(value, field: "description") + } + if let value = object["bindings"] { + instance.bindings = try loadBindings(value, context: context.at("bindings")) + } + if object["connection"] == nil || object["connection"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("connection").path + ": missing required field") + } + if let value = object["connection"] { + instance.connection = try Connection.load(value, context: context.at("connection")) + } + if let value = object["specification"] { + instance.specification = try TypraRuntime.string(value, field: "specification") + } + return instance + } + + private static func loadBindings(_ data: Any, context: LoadContext) throws -> [Binding] { + if let values = data as? [Any] { + return try values.enumerated().map { try Binding.load($1, context: context.atIndex($0)) } + } + let values = try TypraRuntime.dictionary(data, field: "bindings") + return try values.keys.sorted().map { name in + let value = values[name]! + if value is [Any] { + throw TypraRuntimeError.invalidField(field: context.at(name).path, expected: "non-array named collection entry; received category array") + } + var itemData: [String: Any] + if let object = value as? [String: Any] { + itemData = object + } else { + itemData = try Binding.load(value, context: context).save() + } + itemData["name"] = name + return try Binding.load(itemData, context: context.at(name)) + } + } + + private static func saveBindings(_ items: [Binding], context: SaveContext) throws -> Any { + let serialized = try items.map { try $0.save(context) } + if context.collectionFormat == "array" { + return serialized + } + var names = Set() + for itemData in serialized { + guard let name = itemData["name"] as? String, !name.isEmpty, names.insert(name).inserted else { + return serialized + } + } + var result: [String: Any] = [:] + for (_, originalData) in zip(items, serialized) { + var itemData = originalData + let name = itemData.removeValue(forKey: "name") as! String + if context.useShorthand, itemData.count == 1, let value = itemData["input"] { + result[name] = value + } else { + result[name] = itemData + } + } + return result + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["name"] = self.name + result["kind"] = self.kind + if let value = self.description { + result["description"] = value + } + if let value = self.bindings { + result["bindings"] = try Self.saveBindings(value, context: context) + } + result["connection"] = try self.connection.save(context) + result["specification"] = self.specification + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> OpenApiTool { + return try load(TypraRuntime.jsonObject(from: json, typeName: "OpenApiTool"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> OpenApiTool { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "OpenApiTool"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} + +/// A tool that references another .prompty file to be invoked as a tool. The child prompty is executed as a single prompt invocation. Nested agent loops are intentionally not started from PromptyTool. +public struct PromptyTool: TypraModel { + public static let shorthandProperty: String? = nil + public var name: String = "" + public var kind: String = "prompty" + public var description: String? = nil + public var bindings: [Binding]? = nil + public var path: String = "" + public var mode: String = "single" + + public init(name: String = "", kind: String = "prompty", description: String? = nil, bindings: [Binding]? = nil, path: String = "", mode: String = "single") { + self.name = name + self.kind = kind + self.description = description + self.bindings = bindings + self.path = path + self.mode = mode + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> PromptyTool { + let object = try TypraRuntime.object(data, typeName: "PromptyTool") + var instance = PromptyTool() + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + else { + instance.name = "" + } + if let value = object["kind"] { + instance.kind = try TypraRuntime.string(value, field: "kind") + } + else { + instance.kind = "prompty" + } + if let value = object["description"] { + instance.description = try TypraRuntime.string(value, field: "description") + } + if let value = object["bindings"] { + instance.bindings = try loadBindings(value, context: context.at("bindings")) + } + if let value = object["path"] { + instance.path = try TypraRuntime.string(value, field: "path") + } + if let value = object["mode"] { + instance.mode = try TypraRuntime.string(value, field: "mode") + } + else { + instance.mode = "single" + } + return instance + } + + private static func loadBindings(_ data: Any, context: LoadContext) throws -> [Binding] { + if let values = data as? [Any] { + return try values.enumerated().map { try Binding.load($1, context: context.atIndex($0)) } + } + let values = try TypraRuntime.dictionary(data, field: "bindings") + return try values.keys.sorted().map { name in + let value = values[name]! + if value is [Any] { + throw TypraRuntimeError.invalidField(field: context.at(name).path, expected: "non-array named collection entry; received category array") + } + var itemData: [String: Any] + if let object = value as? [String: Any] { + itemData = object + } else { + itemData = try Binding.load(value, context: context).save() + } + itemData["name"] = name + return try Binding.load(itemData, context: context.at(name)) + } + } + + private static func saveBindings(_ items: [Binding], context: SaveContext) throws -> Any { + let serialized = try items.map { try $0.save(context) } + if context.collectionFormat == "array" { + return serialized + } + var names = Set() + for itemData in serialized { + guard let name = itemData["name"] as? String, !name.isEmpty, names.insert(name).inserted else { + return serialized + } + } + var result: [String: Any] = [:] + for (_, originalData) in zip(items, serialized) { + var itemData = originalData + let name = itemData.removeValue(forKey: "name") as! String + if context.useShorthand, itemData.count == 1, let value = itemData["input"] { + result[name] = value + } else { + result[name] = itemData + } + } + return result + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["name"] = self.name + result["kind"] = self.kind + if let value = self.description { + result["description"] = value + } + if let value = self.bindings { + result["bindings"] = try Self.saveBindings(value, context: context) + } + result["path"] = self.path + result["mode"] = self.mode + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> PromptyTool { + return try load(TypraRuntime.jsonObject(from: json, typeName: "PromptyTool"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> PromptyTool { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "PromptyTool"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tooling/tool_context.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tooling/tool_context.swift new file mode 100644 index 000000000..e13aef3ff --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tooling/tool_context.swift @@ -0,0 +1,53 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Context passed to tool handlers during agent loop execution. Provides access to the agent configuration, current conversation state, and arbitrary metadata for tool implementations that need broader context. +public struct ToolContext: TypraModel { + public static let shorthandProperty: String? = nil + public var messages: [Message] = [] + public var metadata: [String: Any]? = nil + + public init(messages: [Message] = [], metadata: [String: Any]? = nil) { + self.messages = messages + self.metadata = metadata + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ToolContext { + let object = try TypraRuntime.object(data, typeName: "ToolContext") + var instance = ToolContext() + if let value = object["messages"] { + instance.messages = try TypraRuntime.array(value, field: "messages").enumerated().map { try Message.load($1, context: context.at("messages").atIndex($0)) } + } + if let value = object["metadata"] { + instance.metadata = try TypraRuntime.dictionary(value, field: "metadata") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["messages"] = try self.messages.map { try $0.save(context) } + if let value = self.metadata { + result["metadata"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ToolContext { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ToolContext"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ToolContext { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ToolContext"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tooling/tool_dispatch_result.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tooling/tool_dispatch_result.swift new file mode 100644 index 000000000..2814e1e52 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tooling/tool_dispatch_result.swift @@ -0,0 +1,60 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// The result of dispatching a single tool call. Pairs the tool call identifier with the tool's name and result for correlation in the agent loop's message assembly. +public struct ToolDispatchResult: TypraModel { + public static let shorthandProperty: String? = nil + public var toolCallId: String = "" + public var name: String = "" + public var result: ToolResult = ToolResult() + + public init(toolCallId: String = "", name: String = "", result: ToolResult = ToolResult()) { + self.toolCallId = toolCallId + self.name = name + self.result = result + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> ToolDispatchResult { + let object = try TypraRuntime.object(data, typeName: "ToolDispatchResult") + var instance = ToolDispatchResult() + if let value = object["toolCallId"] { + instance.toolCallId = try TypraRuntime.string(value, field: "toolCallId") + } + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + if object["result"] == nil || object["result"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("result").path + ": missing required field") + } + if let value = object["result"] { + instance.result = try ToolResult.load(value, context: context.at("result")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["toolCallId"] = self.toolCallId + result["name"] = self.name + result["result"] = try self.result.save(context) + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> ToolDispatchResult { + return try load(TypraRuntime.jsonObject(from: json, typeName: "ToolDispatchResult"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> ToolDispatchResult { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "ToolDispatchResult"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tracing/trace_file.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tracing/trace_file.swift new file mode 100644 index 000000000..6e37deac7 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tracing/trace_file.swift @@ -0,0 +1,60 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// The top-level .tracy file structure written by the file backend (§3.6.1). +public struct TraceFile: TypraModel { + public static let shorthandProperty: String? = nil + public var runtime: String = "" + public var version: String = "" + public var trace: TraceSpan = TraceSpan() + + public init(runtime: String = "", version: String = "", trace: TraceSpan = TraceSpan()) { + self.runtime = runtime + self.version = version + self.trace = trace + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> TraceFile { + let object = try TypraRuntime.object(data, typeName: "TraceFile") + var instance = TraceFile() + if let value = object["runtime"] { + instance.runtime = try TypraRuntime.string(value, field: "runtime") + } + if let value = object["version"] { + instance.version = try TypraRuntime.string(value, field: "version") + } + if object["trace"] == nil || object["trace"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("trace").path + ": missing required field") + } + if let value = object["trace"] { + instance.trace = try TraceSpan.load(value, context: context.at("trace")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["runtime"] = self.runtime + result["version"] = self.version + result["trace"] = try self.trace.save(context) + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> TraceFile { + return try load(TypraRuntime.jsonObject(from: json, typeName: "TraceFile"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> TraceFile { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "TraceFile"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tracing/trace_span.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tracing/trace_span.swift new file mode 100644 index 000000000..563ae23f4 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tracing/trace_span.swift @@ -0,0 +1,110 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// A single trace span capturing one pipeline stage or function invocation. Spans nest via the `__frames` field to form a tree representing the full execution (§3.6.1). +public struct TraceSpan: TypraModel { + public static let shorthandProperty: String? = nil + public var name: String = "" + public var time: TraceTime = TraceTime() + public var signature: String? = nil + public var inputs: [String: Any]? = nil + public var output: Any? = nil + public var error: String? = nil + public var usage: TokenUsage? = nil + public var attributes: [String: Any]? = nil + public var frames: [Any]? = nil + + public init(name: String = "", time: TraceTime = TraceTime(), signature: String? = nil, inputs: [String: Any]? = nil, output: Any? = nil, error: String? = nil, usage: TokenUsage? = nil, attributes: [String: Any]? = nil, frames: [Any]? = nil) { + self.name = name + self.time = time + self.signature = signature + self.inputs = inputs + self.output = output + self.error = error + self.usage = usage + self.attributes = attributes + self.frames = frames + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> TraceSpan { + let object = try TypraRuntime.object(data, typeName: "TraceSpan") + var instance = TraceSpan() + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + if object["__time"] == nil || object["__time"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("__time").path + ": missing required field") + } + if let value = object["__time"] { + instance.time = try TraceTime.load(value, context: context.at("__time")) + } + if let value = object["signature"] { + instance.signature = try TypraRuntime.string(value, field: "signature") + } + if let value = object["inputs"] { + instance.inputs = try TypraRuntime.dictionary(value, field: "inputs") + } + if let value = object["output"] { + instance.output = value + } + if let value = object["error"] { + instance.error = try TypraRuntime.string(value, field: "error") + } + if let value = object["__usage"] { + instance.usage = try TokenUsage.load(value, context: context.at("__usage")) + } + if let value = object["attributes"] { + instance.attributes = try TypraRuntime.dictionary(value, field: "attributes") + } + if let value = object["__frames"] { + instance.frames = try TypraRuntime.array(value, field: "__frames").map { $0 } + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["name"] = self.name + result["__time"] = try self.time.save(context) + if let value = self.signature { + result["signature"] = value + } + if let value = self.inputs { + result["inputs"] = value + } + if let value = self.output { + result["output"] = value + } + if let value = self.error { + result["error"] = value + } + if let value = self.usage { + result["__usage"] = try value.save(context) + } + if let value = self.attributes { + result["attributes"] = value + } + if let value = self.frames { + result["__frames"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> TraceSpan { + return try load(TypraRuntime.jsonObject(from: json, typeName: "TraceSpan"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> TraceSpan { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "TraceSpan"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tracing/trace_time.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tracing/trace_time.swift new file mode 100644 index 000000000..3a0960e0e --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tracing/trace_time.swift @@ -0,0 +1,57 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Timing information for a trace span. +public struct TraceTime: TypraModel { + public static let shorthandProperty: String? = nil + public var start: String = "" + public var end: String = "" + public var duration: Double = 0 + + public init(start: String = "", end: String = "", duration: Double = 0) { + self.start = start + self.end = end + self.duration = duration + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> TraceTime { + let object = try TypraRuntime.object(data, typeName: "TraceTime") + var instance = TraceTime() + if let value = object["start"] { + instance.start = try TypraRuntime.string(value, field: "start") + } + if let value = object["end"] { + instance.end = try TypraRuntime.string(value, field: "end") + } + if let value = object["duration"] { + instance.duration = try TypraRuntime.double(value, field: "duration") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["start"] = self.start + result["end"] = self.end + result["duration"] = self.duration + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> TraceTime { + return try load(TypraRuntime.jsonObject(from: json, typeName: "TraceTime"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> TraceTime { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "TraceTime"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/TypraRuntime.swift b/runtime/swift/prompty-model/Sources/PromptyModel/TypraRuntime.swift new file mode 100644 index 000000000..37e1e27a7 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/TypraRuntime.swift @@ -0,0 +1,170 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import Yams + +public struct LoadContext { + public let path: String + public init(path: String = "") { self.path = path } + public func at(_ segment: String) -> LoadContext { LoadContext(path: path.isEmpty ? segment : "\(path).\(segment)") } + public func atIndex(_ index: Int) -> LoadContext { LoadContext(path: "\(path)[\(index)]") } +} + +public struct SaveContext { + public let collectionFormat: String + public let useShorthand: Bool + + public init(collectionFormat: String = "object", useShorthand: Bool = true) { + self.collectionFormat = collectionFormat == "array" ? "array" : "object" + self.useShorthand = useShorthand + } +} + +public enum TypraRuntimeError: Error, CustomStringConvertible { + case invalidObject(type: String) + case invalidField(field: String, expected: String) + case invalidEnum(type: String, value: String) + case unknownDiscriminator(type: String, field: String, value: String) + case unsupported(String) + + public var description: String { + switch self { + case .invalidObject(let type): return "Expected object for \(type)." + case .invalidField(let field, let expected): return "Expected \(expected) for field \(field)." + case .invalidEnum(let type, let value): return "Invalid \(type) value: \(value)." + case .unknownDiscriminator(let type, let field, let value): return "Unknown \(type) discriminator \(field)=\(value)." + case .unsupported(let message): return message + } + } +} + +public protocol TypraModel { + static func load(_ data: Any, context: LoadContext) throws -> Self + func save(_ context: SaveContext) throws -> [String: Any] +} + +public enum TypraRuntime { + public static func object(_ data: Any, typeName: String) throws -> [String: Any] { + guard let object = data as? [String: Any] else { throw TypraRuntimeError.invalidObject(type: typeName) } + return object + } + + public static func dictionary(_ data: Any, field: String) throws -> [String: Any] { + guard let object = data as? [String: Any] else { throw TypraRuntimeError.invalidField(field: field, expected: "dictionary") } + return object + } + + public static func array(_ data: Any, field: String) throws -> [Any] { + guard let array = data as? [Any] else { throw TypraRuntimeError.invalidField(field: field, expected: "array") } + return array + } + + public static func string(_ data: Any, field: String) throws -> String { + guard let value = data as? String else { throw TypraRuntimeError.invalidField(field: field, expected: "string") } + return value + } + + public static func bool(_ data: Any, field: String) throws -> Bool { + if let value = data as? Bool { return value } + if let number = data as? NSNumber, isBoolNumber(number) { return number.boolValue } + throw TypraRuntimeError.invalidField(field: field, expected: "boolean") + } + + public static func double(_ data: Any, field: String) throws -> Double { + if let number = data as? NSNumber { + if isBoolNumber(number) { throw TypraRuntimeError.invalidField(field: field, expected: "number") } + return number.doubleValue + } + if let decimal = data as? Decimal { return NSDecimalNumber(decimal: decimal).doubleValue } + if let value = data as? Double { return value } + if let value = data as? Float { return Double(value) } + if let value = data as? Int { return Double(value) } + if let value = data as? Int64 { return Double(value) } + if let value = data as? UInt64 { return Double(value) } + throw TypraRuntimeError.invalidField(field: field, expected: "number") + } + + public static func float(_ data: Any, field: String) throws -> Float { + return Float(try double(data, field: field)) + } + + public static func int(_ data: Any, field: String) throws -> Int { + let value = try int64(data, field: field) + guard value >= Int64(Int.min) && value <= Int64(Int.max) else { throw TypraRuntimeError.invalidField(field: field, expected: "integer") } + return Int(value) + } + + public static func int32(_ data: Any, field: String) throws -> Int32 { + let value = try int64(data, field: field) + guard value >= Int64(Int32.min) && value <= Int64(Int32.max) else { throw TypraRuntimeError.invalidField(field: field, expected: "int32") } + return Int32(value) + } + + public static func int64(_ data: Any, field: String) throws -> Int64 { + if let number = data as? NSNumber { + if isBoolNumber(number) { throw TypraRuntimeError.invalidField(field: field, expected: "integer") } + return try exactInt64(number, field: field) + } + if let decimal = data as? Decimal { return try exactInt64(NSDecimalNumber(decimal: decimal), field: field) } + if let value = data as? Int64 { return value } + if let value = data as? Int { return Int64(value) } + if let value = data as? Int32 { return Int64(value) } + if let value = data as? UInt64 { + guard value <= UInt64(Int64.max) else { throw TypraRuntimeError.invalidField(field: field, expected: "int64") } + return Int64(value) + } + if let value = data as? UInt { + guard value <= UInt(Int64.max) else { throw TypraRuntimeError.invalidField(field: field, expected: "int64") } + return Int64(value) + } + if let value = data as? UInt32 { return Int64(value) } + throw TypraRuntimeError.invalidField(field: field, expected: "integer") + } + + private static func exactInt64(_ number: NSNumber, field: String) throws -> Int64 { + switch String(cString: number.objCType) { + case "s", "i", "l", "q": + return number.int64Value + case "C", "S", "I", "L", "Q": + let value = number.uint64Value + guard value <= UInt64(Int64.max) else { throw TypraRuntimeError.invalidField(field: field, expected: "int64") } + return Int64(value) + default: + break + } + var decimal = number.decimalValue + var minimum = Decimal(string: "-9223372036854775808")! + var maximum = Decimal(string: "9223372036854775807")! + guard NSDecimalCompare(&decimal, &minimum) != .orderedAscending && NSDecimalCompare(&decimal, &maximum) != .orderedDescending else { throw TypraRuntimeError.invalidField(field: field, expected: "int64") } + var rounded = Decimal() + NSDecimalRound(&rounded, &decimal, 0, .plain) + guard NSDecimalCompare(&rounded, &decimal) == .orderedSame else { throw TypraRuntimeError.invalidField(field: field, expected: "integer") } + guard let parsed = Int64(NSDecimalNumber(decimal: rounded).stringValue) else { throw TypraRuntimeError.invalidField(field: field, expected: "int64") } + return parsed + } + + public static func isBoolNumber(_ number: NSNumber) -> Bool { + let type = String(cString: number.objCType) + return type == "c" || type == "B" + } + + public static func jsonObject(from json: String, typeName: String) throws -> Any { + guard let data = json.data(using: .utf8) else { throw TypraRuntimeError.invalidObject(type: typeName) } + return try JSONSerialization.jsonObject(with: data, options: []) + } + + public static func jsonString(from value: Any) throws -> String { + let data = try JSONSerialization.data(withJSONObject: value, options: [.sortedKeys]) + return String(data: data, encoding: .utf8) ?? "{}" + } + + public static func yamlObject(from yaml: String, typeName: String) throws -> Any { + guard let value = try Yams.load(yaml: yaml) else { throw TypraRuntimeError.invalidObject(type: typeName) } + return value + } + + public static func yamlString(from value: Any) throws -> String { + return try Yams.dump(object: value) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_image_block.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_image_block.swift new file mode 100644 index 000000000..39d7fc750 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_image_block.swift @@ -0,0 +1,57 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// An image content block using base64-encoded data. Anthropic requires images as base64 with an explicit media type. +public struct AnthropicImageBlock: TypraModel { + public static let shorthandProperty: String? = nil + public var type: String = "image" + public var source: AnthropicImageSource = AnthropicImageSource() + + public init(type: String = "image", source: AnthropicImageSource = AnthropicImageSource()) { + self.type = type + self.source = source + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> AnthropicImageBlock { + let object = try TypraRuntime.object(data, typeName: "AnthropicImageBlock") + var instance = AnthropicImageBlock() + if let value = object["type"] { + instance.type = try TypraRuntime.string(value, field: "type") + } + else { + instance.type = "image" + } + if object["source"] == nil || object["source"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("source").path + ": missing required field") + } + if let value = object["source"] { + instance.source = try AnthropicImageSource.load(value, context: context.at("source")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["type"] = self.type + result["source"] = try self.source.save(context) + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> AnthropicImageBlock { + return try load(TypraRuntime.jsonObject(from: json, typeName: "AnthropicImageBlock"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> AnthropicImageBlock { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "AnthropicImageBlock"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_image_source.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_image_source.swift new file mode 100644 index 000000000..735ef4377 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_image_source.swift @@ -0,0 +1,60 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Source descriptor for an Anthropic base64 image. +public struct AnthropicImageSource: TypraModel { + public static let shorthandProperty: String? = nil + public var type: String = "base64" + public var mediaType: String = "" + public var data: String = "" + + public init(type: String = "base64", mediaType: String = "", data: String = "") { + self.type = type + self.mediaType = mediaType + self.data = data + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> AnthropicImageSource { + let object = try TypraRuntime.object(data, typeName: "AnthropicImageSource") + var instance = AnthropicImageSource() + if let value = object["type"] { + instance.type = try TypraRuntime.string(value, field: "type") + } + else { + instance.type = "base64" + } + if let value = object["media_type"] { + instance.mediaType = try TypraRuntime.string(value, field: "media_type") + } + if let value = object["data"] { + instance.data = try TypraRuntime.string(value, field: "data") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["type"] = self.type + result["media_type"] = self.mediaType + result["data"] = self.data + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> AnthropicImageSource { + return try load(TypraRuntime.jsonObject(from: json, typeName: "AnthropicImageSource"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> AnthropicImageSource { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "AnthropicImageSource"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_messages_request.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_messages_request.swift new file mode 100644 index 000000000..35d566da1 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_messages_request.swift @@ -0,0 +1,105 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// The full request body for the Anthropic Messages API (§7.5). +public struct AnthropicMessagesRequest: TypraModel { + public static let shorthandProperty: String? = nil + public var model: String = "" + public var messages: [AnthropicWireMessage] = [] + public var maxTokens: Int32 = 0 + public var system: String? = nil + public var temperature: Float? = nil + public var topP: Float? = nil + public var topK: Int32? = nil + public var stopSequences: [String]? = nil + public var tools: [AnthropicToolDefinition]? = nil + + public init(model: String = "", messages: [AnthropicWireMessage] = [], maxTokens: Int32 = 0, system: String? = nil, temperature: Float? = nil, topP: Float? = nil, topK: Int32? = nil, stopSequences: [String]? = nil, tools: [AnthropicToolDefinition]? = nil) { + self.model = model + self.messages = messages + self.maxTokens = maxTokens + self.system = system + self.temperature = temperature + self.topP = topP + self.topK = topK + self.stopSequences = stopSequences + self.tools = tools + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> AnthropicMessagesRequest { + let object = try TypraRuntime.object(data, typeName: "AnthropicMessagesRequest") + var instance = AnthropicMessagesRequest() + if let value = object["model"] { + instance.model = try TypraRuntime.string(value, field: "model") + } + if let value = object["messages"] { + instance.messages = try TypraRuntime.array(value, field: "messages").enumerated().map { try AnthropicWireMessage.load($1, context: context.at("messages").atIndex($0)) } + } + if let value = object["max_tokens"] { + instance.maxTokens = try TypraRuntime.int32(value, field: "max_tokens") + } + if let value = object["system"] { + instance.system = try TypraRuntime.string(value, field: "system") + } + if let value = object["temperature"] { + instance.temperature = try TypraRuntime.float(value, field: "temperature") + } + if let value = object["top_p"] { + instance.topP = try TypraRuntime.float(value, field: "top_p") + } + if let value = object["top_k"] { + instance.topK = try TypraRuntime.int32(value, field: "top_k") + } + if let value = object["stop_sequences"] { + instance.stopSequences = try TypraRuntime.array(value, field: "stop_sequences").map { try TypraRuntime.string($0, field: "stop_sequences") } + } + if let value = object["tools"] { + instance.tools = try TypraRuntime.array(value, field: "tools").enumerated().map { try AnthropicToolDefinition.load($1, context: context.at("tools").atIndex($0)) } + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["model"] = self.model + result["messages"] = try self.messages.map { try $0.save(context) } + result["max_tokens"] = self.maxTokens + if let value = self.system { + result["system"] = value + } + if let value = self.temperature { + result["temperature"] = value + } + if let value = self.topP { + result["top_p"] = value + } + if let value = self.topK { + result["top_k"] = value + } + if let value = self.stopSequences { + result["stop_sequences"] = value + } + if let value = self.tools { + result["tools"] = try value.map { try $0.save(context) } + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> AnthropicMessagesRequest { + return try load(TypraRuntime.jsonObject(from: json, typeName: "AnthropicMessagesRequest"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> AnthropicMessagesRequest { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "AnthropicMessagesRequest"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_messages_response.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_messages_response.swift new file mode 100644 index 000000000..4d352ce82 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_messages_response.swift @@ -0,0 +1,90 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// The response body from the Anthropic Messages API. +public struct AnthropicMessagesResponse: TypraModel { + public static let shorthandProperty: String? = nil + public var id: String = "" + public var type: String = "message" + public var role: String = "assistant" + public var content: [Any] = [] + public var model: String = "" + public var stopReason: String = "" + public var usage: AnthropicUsage = AnthropicUsage() + + public init(id: String = "", type: String = "message", role: String = "assistant", content: [Any] = [], model: String = "", stopReason: String = "", usage: AnthropicUsage = AnthropicUsage()) { + self.id = id + self.type = type + self.role = role + self.content = content + self.model = model + self.stopReason = stopReason + self.usage = usage + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> AnthropicMessagesResponse { + let object = try TypraRuntime.object(data, typeName: "AnthropicMessagesResponse") + var instance = AnthropicMessagesResponse() + if let value = object["id"] { + instance.id = try TypraRuntime.string(value, field: "id") + } + if let value = object["type"] { + instance.type = try TypraRuntime.string(value, field: "type") + } + else { + instance.type = "message" + } + if let value = object["role"] { + instance.role = try TypraRuntime.string(value, field: "role") + } + else { + instance.role = "assistant" + } + if let value = object["content"] { + instance.content = try TypraRuntime.array(value, field: "content").map { $0 } + } + if let value = object["model"] { + instance.model = try TypraRuntime.string(value, field: "model") + } + if let value = object["stop_reason"] { + instance.stopReason = try TypraRuntime.string(value, field: "stop_reason") + } + if object["usage"] == nil || object["usage"] is NSNull { + throw TypraRuntimeError.unsupported(context.at("usage").path + ": missing required field") + } + if let value = object["usage"] { + instance.usage = try AnthropicUsage.load(value, context: context.at("usage")) + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["id"] = self.id + result["type"] = self.type + result["role"] = self.role + result["content"] = self.content + result["model"] = self.model + result["stop_reason"] = self.stopReason + result["usage"] = try self.usage.save(context) + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> AnthropicMessagesResponse { + return try load(TypraRuntime.jsonObject(from: json, typeName: "AnthropicMessagesResponse"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> AnthropicMessagesResponse { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "AnthropicMessagesResponse"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_text_block.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_text_block.swift new file mode 100644 index 000000000..c0945fb93 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_text_block.swift @@ -0,0 +1,54 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// A text content block in Anthropic's array-of-blocks message format. +public struct AnthropicTextBlock: TypraModel { + public static let shorthandProperty: String? = nil + public var type: String = "text" + public var text: String = "" + + public init(type: String = "text", text: String = "") { + self.type = type + self.text = text + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> AnthropicTextBlock { + let object = try TypraRuntime.object(data, typeName: "AnthropicTextBlock") + var instance = AnthropicTextBlock() + if let value = object["type"] { + instance.type = try TypraRuntime.string(value, field: "type") + } + else { + instance.type = "text" + } + if let value = object["text"] { + instance.text = try TypraRuntime.string(value, field: "text") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["type"] = self.type + result["text"] = self.text + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> AnthropicTextBlock { + return try load(TypraRuntime.jsonObject(from: json, typeName: "AnthropicTextBlock"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> AnthropicTextBlock { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "AnthropicTextBlock"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_tool_definition.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_tool_definition.swift new file mode 100644 index 000000000..49e55827c --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_tool_definition.swift @@ -0,0 +1,59 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// A tool definition in Anthropic's format. Unlike OpenAI which wraps tools in `{type: "function", function: {...}}`, Anthropic uses a flat structure with `input_schema` (§7.5). +public struct AnthropicToolDefinition: TypraModel { + public static let shorthandProperty: String? = nil + public var name: String = "" + public var description: String? = nil + public var inputSchema: [String: Any] = [:] + + public init(name: String = "", description: String? = nil, inputSchema: [String: Any] = [:]) { + self.name = name + self.description = description + self.inputSchema = inputSchema + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> AnthropicToolDefinition { + let object = try TypraRuntime.object(data, typeName: "AnthropicToolDefinition") + var instance = AnthropicToolDefinition() + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + if let value = object["description"] { + instance.description = try TypraRuntime.string(value, field: "description") + } + if let value = object["input_schema"] { + instance.inputSchema = try TypraRuntime.dictionary(value, field: "input_schema") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["name"] = self.name + if let value = self.description { + result["description"] = value + } + result["input_schema"] = self.inputSchema + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> AnthropicToolDefinition { + return try load(TypraRuntime.jsonObject(from: json, typeName: "AnthropicToolDefinition"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> AnthropicToolDefinition { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "AnthropicToolDefinition"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_tool_result_block.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_tool_result_block.swift new file mode 100644 index 000000000..08dd58b6d --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_tool_result_block.swift @@ -0,0 +1,60 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// A tool result content block sent back to the API with the tool's output. +public struct AnthropicToolResultBlock: TypraModel { + public static let shorthandProperty: String? = nil + public var type: String = "tool_result" + public var toolUseId: String = "" + public var content: String = "" + + public init(type: String = "tool_result", toolUseId: String = "", content: String = "") { + self.type = type + self.toolUseId = toolUseId + self.content = content + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> AnthropicToolResultBlock { + let object = try TypraRuntime.object(data, typeName: "AnthropicToolResultBlock") + var instance = AnthropicToolResultBlock() + if let value = object["type"] { + instance.type = try TypraRuntime.string(value, field: "type") + } + else { + instance.type = "tool_result" + } + if let value = object["tool_use_id"] { + instance.toolUseId = try TypraRuntime.string(value, field: "tool_use_id") + } + if let value = object["content"] { + instance.content = try TypraRuntime.string(value, field: "content") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["type"] = self.type + result["tool_use_id"] = self.toolUseId + result["content"] = self.content + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> AnthropicToolResultBlock { + return try load(TypraRuntime.jsonObject(from: json, typeName: "AnthropicToolResultBlock"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> AnthropicToolResultBlock { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "AnthropicToolResultBlock"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_tool_use_block.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_tool_use_block.swift new file mode 100644 index 000000000..de499b454 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_tool_use_block.swift @@ -0,0 +1,66 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// A tool use content block returned in an assistant message when the model wants to invoke a tool. +public struct AnthropicToolUseBlock: TypraModel { + public static let shorthandProperty: String? = nil + public var type: String = "tool_use" + public var id: String = "" + public var name: String = "" + public var input: [String: Any] = [:] + + public init(type: String = "tool_use", id: String = "", name: String = "", input: [String: Any] = [:]) { + self.type = type + self.id = id + self.name = name + self.input = input + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> AnthropicToolUseBlock { + let object = try TypraRuntime.object(data, typeName: "AnthropicToolUseBlock") + var instance = AnthropicToolUseBlock() + if let value = object["type"] { + instance.type = try TypraRuntime.string(value, field: "type") + } + else { + instance.type = "tool_use" + } + if let value = object["id"] { + instance.id = try TypraRuntime.string(value, field: "id") + } + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + if let value = object["input"] { + instance.input = try TypraRuntime.dictionary(value, field: "input") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["type"] = self.type + result["id"] = self.id + result["name"] = self.name + result["input"] = self.input + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> AnthropicToolUseBlock { + return try load(TypraRuntime.jsonObject(from: json, typeName: "AnthropicToolUseBlock"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> AnthropicToolUseBlock { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "AnthropicToolUseBlock"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_usage.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_usage.swift new file mode 100644 index 000000000..427d40593 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_usage.swift @@ -0,0 +1,51 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// Usage statistics returned in an Anthropic Messages API response. +public struct AnthropicUsage: TypraModel { + public static let shorthandProperty: String? = nil + public var inputTokens: Int32 = 0 + public var outputTokens: Int32 = 0 + + public init(inputTokens: Int32 = 0, outputTokens: Int32 = 0) { + self.inputTokens = inputTokens + self.outputTokens = outputTokens + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> AnthropicUsage { + let object = try TypraRuntime.object(data, typeName: "AnthropicUsage") + var instance = AnthropicUsage() + if let value = object["input_tokens"] { + instance.inputTokens = try TypraRuntime.int32(value, field: "input_tokens") + } + if let value = object["output_tokens"] { + instance.outputTokens = try TypraRuntime.int32(value, field: "output_tokens") + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["input_tokens"] = self.inputTokens + result["output_tokens"] = self.outputTokens + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> AnthropicUsage { + return try load(TypraRuntime.jsonObject(from: json, typeName: "AnthropicUsage"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> AnthropicUsage { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "AnthropicUsage"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_wire_message.swift b/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_wire_message.swift new file mode 100644 index 000000000..4b388eef5 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_wire_message.swift @@ -0,0 +1,51 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// A single message in the Anthropic Messages API wire format. Anthropic always uses the array-of-blocks form for content, even when there is only one text block (§7.5). +public struct AnthropicWireMessage: TypraModel { + public static let shorthandProperty: String? = nil + public var role: String = "" + public var content: [Any] = [] + + public init(role: String = "", content: [Any] = []) { + self.role = role + self.content = content + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> AnthropicWireMessage { + let object = try TypraRuntime.object(data, typeName: "AnthropicWireMessage") + var instance = AnthropicWireMessage() + if let value = object["role"] { + instance.role = try TypraRuntime.string(value, field: "role") + } + if let value = object["content"] { + instance.content = try TypraRuntime.array(value, field: "content").map { $0 } + } + return instance + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["role"] = self.role + result["content"] = self.content + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> AnthropicWireMessage { + return try load(TypraRuntime.jsonObject(from: json, typeName: "AnthropicWireMessage"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> AnthropicWireMessage { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "AnthropicWireMessage"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/agent.swift b/runtime/swift/prompty-model/Sources/PromptyModel/agent.swift new file mode 100644 index 000000000..137a1f8cf --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/agent.swift @@ -0,0 +1,243 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +/// An Agent is the root model produced from a .prompty markdown file for LLM prompts. The frontmatter defines structured metadata including model configuration, input/output schemas, tools, and template settings. The markdown body becomes the instructions. This is the single root type for the Prompty schema — there is no abstract base class or kind discriminator. A .prompty file always produces an Agent instance. Runtime loaders may resolve frontmatter references such as `${env:VAR}` and `${file:relative/path}`. File references must be treated as a host-controlled capability: by default they are scoped to the containing .prompty file's directory tree after canonicalization, and any additional allowed roots must be supplied by the host application's load options rather than frontmatter. +public struct Agent: TypraModel { + public static let shorthandProperty: String? = nil + public var name: String = "" + public var displayName: String? = nil + public var description: String? = nil + public var metadata: [String: Any]? = nil + public var inputs: [Property]? = nil + public var outputs: [Property]? = nil + public var model: Model? = nil + public var tools: [Tool]? = nil + public var template: Template? = nil + public var instructions: String? = nil + + public init(name: String = "", displayName: String? = nil, description: String? = nil, metadata: [String: Any]? = nil, inputs: [Property]? = nil, outputs: [Property]? = nil, model: Model? = nil, tools: [Tool]? = nil, template: Template? = nil, instructions: String? = nil) { + self.name = name + self.displayName = displayName + self.description = description + self.metadata = metadata + self.inputs = inputs + self.outputs = outputs + self.model = model + self.tools = tools + self.template = template + self.instructions = instructions + } + + public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> Agent { + let object = try TypraRuntime.object(data, typeName: "Agent") + var instance = Agent() + if let value = object["name"] { + instance.name = try TypraRuntime.string(value, field: "name") + } + else { + instance.name = "" + } + if let value = object["displayName"] { + instance.displayName = try TypraRuntime.string(value, field: "displayName") + } + if let value = object["description"] { + instance.description = try TypraRuntime.string(value, field: "description") + } + if let value = object["metadata"] { + instance.metadata = try TypraRuntime.dictionary(value, field: "metadata") + } + if let value = object["inputs"] { + instance.inputs = try loadInputs(value, context: context.at("inputs")) + } + if let value = object["outputs"] { + instance.outputs = try loadOutputs(value, context: context.at("outputs")) + } + if let value = object["model"] { + instance.model = try Model.load(value, context: context.at("model")) + } + if let value = object["tools"] { + instance.tools = try loadTools(value, context: context.at("tools")) + } + if let value = object["template"] { + instance.template = try Template.load(value, context: context.at("template")) + } + if let value = object["instructions"] { + instance.instructions = try TypraRuntime.string(value, field: "instructions") + } + return instance + } + + private static func loadInputs(_ data: Any, context: LoadContext) throws -> [Property] { + if let values = data as? [Any] { + return try values.enumerated().map { try Property.load($1, context: context.atIndex($0)) } + } + let values = try TypraRuntime.dictionary(data, field: "inputs") + return try values.keys.sorted().map { name in + let value = values[name]! + if value is [Any] { + throw TypraRuntimeError.invalidField(field: context.at(name).path, expected: "non-array named collection entry; received category array") + } + var itemData: [String: Any] + if let object = value as? [String: Any] { + itemData = object + } else { + itemData = try Property.load(value, context: context).save() + } + itemData["name"] = name + return try Property.load(itemData, context: context.at(name)) + } + } + + private static func saveInputs(_ items: [Property], context: SaveContext) throws -> Any { + let serialized = try items.map { try $0.save(context) } + if context.collectionFormat == "array" { + return serialized + } + var names = Set() + for itemData in serialized { + guard let name = itemData["name"] as? String, !name.isEmpty, names.insert(name).inserted else { + return serialized + } + } + var result: [String: Any] = [:] + for (_, originalData) in zip(items, serialized) { + var itemData = originalData + let name = itemData.removeValue(forKey: "name") as! String + result[name] = itemData + } + return result + } + + private static func loadOutputs(_ data: Any, context: LoadContext) throws -> [Property] { + if let values = data as? [Any] { + return try values.enumerated().map { try Property.load($1, context: context.atIndex($0)) } + } + let values = try TypraRuntime.dictionary(data, field: "outputs") + return try values.keys.sorted().map { name in + let value = values[name]! + if value is [Any] { + throw TypraRuntimeError.invalidField(field: context.at(name).path, expected: "non-array named collection entry; received category array") + } + var itemData: [String: Any] + if let object = value as? [String: Any] { + itemData = object + } else { + itemData = try Property.load(value, context: context).save() + } + itemData["name"] = name + return try Property.load(itemData, context: context.at(name)) + } + } + + private static func saveOutputs(_ items: [Property], context: SaveContext) throws -> Any { + let serialized = try items.map { try $0.save(context) } + if context.collectionFormat == "array" { + return serialized + } + var names = Set() + for itemData in serialized { + guard let name = itemData["name"] as? String, !name.isEmpty, names.insert(name).inserted else { + return serialized + } + } + var result: [String: Any] = [:] + for (_, originalData) in zip(items, serialized) { + var itemData = originalData + let name = itemData.removeValue(forKey: "name") as! String + result[name] = itemData + } + return result + } + + private static func loadTools(_ data: Any, context: LoadContext) throws -> [Tool] { + if let values = data as? [Any] { + return try values.enumerated().map { try Tool.load($1, context: context.atIndex($0)) } + } + let values = try TypraRuntime.dictionary(data, field: "tools") + return try values.keys.sorted().map { name in + let value = values[name]! + if value is [Any] { + throw TypraRuntimeError.invalidField(field: context.at(name).path, expected: "non-array named collection entry; received category array") + } + var itemData: [String: Any] + if let object = value as? [String: Any] { + itemData = object + } else { + itemData = try Tool.load(value, context: context).save() + } + itemData["name"] = name + return try Tool.load(itemData, context: context.at(name)) + } + } + + private static func saveTools(_ items: [Tool], context: SaveContext) throws -> Any { + let serialized = try items.map { try $0.save(context) } + if context.collectionFormat == "array" { + return serialized + } + var names = Set() + for itemData in serialized { + guard let name = itemData["name"] as? String, !name.isEmpty, names.insert(name).inserted else { + return serialized + } + } + var result: [String: Any] = [:] + for (_, originalData) in zip(items, serialized) { + var itemData = originalData + let name = itemData.removeValue(forKey: "name") as! String + result[name] = itemData + } + return result + } + + public func save(_ context: SaveContext = SaveContext()) throws -> [String: Any] { + var result: [String: Any] = [:] + result["name"] = self.name + if let value = self.displayName { + result["displayName"] = value + } + if let value = self.description { + result["description"] = value + } + if let value = self.metadata { + result["metadata"] = value + } + if let value = self.inputs { + result["inputs"] = try Self.saveInputs(value, context: context) + } + if let value = self.outputs { + result["outputs"] = try Self.saveOutputs(value, context: context) + } + if let value = self.model { + result["model"] = try value.save(context) + } + if let value = self.tools { + result["tools"] = try Self.saveTools(value, context: context) + } + if let value = self.template { + result["template"] = try value.save(context) + } + if let value = self.instructions { + result["instructions"] = value + } + return result + } + + public static func fromJSON(_ json: String, context: LoadContext = LoadContext()) throws -> Agent { + return try load(TypraRuntime.jsonObject(from: json, typeName: "Agent"), context: context) + } + + public func toJSON(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.jsonString(from: save(context)) + } + + public static func fromYAML(_ yaml: String, context: LoadContext = LoadContext()) throws -> Agent { + return try load(TypraRuntime.yamlObject(from: yaml, typeName: "Agent"), context: context) + } + + public func toYAML(_ context: SaveContext = SaveContext()) throws -> String { + return try TypraRuntime.yamlString(from: save(context)) + } +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/checkpoint_store.swift b/runtime/swift/prompty-model/Sources/PromptyModel/checkpoint_store.swift new file mode 100644 index 000000000..771390d90 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/checkpoint_store.swift @@ -0,0 +1,10 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public protocol CheckpointStore: Sendable { + func listCheckpoints(sessionId: String) async throws -> [Checkpoint] + func load(sessionId: String, checkpointId: String) async throws -> Checkpoint? + func save(checkpoint: Checkpoint) async throws -> Checkpoint +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/discovery_conformance.swift b/runtime/swift/prompty-model/Sources/PromptyModel/discovery_conformance.swift new file mode 100644 index 000000000..77ee08cc6 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/discovery_conformance.swift @@ -0,0 +1,9 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public protocol DiscoveryConformance: Sendable { + func enrich(base: ModelInfo) async throws -> ModelInfo + func mapModel(raw: Any) async throws -> ModelInfo +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/engine_durability_port.swift b/runtime/swift/prompty-model/Sources/PromptyModel/engine_durability_port.swift new file mode 100644 index 000000000..56d290b80 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/engine_durability_port.swift @@ -0,0 +1,9 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public protocol EngineDurabilityPort: Sendable { + func append(event: EngineEvent) async throws + func appendWithCheckpoint(events: [EngineEvent], checkpoint: EngineCheckpoint) async throws +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/engine_permission_port.swift b/runtime/swift/prompty-model/Sources/PromptyModel/engine_permission_port.swift new file mode 100644 index 000000000..2dc6e4af1 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/engine_permission_port.swift @@ -0,0 +1,8 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public protocol EnginePermissionPort: Sendable { + func authorize(request: ModelToolRequest) async throws -> EnginePermissionDecision +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/engine_post_commit_port.swift b/runtime/swift/prompty-model/Sources/PromptyModel/engine_post_commit_port.swift new file mode 100644 index 000000000..432eda384 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/engine_post_commit_port.swift @@ -0,0 +1,8 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public protocol EnginePostCommitPort: Sendable { + func afterCommit(effectId: String, commit: TurnCommit) async throws +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/engine_tool_port.swift b/runtime/swift/prompty-model/Sources/PromptyModel/engine_tool_port.swift new file mode 100644 index 000000000..2f6963638 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/engine_tool_port.swift @@ -0,0 +1,8 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public protocol EngineToolPort: Sendable { + func execute(request: ModelToolRequest) async throws -> ModelToolResult +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/event_journal_writer.swift b/runtime/swift/prompty-model/Sources/PromptyModel/event_journal_writer.swift new file mode 100644 index 000000000..253590675 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/event_journal_writer.swift @@ -0,0 +1,10 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public protocol EventJournalWriter: Sendable { + func appendSession(sessionEvent: SessionEvent) throws -> Bool + func appendTurn(turnEvent: TurnEvent) throws -> Bool + func close(summary: SessionSummary?) throws -> Bool +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/event_sink.swift b/runtime/swift/prompty-model/Sources/PromptyModel/event_sink.swift new file mode 100644 index 000000000..3e39b2c4e --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/event_sink.swift @@ -0,0 +1,9 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public protocol EventSink: Sendable { + func emitSession(sessionEvent: SessionEvent) throws -> Bool + func emitTurn(turnEvent: TurnEvent) throws -> Bool +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/executor.swift b/runtime/swift/prompty-model/Sources/PromptyModel/executor.swift new file mode 100644 index 000000000..07f6fc882 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/executor.swift @@ -0,0 +1,10 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public protocol Executor: Sendable { + func execute(agent: Agent, messages: [Message]) async throws -> Any + func executeStream(agent: Agent, messages: [Message]) async throws -> Any + func formatToolMessages(rawResponse: Any, toolCalls: [ToolCall], toolResults: [String], textContent: String?) throws -> [Message] +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/host_tool_executor.swift b/runtime/swift/prompty-model/Sources/PromptyModel/host_tool_executor.swift new file mode 100644 index 000000000..4cca9dea5 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/host_tool_executor.swift @@ -0,0 +1,8 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public protocol HostToolExecutor: Sendable { + func execute(request: HostToolRequest) async throws -> HostToolResult +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/load_conformance.swift b/runtime/swift/prompty-model/Sources/PromptyModel/load_conformance.swift new file mode 100644 index 000000000..a597716f2 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/load_conformance.swift @@ -0,0 +1,8 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public protocol LoadConformance: Sendable { + func load(source: Any) async throws -> Agent +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/model_lister.swift b/runtime/swift/prompty-model/Sources/PromptyModel/model_lister.swift new file mode 100644 index 000000000..60cb335a7 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/model_lister.swift @@ -0,0 +1,8 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public protocol ModelLister: Sendable { + func listModels(connection: Any) async throws -> [ModelInfo] +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/parser.swift b/runtime/swift/prompty-model/Sources/PromptyModel/parser.swift new file mode 100644 index 000000000..aba70fbdf --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/parser.swift @@ -0,0 +1,9 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public protocol Parser: Sendable { + func parse(agent: Agent, rendered: String, context: [String: Any]?) async throws -> [Message] + func preRender(template: String) throws -> Any? +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/permission_resolver.swift b/runtime/swift/prompty-model/Sources/PromptyModel/permission_resolver.swift new file mode 100644 index 000000000..fb33cdbeb --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/permission_resolver.swift @@ -0,0 +1,8 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public protocol PermissionResolver: Sendable { + func request(request: PermissionRequest) async throws -> PermissionDecision +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/processor.swift b/runtime/swift/prompty-model/Sources/PromptyModel/processor.swift new file mode 100644 index 000000000..67c9d75a2 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/processor.swift @@ -0,0 +1,9 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public protocol Processor: Sendable { + func process(agent: Agent, response: Any) async throws -> Any + func processStream(stream: Any) async throws -> Any +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/renderer.swift b/runtime/swift/prompty-model/Sources/PromptyModel/renderer.swift new file mode 100644 index 000000000..8380ee52d --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/renderer.swift @@ -0,0 +1,8 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public protocol Renderer: Sendable { + func render(agent: Agent, template: String, inputs: [String: Any]) async throws -> String +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/turn_conformance.swift b/runtime/swift/prompty-model/Sources/PromptyModel/turn_conformance.swift new file mode 100644 index 000000000..8dfd6be8a --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/turn_conformance.swift @@ -0,0 +1,10 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public protocol TurnConformance: Sendable { + func replay(input: Any) async throws -> Any + func run(input: Any) async throws -> Any + func runTurn(input: Any) async throws -> Any +} diff --git a/runtime/swift/prompty-model/Sources/PromptyModel/wire_conformance.swift b/runtime/swift/prompty-model/Sources/PromptyModel/wire_conformance.swift new file mode 100644 index 000000000..6314067a7 --- /dev/null +++ b/runtime/swift/prompty-model/Sources/PromptyModel/wire_conformance.swift @@ -0,0 +1,8 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation + +public protocol WireConformance: Sendable { + func toRequest(agent: Agent, messages: [Message]) async throws -> Any +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/AgentTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/AgentTests.swift new file mode 100644 index 000000000..6a36db1f4 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/AgentTests.swift @@ -0,0 +1,1373 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class AgentTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "name": "basic-prompt", + "displayName": "Basic Prompt", + "description": "A basic prompt that uses the GPT-3 chat API to answer questions", + "metadata": { + "authors": [ + "sethjuarez", + "jietong" + ], + "tags": [ + "example", + "prompt" + ] + }, + "inputs": { + "firstName": { + "kind": "string", + "default": "Jane" + }, + "lastName": { + "kind": "string", + "default": "Doe" + }, + "question": { + "kind": "string", + "default": "What is the meaning of life?" + } + }, + "outputs": { + "answer": { + "kind": "string", + "description": "The answer to the user's question." + } + }, + "model": { + "id": "gpt-35-turbo", + "connection": { + "kind": "key", + "endpoint": "https://{your-custom-endpoint}.openai.azure.com/", + "apiKey": "{your-api-key}" + } + }, + "tools": [ + { + "name": "getCurrentWeather", + "kind": "function", + "description": "Get the current weather in a given location", + "parameters": { + "location": { + "kind": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "kind": "string", + "description": "The unit of temperature, e.g. Celsius or Fahrenheit" + } + } + } + ], + "template": { + "format": "mustache", + "parser": "prompty" + }, + "instructions": "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some\\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to\\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" +} +""" + let instance = try Agent.fromJSON(json) + XCTAssertEqual(instance.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(instance.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(instance.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(instance.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(instance.metadata) + XCTAssertEqual((try XCTUnwrap(instance.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(instance.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(instance.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(instance.template)).parser.kind, "prompty") + let reloaded = try Agent.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(reloaded.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(reloaded.metadata) + XCTAssertEqual((try XCTUnwrap(reloaded.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(reloaded.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(reloaded.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(reloaded.template)).parser.kind, "prompty") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +name: basic-prompt +displayName: Basic Prompt +description: A basic prompt that uses the GPT-3 chat API to answer questions +metadata: + authors: + - sethjuarez + - jietong + tags: + - example + - prompt +inputs: + firstName: + kind: string + default: Jane + lastName: + kind: string + default: Doe + question: + kind: string + default: What is the meaning of life? +outputs: + answer: + kind: string + description: The answer to the user's question. +model: + id: gpt-35-turbo + connection: + kind: key + endpoint: "https://{your-custom-endpoint}.openai.azure.com/" + apiKey: "{your-api-key}" +tools: + - name: getCurrentWeather + kind: function + description: Get the current weather in a given location + parameters: + location: + kind: string + description: The city and state, e.g. San Francisco, CA + unit: + kind: string + description: The unit of temperature, e.g. Celsius or Fahrenheit +template: + format: mustache + parser: prompty +instructions: "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some\\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to\\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" + +""" + let instance = try Agent.fromYAML(yaml) + XCTAssertEqual(instance.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(instance.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(instance.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(instance.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(instance.metadata) + XCTAssertEqual((try XCTUnwrap(instance.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(instance.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(instance.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(instance.template)).parser.kind, "prompty") + let reloaded = try Agent.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(reloaded.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(reloaded.metadata) + XCTAssertEqual((try XCTUnwrap(reloaded.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(reloaded.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(reloaded.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(reloaded.template)).parser.kind, "prompty") + } + + func testJSONRoundTrip2() throws { + let json = """ +{ + "name": "basic-prompt", + "displayName": "Basic Prompt", + "description": "A basic prompt that uses the GPT-3 chat API to answer questions", + "metadata": { + "authors": [ + "sethjuarez", + "jietong" + ], + "tags": [ + "example", + "prompt" + ] + }, + "inputs": { + "firstName": { + "kind": "string", + "default": "Jane" + }, + "lastName": { + "kind": "string", + "default": "Doe" + }, + "question": { + "kind": "string", + "default": "What is the meaning of life?" + } + }, + "outputs": { + "answer": { + "kind": "string", + "description": "The answer to the user's question." + } + }, + "model": { + "id": "gpt-35-turbo", + "connection": { + "kind": "key", + "endpoint": "https://{your-custom-endpoint}.openai.azure.com/", + "apiKey": "{your-api-key}" + } + }, + "tools": { + "getCurrentWeather": { + "kind": "function", + "description": "Get the current weather in a given location", + "parameters": { + "location": { + "kind": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "kind": "string", + "description": "The unit of temperature, e.g. Celsius or Fahrenheit" + } + } + } + }, + "template": { + "format": "mustache", + "parser": "prompty" + }, + "instructions": "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some\\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to\\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" +} +""" + let instance = try Agent.fromJSON(json) + XCTAssertEqual(instance.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(instance.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(instance.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(instance.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(instance.metadata) + XCTAssertEqual((try XCTUnwrap(instance.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(instance.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(instance.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(instance.template)).parser.kind, "prompty") + let reloaded = try Agent.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(reloaded.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(reloaded.metadata) + XCTAssertEqual((try XCTUnwrap(reloaded.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(reloaded.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(reloaded.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(reloaded.template)).parser.kind, "prompty") + } + + func testYAMLRoundTrip2() throws { + let yaml = """ +name: basic-prompt +displayName: Basic Prompt +description: A basic prompt that uses the GPT-3 chat API to answer questions +metadata: + authors: + - sethjuarez + - jietong + tags: + - example + - prompt +inputs: + firstName: + kind: string + default: Jane + lastName: + kind: string + default: Doe + question: + kind: string + default: What is the meaning of life? +outputs: + answer: + kind: string + description: The answer to the user's question. +model: + id: gpt-35-turbo + connection: + kind: key + endpoint: "https://{your-custom-endpoint}.openai.azure.com/" + apiKey: "{your-api-key}" +tools: + getCurrentWeather: + kind: function + description: Get the current weather in a given location + parameters: + location: + kind: string + description: The city and state, e.g. San Francisco, CA + unit: + kind: string + description: The unit of temperature, e.g. Celsius or Fahrenheit +template: + format: mustache + parser: prompty +instructions: "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some\\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to\\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" + +""" + let instance = try Agent.fromYAML(yaml) + XCTAssertEqual(instance.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(instance.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(instance.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(instance.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(instance.metadata) + XCTAssertEqual((try XCTUnwrap(instance.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(instance.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(instance.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(instance.template)).parser.kind, "prompty") + let reloaded = try Agent.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(reloaded.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(reloaded.metadata) + XCTAssertEqual((try XCTUnwrap(reloaded.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(reloaded.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(reloaded.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(reloaded.template)).parser.kind, "prompty") + } + + func testJSONRoundTrip3() throws { + let json = """ +{ + "name": "basic-prompt", + "displayName": "Basic Prompt", + "description": "A basic prompt that uses the GPT-3 chat API to answer questions", + "metadata": { + "authors": [ + "sethjuarez", + "jietong" + ], + "tags": [ + "example", + "prompt" + ] + }, + "inputs": { + "firstName": { + "kind": "string", + "default": "Jane" + }, + "lastName": { + "kind": "string", + "default": "Doe" + }, + "question": { + "kind": "string", + "default": "What is the meaning of life?" + } + }, + "outputs": [ + { + "name": "answer", + "kind": "string", + "description": "The answer to the user's question." + } + ], + "model": { + "id": "gpt-35-turbo", + "connection": { + "kind": "key", + "endpoint": "https://{your-custom-endpoint}.openai.azure.com/", + "apiKey": "{your-api-key}" + } + }, + "tools": [ + { + "name": "getCurrentWeather", + "kind": "function", + "description": "Get the current weather in a given location", + "parameters": { + "location": { + "kind": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "kind": "string", + "description": "The unit of temperature, e.g. Celsius or Fahrenheit" + } + } + } + ], + "template": { + "format": "mustache", + "parser": "prompty" + }, + "instructions": "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some\\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to\\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" +} +""" + let instance = try Agent.fromJSON(json) + XCTAssertEqual(instance.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(instance.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(instance.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(instance.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(instance.metadata) + XCTAssertEqual((try XCTUnwrap(instance.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(instance.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(instance.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(instance.template)).parser.kind, "prompty") + let reloaded = try Agent.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(reloaded.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(reloaded.metadata) + XCTAssertEqual((try XCTUnwrap(reloaded.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(reloaded.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(reloaded.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(reloaded.template)).parser.kind, "prompty") + } + + func testYAMLRoundTrip3() throws { + let yaml = """ +name: basic-prompt +displayName: Basic Prompt +description: A basic prompt that uses the GPT-3 chat API to answer questions +metadata: + authors: + - sethjuarez + - jietong + tags: + - example + - prompt +inputs: + firstName: + kind: string + default: Jane + lastName: + kind: string + default: Doe + question: + kind: string + default: What is the meaning of life? +outputs: + - name: answer + kind: string + description: The answer to the user's question. +model: + id: gpt-35-turbo + connection: + kind: key + endpoint: "https://{your-custom-endpoint}.openai.azure.com/" + apiKey: "{your-api-key}" +tools: + - name: getCurrentWeather + kind: function + description: Get the current weather in a given location + parameters: + location: + kind: string + description: The city and state, e.g. San Francisco, CA + unit: + kind: string + description: The unit of temperature, e.g. Celsius or Fahrenheit +template: + format: mustache + parser: prompty +instructions: "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some\\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to\\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" + +""" + let instance = try Agent.fromYAML(yaml) + XCTAssertEqual(instance.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(instance.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(instance.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(instance.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(instance.metadata) + XCTAssertEqual((try XCTUnwrap(instance.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(instance.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(instance.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(instance.template)).parser.kind, "prompty") + let reloaded = try Agent.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(reloaded.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(reloaded.metadata) + XCTAssertEqual((try XCTUnwrap(reloaded.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(reloaded.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(reloaded.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(reloaded.template)).parser.kind, "prompty") + } + + func testJSONRoundTrip4() throws { + let json = """ +{ + "name": "basic-prompt", + "displayName": "Basic Prompt", + "description": "A basic prompt that uses the GPT-3 chat API to answer questions", + "metadata": { + "authors": [ + "sethjuarez", + "jietong" + ], + "tags": [ + "example", + "prompt" + ] + }, + "inputs": { + "firstName": { + "kind": "string", + "default": "Jane" + }, + "lastName": { + "kind": "string", + "default": "Doe" + }, + "question": { + "kind": "string", + "default": "What is the meaning of life?" + } + }, + "outputs": [ + { + "name": "answer", + "kind": "string", + "description": "The answer to the user's question." + } + ], + "model": { + "id": "gpt-35-turbo", + "connection": { + "kind": "key", + "endpoint": "https://{your-custom-endpoint}.openai.azure.com/", + "apiKey": "{your-api-key}" + } + }, + "tools": { + "getCurrentWeather": { + "kind": "function", + "description": "Get the current weather in a given location", + "parameters": { + "location": { + "kind": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "kind": "string", + "description": "The unit of temperature, e.g. Celsius or Fahrenheit" + } + } + } + }, + "template": { + "format": "mustache", + "parser": "prompty" + }, + "instructions": "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some\\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to\\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" +} +""" + let instance = try Agent.fromJSON(json) + XCTAssertEqual(instance.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(instance.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(instance.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(instance.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(instance.metadata) + XCTAssertEqual((try XCTUnwrap(instance.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(instance.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(instance.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(instance.template)).parser.kind, "prompty") + let reloaded = try Agent.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(reloaded.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(reloaded.metadata) + XCTAssertEqual((try XCTUnwrap(reloaded.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(reloaded.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(reloaded.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(reloaded.template)).parser.kind, "prompty") + } + + func testYAMLRoundTrip4() throws { + let yaml = """ +name: basic-prompt +displayName: Basic Prompt +description: A basic prompt that uses the GPT-3 chat API to answer questions +metadata: + authors: + - sethjuarez + - jietong + tags: + - example + - prompt +inputs: + firstName: + kind: string + default: Jane + lastName: + kind: string + default: Doe + question: + kind: string + default: What is the meaning of life? +outputs: + - name: answer + kind: string + description: The answer to the user's question. +model: + id: gpt-35-turbo + connection: + kind: key + endpoint: "https://{your-custom-endpoint}.openai.azure.com/" + apiKey: "{your-api-key}" +tools: + getCurrentWeather: + kind: function + description: Get the current weather in a given location + parameters: + location: + kind: string + description: The city and state, e.g. San Francisco, CA + unit: + kind: string + description: The unit of temperature, e.g. Celsius or Fahrenheit +template: + format: mustache + parser: prompty +instructions: "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some\\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to\\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" + +""" + let instance = try Agent.fromYAML(yaml) + XCTAssertEqual(instance.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(instance.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(instance.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(instance.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(instance.metadata) + XCTAssertEqual((try XCTUnwrap(instance.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(instance.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(instance.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(instance.template)).parser.kind, "prompty") + let reloaded = try Agent.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(reloaded.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(reloaded.metadata) + XCTAssertEqual((try XCTUnwrap(reloaded.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(reloaded.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(reloaded.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(reloaded.template)).parser.kind, "prompty") + } + + func testJSONRoundTrip5() throws { + let json = """ +{ + "name": "basic-prompt", + "displayName": "Basic Prompt", + "description": "A basic prompt that uses the GPT-3 chat API to answer questions", + "metadata": { + "authors": [ + "sethjuarez", + "jietong" + ], + "tags": [ + "example", + "prompt" + ] + }, + "inputs": [ + { + "name": "firstName", + "kind": "string", + "default": "Jane" + }, + { + "name": "lastName", + "kind": "string", + "default": "Doe" + }, + { + "name": "question", + "kind": "string", + "default": "What is the meaning of life?" + } + ], + "outputs": { + "answer": { + "kind": "string", + "description": "The answer to the user's question." + } + }, + "model": { + "id": "gpt-35-turbo", + "connection": { + "kind": "key", + "endpoint": "https://{your-custom-endpoint}.openai.azure.com/", + "apiKey": "{your-api-key}" + } + }, + "tools": [ + { + "name": "getCurrentWeather", + "kind": "function", + "description": "Get the current weather in a given location", + "parameters": { + "location": { + "kind": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "kind": "string", + "description": "The unit of temperature, e.g. Celsius or Fahrenheit" + } + } + } + ], + "template": { + "format": "mustache", + "parser": "prompty" + }, + "instructions": "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some\\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to\\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" +} +""" + let instance = try Agent.fromJSON(json) + XCTAssertEqual(instance.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(instance.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(instance.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(instance.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(instance.metadata) + XCTAssertEqual((try XCTUnwrap(instance.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(instance.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(instance.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(instance.template)).parser.kind, "prompty") + let reloaded = try Agent.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(reloaded.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(reloaded.metadata) + XCTAssertEqual((try XCTUnwrap(reloaded.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(reloaded.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(reloaded.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(reloaded.template)).parser.kind, "prompty") + } + + func testYAMLRoundTrip5() throws { + let yaml = """ +name: basic-prompt +displayName: Basic Prompt +description: A basic prompt that uses the GPT-3 chat API to answer questions +metadata: + authors: + - sethjuarez + - jietong + tags: + - example + - prompt +inputs: + - name: firstName + kind: string + default: Jane + - name: lastName + kind: string + default: Doe + - name: question + kind: string + default: What is the meaning of life? +outputs: + answer: + kind: string + description: The answer to the user's question. +model: + id: gpt-35-turbo + connection: + kind: key + endpoint: "https://{your-custom-endpoint}.openai.azure.com/" + apiKey: "{your-api-key}" +tools: + - name: getCurrentWeather + kind: function + description: Get the current weather in a given location + parameters: + location: + kind: string + description: The city and state, e.g. San Francisco, CA + unit: + kind: string + description: The unit of temperature, e.g. Celsius or Fahrenheit +template: + format: mustache + parser: prompty +instructions: "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some\\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to\\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" + +""" + let instance = try Agent.fromYAML(yaml) + XCTAssertEqual(instance.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(instance.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(instance.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(instance.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(instance.metadata) + XCTAssertEqual((try XCTUnwrap(instance.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(instance.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(instance.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(instance.template)).parser.kind, "prompty") + let reloaded = try Agent.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(reloaded.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(reloaded.metadata) + XCTAssertEqual((try XCTUnwrap(reloaded.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(reloaded.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(reloaded.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(reloaded.template)).parser.kind, "prompty") + } + + func testJSONRoundTrip6() throws { + let json = """ +{ + "name": "basic-prompt", + "displayName": "Basic Prompt", + "description": "A basic prompt that uses the GPT-3 chat API to answer questions", + "metadata": { + "authors": [ + "sethjuarez", + "jietong" + ], + "tags": [ + "example", + "prompt" + ] + }, + "inputs": [ + { + "name": "firstName", + "kind": "string", + "default": "Jane" + }, + { + "name": "lastName", + "kind": "string", + "default": "Doe" + }, + { + "name": "question", + "kind": "string", + "default": "What is the meaning of life?" + } + ], + "outputs": { + "answer": { + "kind": "string", + "description": "The answer to the user's question." + } + }, + "model": { + "id": "gpt-35-turbo", + "connection": { + "kind": "key", + "endpoint": "https://{your-custom-endpoint}.openai.azure.com/", + "apiKey": "{your-api-key}" + } + }, + "tools": { + "getCurrentWeather": { + "kind": "function", + "description": "Get the current weather in a given location", + "parameters": { + "location": { + "kind": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "kind": "string", + "description": "The unit of temperature, e.g. Celsius or Fahrenheit" + } + } + } + }, + "template": { + "format": "mustache", + "parser": "prompty" + }, + "instructions": "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some\\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to\\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" +} +""" + let instance = try Agent.fromJSON(json) + XCTAssertEqual(instance.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(instance.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(instance.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(instance.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(instance.metadata) + XCTAssertEqual((try XCTUnwrap(instance.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(instance.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(instance.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(instance.template)).parser.kind, "prompty") + let reloaded = try Agent.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(reloaded.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(reloaded.metadata) + XCTAssertEqual((try XCTUnwrap(reloaded.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(reloaded.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(reloaded.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(reloaded.template)).parser.kind, "prompty") + } + + func testYAMLRoundTrip6() throws { + let yaml = """ +name: basic-prompt +displayName: Basic Prompt +description: A basic prompt that uses the GPT-3 chat API to answer questions +metadata: + authors: + - sethjuarez + - jietong + tags: + - example + - prompt +inputs: + - name: firstName + kind: string + default: Jane + - name: lastName + kind: string + default: Doe + - name: question + kind: string + default: What is the meaning of life? +outputs: + answer: + kind: string + description: The answer to the user's question. +model: + id: gpt-35-turbo + connection: + kind: key + endpoint: "https://{your-custom-endpoint}.openai.azure.com/" + apiKey: "{your-api-key}" +tools: + getCurrentWeather: + kind: function + description: Get the current weather in a given location + parameters: + location: + kind: string + description: The city and state, e.g. San Francisco, CA + unit: + kind: string + description: The unit of temperature, e.g. Celsius or Fahrenheit +template: + format: mustache + parser: prompty +instructions: "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some\\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to\\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" + +""" + let instance = try Agent.fromYAML(yaml) + XCTAssertEqual(instance.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(instance.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(instance.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(instance.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(instance.metadata) + XCTAssertEqual((try XCTUnwrap(instance.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(instance.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(instance.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(instance.template)).parser.kind, "prompty") + let reloaded = try Agent.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(reloaded.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(reloaded.metadata) + XCTAssertEqual((try XCTUnwrap(reloaded.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(reloaded.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(reloaded.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(reloaded.template)).parser.kind, "prompty") + } + + func testJSONRoundTrip7() throws { + let json = """ +{ + "name": "basic-prompt", + "displayName": "Basic Prompt", + "description": "A basic prompt that uses the GPT-3 chat API to answer questions", + "metadata": { + "authors": [ + "sethjuarez", + "jietong" + ], + "tags": [ + "example", + "prompt" + ] + }, + "inputs": [ + { + "name": "firstName", + "kind": "string", + "default": "Jane" + }, + { + "name": "lastName", + "kind": "string", + "default": "Doe" + }, + { + "name": "question", + "kind": "string", + "default": "What is the meaning of life?" + } + ], + "outputs": [ + { + "name": "answer", + "kind": "string", + "description": "The answer to the user's question." + } + ], + "model": { + "id": "gpt-35-turbo", + "connection": { + "kind": "key", + "endpoint": "https://{your-custom-endpoint}.openai.azure.com/", + "apiKey": "{your-api-key}" + } + }, + "tools": [ + { + "name": "getCurrentWeather", + "kind": "function", + "description": "Get the current weather in a given location", + "parameters": { + "location": { + "kind": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "kind": "string", + "description": "The unit of temperature, e.g. Celsius or Fahrenheit" + } + } + } + ], + "template": { + "format": "mustache", + "parser": "prompty" + }, + "instructions": "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some\\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to\\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" +} +""" + let instance = try Agent.fromJSON(json) + XCTAssertEqual(instance.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(instance.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(instance.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(instance.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(instance.metadata) + XCTAssertEqual((try XCTUnwrap(instance.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(instance.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(instance.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(instance.template)).parser.kind, "prompty") + let reloaded = try Agent.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(reloaded.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(reloaded.metadata) + XCTAssertEqual((try XCTUnwrap(reloaded.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(reloaded.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(reloaded.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(reloaded.template)).parser.kind, "prompty") + } + + func testYAMLRoundTrip7() throws { + let yaml = """ +name: basic-prompt +displayName: Basic Prompt +description: A basic prompt that uses the GPT-3 chat API to answer questions +metadata: + authors: + - sethjuarez + - jietong + tags: + - example + - prompt +inputs: + - name: firstName + kind: string + default: Jane + - name: lastName + kind: string + default: Doe + - name: question + kind: string + default: What is the meaning of life? +outputs: + - name: answer + kind: string + description: The answer to the user's question. +model: + id: gpt-35-turbo + connection: + kind: key + endpoint: "https://{your-custom-endpoint}.openai.azure.com/" + apiKey: "{your-api-key}" +tools: + - name: getCurrentWeather + kind: function + description: Get the current weather in a given location + parameters: + location: + kind: string + description: The city and state, e.g. San Francisco, CA + unit: + kind: string + description: The unit of temperature, e.g. Celsius or Fahrenheit +template: + format: mustache + parser: prompty +instructions: "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some\\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to\\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" + +""" + let instance = try Agent.fromYAML(yaml) + XCTAssertEqual(instance.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(instance.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(instance.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(instance.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(instance.metadata) + XCTAssertEqual((try XCTUnwrap(instance.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(instance.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(instance.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(instance.template)).parser.kind, "prompty") + let reloaded = try Agent.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(reloaded.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(reloaded.metadata) + XCTAssertEqual((try XCTUnwrap(reloaded.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(reloaded.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(reloaded.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(reloaded.template)).parser.kind, "prompty") + } + + func testJSONRoundTrip8() throws { + let json = """ +{ + "name": "basic-prompt", + "displayName": "Basic Prompt", + "description": "A basic prompt that uses the GPT-3 chat API to answer questions", + "metadata": { + "authors": [ + "sethjuarez", + "jietong" + ], + "tags": [ + "example", + "prompt" + ] + }, + "inputs": [ + { + "name": "firstName", + "kind": "string", + "default": "Jane" + }, + { + "name": "lastName", + "kind": "string", + "default": "Doe" + }, + { + "name": "question", + "kind": "string", + "default": "What is the meaning of life?" + } + ], + "outputs": [ + { + "name": "answer", + "kind": "string", + "description": "The answer to the user's question." + } + ], + "model": { + "id": "gpt-35-turbo", + "connection": { + "kind": "key", + "endpoint": "https://{your-custom-endpoint}.openai.azure.com/", + "apiKey": "{your-api-key}" + } + }, + "tools": { + "getCurrentWeather": { + "kind": "function", + "description": "Get the current weather in a given location", + "parameters": { + "location": { + "kind": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "kind": "string", + "description": "The unit of temperature, e.g. Celsius or Fahrenheit" + } + } + } + }, + "template": { + "format": "mustache", + "parser": "prompty" + }, + "instructions": "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some\\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to\\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" +} +""" + let instance = try Agent.fromJSON(json) + XCTAssertEqual(instance.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(instance.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(instance.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(instance.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(instance.metadata) + XCTAssertEqual((try XCTUnwrap(instance.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(instance.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(instance.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(instance.template)).parser.kind, "prompty") + let reloaded = try Agent.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(reloaded.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(reloaded.metadata) + XCTAssertEqual((try XCTUnwrap(reloaded.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(reloaded.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(reloaded.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(reloaded.template)).parser.kind, "prompty") + } + + func testYAMLRoundTrip8() throws { + let yaml = """ +name: basic-prompt +displayName: Basic Prompt +description: A basic prompt that uses the GPT-3 chat API to answer questions +metadata: + authors: + - sethjuarez + - jietong + tags: + - example + - prompt +inputs: + - name: firstName + kind: string + default: Jane + - name: lastName + kind: string + default: Doe + - name: question + kind: string + default: What is the meaning of life? +outputs: + - name: answer + kind: string + description: The answer to the user's question. +model: + id: gpt-35-turbo + connection: + kind: key + endpoint: "https://{your-custom-endpoint}.openai.azure.com/" + apiKey: "{your-api-key}" +tools: + getCurrentWeather: + kind: function + description: Get the current weather in a given location + parameters: + location: + kind: string + description: The city and state, e.g. San Francisco, CA + unit: + kind: string + description: The unit of temperature, e.g. Celsius or Fahrenheit +template: + format: mustache + parser: prompty +instructions: "system:\\nYou are an AI assistant who helps people find information.\\nAs the assistant, you answer questions briefly, succinctly,\\nand in a personable manner using markdown and even add some\\npersonal flair with appropriate emojis.\\n\\n# Customer\\nYou are helping {{firstName}} {{lastName}} to find answers to\\ntheir questions. Use their name to address them in your responses.\\nuser:\\n{{question}}" + +""" + let instance = try Agent.fromYAML(yaml) + XCTAssertEqual(instance.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(instance.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(instance.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(instance.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(instance.metadata) + XCTAssertEqual((try XCTUnwrap(instance.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(instance.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(instance.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(instance.template)).parser.kind, "prompty") + let reloaded = try Agent.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.name, "basic-prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.displayName)), "Basic Prompt") + XCTAssertEqual((try XCTUnwrap(reloaded.description)), "A basic prompt that uses the GPT-3 chat API to answer questions") + XCTAssertEqual((try XCTUnwrap(reloaded.instructions)), "system:\nYou are an AI assistant who helps people find information.\nAs the assistant, you answer questions briefly, succinctly,\nand in a personable manner using markdown and even add some\npersonal flair with appropriate emojis.\n\n# Customer\nYou are helping {{firstName}} {{lastName}} to find answers to\ntheir questions. Use their name to address them in your responses.\nuser:\n{{question}}") + XCTAssertNotNil(reloaded.metadata) + XCTAssertEqual((try XCTUnwrap(reloaded.inputs)).count, 3) + XCTAssertEqual((try XCTUnwrap(reloaded.outputs)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.model)).id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(reloaded.tools)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.template)).format.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(reloaded.template)).parser.kind, "prompty") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/ConformanceTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/ConformanceTests.swift new file mode 100644 index 000000000..c90a6a27c --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/ConformanceTests.swift @@ -0,0 +1,30 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import XCTest +@testable import PromptyModel + +final class ConformanceTests: XCTestCase { + func testIntegerValidationRejectsUnsafeValues() throws { + XCTAssertEqual(try TypraRuntime.int64(NSNumber(value: Int64.max), field: "value"), Int64.max) + XCTAssertEqual(try TypraRuntime.int64(UInt64(256), field: "value"), 256) + XCTAssertEqual(try TypraRuntime.int64(UInt32(256), field: "value"), 256) + XCTAssertThrowsError(try TypraRuntime.int64(NSDecimalNumber(mantissa: 9223372036854775808, exponent: 0, isNegative: false), field: "value")) + XCTAssertThrowsError(try TypraRuntime.int64(NSDecimalNumber(mantissa: 9223372036854775809, exponent: 0, isNegative: true), field: "value")) + XCTAssertThrowsError(try TypraRuntime.int64(UInt64.max, field: "value")) + XCTAssertThrowsError(try TypraRuntime.int64(1.5, field: "value")) + XCTAssertThrowsError(try TypraRuntime.int32(2147483648, field: "value")) + } + + func testJsonBooleansAreNotNumericValues() throws { + let parsed = try TypraRuntime.jsonObject(from: "{\"intZero\":0,\"intOne\":1,\"boolTrue\":true,\"boolFalse\":false}", typeName: "NumericProbe") as! [String: Any] + XCTAssertEqual(try TypraRuntime.int64(parsed["intZero"]!, field: "intZero"), 0) + XCTAssertEqual(try TypraRuntime.int64(parsed["intOne"]!, field: "intOne"), 1) + XCTAssertEqual(try TypraRuntime.double(parsed["intZero"]!, field: "intZero"), 0) + XCTAssertEqual(try TypraRuntime.double(parsed["intOne"]!, field: "intOne"), 1) + XCTAssertThrowsError(try TypraRuntime.int64(parsed["boolTrue"]!, field: "boolTrue")) + XCTAssertThrowsError(try TypraRuntime.int64(parsed["boolFalse"]!, field: "boolFalse")) + XCTAssertThrowsError(try TypraRuntime.double(parsed["boolTrue"]!, field: "boolTrue")) + XCTAssertThrowsError(try TypraRuntime.double(parsed["boolFalse"]!, field: "boolFalse")) + } +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Connectivity/AuthorizationCodeFlowTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Connectivity/AuthorizationCodeFlowTests.swift new file mode 100644 index 000000000..c5c129c5f --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Connectivity/AuthorizationCodeFlowTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class AuthorizationCodeFlowTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "authUrl": "sample", + "codeVerifier": "sample" +} +""" + let instance = try AuthorizationCodeFlow.fromJSON(json) + XCTAssertEqual(instance.authUrl, "sample") + XCTAssertEqual(instance.codeVerifier, "sample") + let reloaded = try AuthorizationCodeFlow.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.authUrl, "sample") + XCTAssertEqual(reloaded.codeVerifier, "sample") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +authUrl: sample +codeVerifier: sample + +""" + let instance = try AuthorizationCodeFlow.fromYAML(yaml) + XCTAssertEqual(instance.authUrl, "sample") + XCTAssertEqual(instance.codeVerifier, "sample") + let reloaded = try AuthorizationCodeFlow.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.authUrl, "sample") + XCTAssertEqual(reloaded.codeVerifier, "sample") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Connectivity/ConnectionTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Connectivity/ConnectionTests.swift new file mode 100644 index 000000000..f0f1fc471 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Connectivity/ConnectionTests.swift @@ -0,0 +1,56 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ConnectionTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "kind": "reference", + "authenticationMode": "system", + "usageDescription": "This will allow the agent to respond to an email on your behalf" +} +""" + let instance = try Connection.fromJSON(json) + if case .referenceConnection(let concrete) = instance { + XCTAssertEqual((try XCTUnwrap(concrete.authenticationMode)).rawValue, "system") + XCTAssertEqual((try XCTUnwrap(concrete.usageDescription)), "This will allow the agent to respond to an email on your behalf") + } else { + XCTFail("Expected ReferenceConnection") + } + let reloaded = try Connection.fromJSON(try instance.toJSON()) + if case .referenceConnection(let concrete) = reloaded { + XCTAssertEqual((try XCTUnwrap(concrete.authenticationMode)).rawValue, "system") + XCTAssertEqual((try XCTUnwrap(concrete.usageDescription)), "This will allow the agent to respond to an email on your behalf") + } else { + XCTFail("Expected ReferenceConnection") + } + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +kind: reference +authenticationMode: system +usageDescription: This will allow the agent to respond to an email on your behalf + +""" + let instance = try Connection.fromYAML(yaml) + if case .referenceConnection(let concrete) = instance { + XCTAssertEqual((try XCTUnwrap(concrete.authenticationMode)).rawValue, "system") + XCTAssertEqual((try XCTUnwrap(concrete.usageDescription)), "This will allow the agent to respond to an email on your behalf") + } else { + XCTFail("Expected ReferenceConnection") + } + let reloaded = try Connection.fromYAML(try instance.toYAML()) + if case .referenceConnection(let concrete) = reloaded { + XCTAssertEqual((try XCTUnwrap(concrete.authenticationMode)).rawValue, "system") + XCTAssertEqual((try XCTUnwrap(concrete.usageDescription)), "This will allow the agent to respond to an email on your behalf") + } else { + XCTFail("Expected ReferenceConnection") + } + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Connectivity/DeviceAuthorizationTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Connectivity/DeviceAuthorizationTests.swift new file mode 100644 index 000000000..8dd7bd1ea --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Connectivity/DeviceAuthorizationTests.swift @@ -0,0 +1,56 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class DeviceAuthorizationTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "deviceCode": "sample", + "userCode": "sample", + "verificationUri": "sample", + "expiresIn": 1, + "interval": 1 +} +""" + let instance = try DeviceAuthorization.fromJSON(json) + XCTAssertEqual(instance.deviceCode, "sample") + XCTAssertEqual(instance.userCode, "sample") + XCTAssertEqual(instance.verificationUri, "sample") + XCTAssertEqual(instance.expiresIn, 1) + XCTAssertEqual(instance.interval, 1) + let reloaded = try DeviceAuthorization.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.deviceCode, "sample") + XCTAssertEqual(reloaded.userCode, "sample") + XCTAssertEqual(reloaded.verificationUri, "sample") + XCTAssertEqual(reloaded.expiresIn, 1) + XCTAssertEqual(reloaded.interval, 1) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +deviceCode: sample +userCode: sample +verificationUri: sample +expiresIn: 1 +interval: 1 + +""" + let instance = try DeviceAuthorization.fromYAML(yaml) + XCTAssertEqual(instance.deviceCode, "sample") + XCTAssertEqual(instance.userCode, "sample") + XCTAssertEqual(instance.verificationUri, "sample") + XCTAssertEqual(instance.expiresIn, 1) + XCTAssertEqual(instance.interval, 1) + let reloaded = try DeviceAuthorization.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.deviceCode, "sample") + XCTAssertEqual(reloaded.userCode, "sample") + XCTAssertEqual(reloaded.verificationUri, "sample") + XCTAssertEqual(reloaded.expiresIn, 1) + XCTAssertEqual(reloaded.interval, 1) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Connectivity/OAuthTokenTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Connectivity/OAuthTokenTests.swift new file mode 100644 index 000000000..142ec70d3 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Connectivity/OAuthTokenTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class OAuthTokenTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "refreshToken": "0.AXoAoffline-refresh-token-value", + "scope": "https://cognitiveservices.azure.com/.default offline_access" +} +""" + let instance = try OAuthToken.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.refreshToken)), "0.AXoAoffline-refresh-token-value") + XCTAssertEqual((try XCTUnwrap(instance.scope)), "https://cognitiveservices.azure.com/.default offline_access") + let reloaded = try OAuthToken.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.refreshToken)), "0.AXoAoffline-refresh-token-value") + XCTAssertEqual((try XCTUnwrap(reloaded.scope)), "https://cognitiveservices.azure.com/.default offline_access") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +refreshToken: 0.AXoAoffline-refresh-token-value +scope: "https://cognitiveservices.azure.com/.default offline_access" + +""" + let instance = try OAuthToken.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.refreshToken)), "0.AXoAoffline-refresh-token-value") + XCTAssertEqual((try XCTUnwrap(instance.scope)), "https://cognitiveservices.azure.com/.default offline_access") + let reloaded = try OAuthToken.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.refreshToken)), "0.AXoAoffline-refresh-token-value") + XCTAssertEqual((try XCTUnwrap(reloaded.scope)), "https://cognitiveservices.azure.com/.default offline_access") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Conversation/ContentPartTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Conversation/ContentPartTests.swift new file mode 100644 index 000000000..02a432119 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Conversation/ContentPartTests.swift @@ -0,0 +1,50 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ContentPartTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "kind": "text", + "value": "Hello, world!" +} +""" + let instance = try ContentPart.fromJSON(json) + if case .textPart(let concrete) = instance { + XCTAssertEqual(concrete.value, "Hello, world!") + } else { + XCTFail("Expected TextPart") + } + let reloaded = try ContentPart.fromJSON(try instance.toJSON()) + if case .textPart(let concrete) = reloaded { + XCTAssertEqual(concrete.value, "Hello, world!") + } else { + XCTFail("Expected TextPart") + } + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +kind: text +value: Hello, world! + +""" + let instance = try ContentPart.fromYAML(yaml) + if case .textPart(let concrete) = instance { + XCTAssertEqual(concrete.value, "Hello, world!") + } else { + XCTFail("Expected TextPart") + } + let reloaded = try ContentPart.fromYAML(try instance.toYAML()) + if case .textPart(let concrete) = reloaded { + XCTAssertEqual(concrete.value, "Hello, world!") + } else { + XCTFail("Expected TextPart") + } + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Conversation/MessageTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Conversation/MessageTests.swift new file mode 100644 index 000000000..ae674d46f --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Conversation/MessageTests.swift @@ -0,0 +1,54 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class MessageTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "role": "user", + "parts": [ + { + "kind": "text", + "value": "Hello!" + } + ], + "metadata": { + "source": "user-input" + } +} +""" + let instance = try Message.fromJSON(json) + XCTAssertEqual(instance.role, Role.user) + XCTAssertEqual(instance.parts.count, 1) + XCTAssertNotNil(instance.metadata) + let reloaded = try Message.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.role, Role.user) + XCTAssertEqual(reloaded.parts.count, 1) + XCTAssertNotNil(reloaded.metadata) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +role: user +parts: + - kind: text + value: Hello! +metadata: + source: user-input + +""" + let instance = try Message.fromYAML(yaml) + XCTAssertEqual(instance.role, Role.user) + XCTAssertEqual(instance.parts.count, 1) + XCTAssertNotNil(instance.metadata) + let reloaded = try Message.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.role, Role.user) + XCTAssertEqual(reloaded.parts.count, 1) + XCTAssertNotNil(reloaded.metadata) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Conversation/ThreadMarkerTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Conversation/ThreadMarkerTests.swift new file mode 100644 index 000000000..8ba829843 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Conversation/ThreadMarkerTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ThreadMarkerTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "name": "thread", + "kind": "thread" +} +""" + let instance = try ThreadMarker.fromJSON(json) + XCTAssertEqual(instance.name, "thread") + XCTAssertEqual(instance.kind, "thread") + let reloaded = try ThreadMarker.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.name, "thread") + XCTAssertEqual(reloaded.kind, "thread") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +name: thread +kind: thread + +""" + let instance = try ThreadMarker.fromYAML(yaml) + XCTAssertEqual(instance.name, "thread") + XCTAssertEqual(instance.kind, "thread") + let reloaded = try ThreadMarker.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.name, "thread") + XCTAssertEqual(reloaded.kind, "thread") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Conversation/ToolCallTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Conversation/ToolCallTests.swift new file mode 100644 index 000000000..ee8b6b132 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Conversation/ToolCallTests.swift @@ -0,0 +1,44 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ToolCallTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\\"city\\": \\"Paris\\"}" +} +""" + let instance = try ToolCall.fromJSON(json) + XCTAssertEqual(instance.id, "call_abc123") + XCTAssertEqual(instance.name, "get_weather") + XCTAssertEqual(instance.arguments, "{\"city\": \"Paris\"}") + let reloaded = try ToolCall.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.id, "call_abc123") + XCTAssertEqual(reloaded.name, "get_weather") + XCTAssertEqual(reloaded.arguments, "{\"city\": \"Paris\"}") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +id: call_abc123 +name: get_weather +arguments: "{\\"city\\": \\"Paris\\"}" + +""" + let instance = try ToolCall.fromYAML(yaml) + XCTAssertEqual(instance.id, "call_abc123") + XCTAssertEqual(instance.name, "get_weather") + XCTAssertEqual(instance.arguments, "{\"city\": \"Paris\"}") + let reloaded = try ToolCall.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.id, "call_abc123") + XCTAssertEqual(reloaded.name, "get_weather") + XCTAssertEqual(reloaded.arguments, "{\"city\": \"Paris\"}") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Conversation/ToolResultTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Conversation/ToolResultTests.swift new file mode 100644 index 000000000..cbc099b78 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Conversation/ToolResultTests.swift @@ -0,0 +1,57 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ToolResultTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ], + "errorKind": "missing_tool", + "errorMessage": "Tool 'get_weather' is not registered", + "durationMs": 42 +} +""" + let instance = try ToolResult.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.errorKind)), "missing_tool") + XCTAssertEqual((try XCTUnwrap(instance.errorMessage)), "Tool 'get_weather' is not registered") + XCTAssertEqual((try XCTUnwrap(instance.durationMs)), 42) + XCTAssertEqual(instance.parts.count, 1) + let reloaded = try ToolResult.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.errorKind)), "missing_tool") + XCTAssertEqual((try XCTUnwrap(reloaded.errorMessage)), "Tool 'get_weather' is not registered") + XCTAssertEqual((try XCTUnwrap(reloaded.durationMs)), 42) + XCTAssertEqual(reloaded.parts.count, 1) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +parts: + - kind: text + value: 72°F and sunny +errorKind: missing_tool +errorMessage: Tool 'get_weather' is not registered +durationMs: 42 + +""" + let instance = try ToolResult.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.errorKind)), "missing_tool") + XCTAssertEqual((try XCTUnwrap(instance.errorMessage)), "Tool 'get_weather' is not registered") + XCTAssertEqual((try XCTUnwrap(instance.durationMs)), 42) + XCTAssertEqual(instance.parts.count, 1) + let reloaded = try ToolResult.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.errorKind)), "missing_tool") + XCTAssertEqual((try XCTUnwrap(reloaded.errorMessage)), "Tool 'get_weather' is not registered") + XCTAssertEqual((try XCTUnwrap(reloaded.durationMs)), 42) + XCTAssertEqual(reloaded.parts.count, 1) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Core/FileNotFoundErrorTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Core/FileNotFoundErrorTests.swift new file mode 100644 index 000000000..8d1006809 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Core/FileNotFoundErrorTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class FileNotFoundErrorTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "message": "Prompty file not found: ./chat.prompty", + "path": "./chat.prompty" +} +""" + let instance = try FileNotFoundError.fromJSON(json) + XCTAssertEqual(instance.message, "Prompty file not found: ./chat.prompty") + XCTAssertEqual(instance.path, "./chat.prompty") + let reloaded = try FileNotFoundError.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.message, "Prompty file not found: ./chat.prompty") + XCTAssertEqual(reloaded.path, "./chat.prompty") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +message: "Prompty file not found: ./chat.prompty" +path: ./chat.prompty + +""" + let instance = try FileNotFoundError.fromYAML(yaml) + XCTAssertEqual(instance.message, "Prompty file not found: ./chat.prompty") + XCTAssertEqual(instance.path, "./chat.prompty") + let reloaded = try FileNotFoundError.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.message, "Prompty file not found: ./chat.prompty") + XCTAssertEqual(reloaded.path, "./chat.prompty") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Core/InvokerErrorTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Core/InvokerErrorTests.swift new file mode 100644 index 000000000..0734e66a2 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Core/InvokerErrorTests.swift @@ -0,0 +1,44 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class InvokerErrorTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "message": "No renderer registered for key: jinja2", + "component": "renderer", + "key": "jinja2" +} +""" + let instance = try InvokerError.fromJSON(json) + XCTAssertEqual(instance.message, "No renderer registered for key: jinja2") + XCTAssertEqual(instance.component, "renderer") + XCTAssertEqual(instance.key, "jinja2") + let reloaded = try InvokerError.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.message, "No renderer registered for key: jinja2") + XCTAssertEqual(reloaded.component, "renderer") + XCTAssertEqual(reloaded.key, "jinja2") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +message: "No renderer registered for key: jinja2" +component: renderer +key: jinja2 + +""" + let instance = try InvokerError.fromYAML(yaml) + XCTAssertEqual(instance.message, "No renderer registered for key: jinja2") + XCTAssertEqual(instance.component, "renderer") + XCTAssertEqual(instance.key, "jinja2") + let reloaded = try InvokerError.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.message, "No renderer registered for key: jinja2") + XCTAssertEqual(reloaded.component, "renderer") + XCTAssertEqual(reloaded.key, "jinja2") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Core/PropertyTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Core/PropertyTests.swift new file mode 100644 index 000000000..1f322ad35 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Core/PropertyTests.swift @@ -0,0 +1,133 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class PropertyTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "name": "my-input", + "kind": "string", + "description": "A description of the input property", + "required": true, + "nullable": true, + "default": "default value", + "example": "example value", + "enumValues": [ + "value1", + "value2", + "value3" + ] +} +""" + let instance = try Property.fromJSON(json) + if case .unknown(let concrete) = instance { + XCTAssertEqual(concrete["name"] as? String, "my-input") + XCTAssertEqual(concrete["kind"] as? String, "string") + XCTAssertEqual(concrete["description"] as? String, "A description of the input property") + XCTAssertEqual(concrete["required"] as? Bool, true) + XCTAssertEqual(concrete["nullable"] as? Bool, true) + XCTAssertEqual(concrete["default"] as? String, "default value") + XCTAssertEqual(concrete["example"] as? String, "example value") + } else { + XCTFail("Expected Property.unknown") + } + let reloaded = try Property.fromJSON(try instance.toJSON()) + if case .unknown(let concrete) = reloaded { + XCTAssertEqual(concrete["name"] as? String, "my-input") + XCTAssertEqual(concrete["kind"] as? String, "string") + XCTAssertEqual(concrete["description"] as? String, "A description of the input property") + XCTAssertEqual(concrete["required"] as? Bool, true) + XCTAssertEqual(concrete["nullable"] as? Bool, true) + XCTAssertEqual(concrete["default"] as? String, "default value") + XCTAssertEqual(concrete["example"] as? String, "example value") + } else { + XCTFail("Expected Property.unknown") + } + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +name: my-input +kind: string +description: A description of the input property +required: true +nullable: true +default: default value +example: example value +enumValues: + - value1 + - value2 + - value3 + +""" + let instance = try Property.fromYAML(yaml) + if case .unknown(let concrete) = instance { + XCTAssertEqual(concrete["name"] as? String, "my-input") + XCTAssertEqual(concrete["kind"] as? String, "string") + XCTAssertEqual(concrete["description"] as? String, "A description of the input property") + XCTAssertEqual(concrete["required"] as? Bool, true) + XCTAssertEqual(concrete["nullable"] as? Bool, true) + XCTAssertEqual(concrete["default"] as? String, "default value") + XCTAssertEqual(concrete["example"] as? String, "example value") + } else { + XCTFail("Expected Property.unknown") + } + let reloaded = try Property.fromYAML(try instance.toYAML()) + if case .unknown(let concrete) = reloaded { + XCTAssertEqual(concrete["name"] as? String, "my-input") + XCTAssertEqual(concrete["kind"] as? String, "string") + XCTAssertEqual(concrete["description"] as? String, "A description of the input property") + XCTAssertEqual(concrete["required"] as? Bool, true) + XCTAssertEqual(concrete["nullable"] as? Bool, true) + XCTAssertEqual(concrete["default"] as? String, "default value") + XCTAssertEqual(concrete["example"] as? String, "example value") + } else { + XCTFail("Expected Property.unknown") + } + } + + func testScalarCoercion1() throws { + let instance = try Property.load(false) + if case .unknown(let concrete) = instance { + XCTAssertEqual(concrete["kind"] as? String, "boolean") + XCTAssertEqual(concrete["example"] as? Bool, false) + } else { + XCTFail("Expected unknown polymorphic fallback") + } + } + + func testScalarCoercion2() throws { + let instance = try Property.load(3.14) + if case .unknown(let concrete) = instance { + XCTAssertEqual(concrete["kind"] as? String, "float") + XCTAssertEqual(concrete["example"] as? Double, 3.14) + } else { + XCTFail("Expected unknown polymorphic fallback") + } + } + + func testScalarCoercion3() throws { + let instance = try Property.load(4) + if case .unknown(let concrete) = instance { + XCTAssertEqual(concrete["kind"] as? String, "integer") + XCTAssertEqual(concrete["example"] as? Int, 4) + } else { + XCTFail("Expected unknown polymorphic fallback") + } + } + + func testScalarCoercion4() throws { + let instance = try Property.load("example") + if case .unknown(let concrete) = instance { + XCTAssertEqual(concrete["kind"] as? String, "string") + XCTAssertEqual(concrete["example"] as? String, "example") + } else { + XCTFail("Expected unknown polymorphic fallback") + } + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Core/ValidationErrorTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Core/ValidationErrorTests.swift new file mode 100644 index 000000000..e4c6dd7ab --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Core/ValidationErrorTests.swift @@ -0,0 +1,44 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ValidationErrorTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "message": "Missing required input: firstName", + "property": "firstName", + "constraint": "required" +} +""" + let instance = try ValidationError.fromJSON(json) + XCTAssertEqual(instance.message, "Missing required input: firstName") + XCTAssertEqual(instance.property, "firstName") + XCTAssertEqual(instance.constraint, "required") + let reloaded = try ValidationError.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.message, "Missing required input: firstName") + XCTAssertEqual(reloaded.property, "firstName") + XCTAssertEqual(reloaded.constraint, "required") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +message: "Missing required input: firstName" +property: firstName +constraint: required + +""" + let instance = try ValidationError.fromYAML(yaml) + XCTAssertEqual(instance.message, "Missing required input: firstName") + XCTAssertEqual(instance.property, "firstName") + XCTAssertEqual(instance.constraint, "required") + let reloaded = try ValidationError.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.message, "Missing required input: firstName") + XCTAssertEqual(reloaded.property, "firstName") + XCTAssertEqual(reloaded.constraint, "required") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Core/ValidationResultTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Core/ValidationResultTests.swift new file mode 100644 index 000000000..40e3cdcb7 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Core/ValidationResultTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ValidationResultTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "valid": true, + "errors": [] +} +""" + let instance = try ValidationResult.fromJSON(json) + XCTAssertEqual(instance.valid, true) + XCTAssertEqual(instance.errors.count, 0) + let reloaded = try ValidationResult.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.valid, true) + XCTAssertEqual(reloaded.errors.count, 0) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +valid: true +errors: [] + +""" + let instance = try ValidationResult.fromYAML(yaml) + XCTAssertEqual(instance.valid, true) + XCTAssertEqual(instance.errors.count, 0) + let reloaded = try ValidationResult.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.valid, true) + XCTAssertEqual(reloaded.errors.count, 0) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/CheckpointTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/CheckpointTests.swift new file mode 100644 index 000000000..1c225384d --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/CheckpointTests.swift @@ -0,0 +1,62 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class CheckpointTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "id": "chk_abc123", + "sessionId": "sess_abc123", + "turnId": "turn_001", + "checkpointNumber": 3, + "title": "Added harness contracts", + "createdAt": "2026-06-09T20:00:00Z" +} +""" + let instance = try Checkpoint.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.id)), "chk_abc123") + XCTAssertEqual((try XCTUnwrap(instance.sessionId)), "sess_abc123") + XCTAssertEqual((try XCTUnwrap(instance.turnId)), "turn_001") + XCTAssertEqual((try XCTUnwrap(instance.checkpointNumber)), 3) + XCTAssertEqual(instance.title, "Added harness contracts") + XCTAssertEqual((try XCTUnwrap(instance.createdAt)), "2026-06-09T20:00:00Z") + let reloaded = try Checkpoint.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.id)), "chk_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.sessionId)), "sess_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.turnId)), "turn_001") + XCTAssertEqual((try XCTUnwrap(reloaded.checkpointNumber)), 3) + XCTAssertEqual(reloaded.title, "Added harness contracts") + XCTAssertEqual((try XCTUnwrap(reloaded.createdAt)), "2026-06-09T20:00:00Z") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +id: chk_abc123 +sessionId: sess_abc123 +turnId: turn_001 +checkpointNumber: 3 +title: Added harness contracts +createdAt: "2026-06-09T20:00:00Z" + +""" + let instance = try Checkpoint.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.id)), "chk_abc123") + XCTAssertEqual((try XCTUnwrap(instance.sessionId)), "sess_abc123") + XCTAssertEqual((try XCTUnwrap(instance.turnId)), "turn_001") + XCTAssertEqual((try XCTUnwrap(instance.checkpointNumber)), 3) + XCTAssertEqual(instance.title, "Added harness contracts") + XCTAssertEqual((try XCTUnwrap(instance.createdAt)), "2026-06-09T20:00:00Z") + let reloaded = try Checkpoint.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.id)), "chk_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.sessionId)), "sess_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.turnId)), "turn_001") + XCTAssertEqual((try XCTUnwrap(reloaded.checkpointNumber)), 3) + XCTAssertEqual(reloaded.title, "Added harness contracts") + XCTAssertEqual((try XCTUnwrap(reloaded.createdAt)), "2026-06-09T20:00:00Z") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/CompactionCompletePayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/CompactionCompletePayloadTests.swift new file mode 100644 index 000000000..bacd9bc2a --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/CompactionCompletePayloadTests.swift @@ -0,0 +1,44 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class CompactionCompletePayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "removed": 5, + "remaining": 3, + "summaryLength": 1200 +} +""" + let instance = try CompactionCompletePayload.fromJSON(json) + XCTAssertEqual(instance.removed, 5) + XCTAssertEqual(instance.remaining, 3) + XCTAssertEqual((try XCTUnwrap(instance.summaryLength)), 1200) + let reloaded = try CompactionCompletePayload.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.removed, 5) + XCTAssertEqual(reloaded.remaining, 3) + XCTAssertEqual((try XCTUnwrap(reloaded.summaryLength)), 1200) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +removed: 5 +remaining: 3 +summaryLength: 1200 + +""" + let instance = try CompactionCompletePayload.fromYAML(yaml) + XCTAssertEqual(instance.removed, 5) + XCTAssertEqual(instance.remaining, 3) + XCTAssertEqual((try XCTUnwrap(instance.summaryLength)), 1200) + let reloaded = try CompactionCompletePayload.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.removed, 5) + XCTAssertEqual(reloaded.remaining, 3) + XCTAssertEqual((try XCTUnwrap(reloaded.summaryLength)), 1200) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/CompactionFailedPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/CompactionFailedPayloadTests.swift new file mode 100644 index 000000000..bce53150d --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/CompactionFailedPayloadTests.swift @@ -0,0 +1,32 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class CompactionFailedPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "message": "Summarization prompt exceeded context window" +} +""" + let instance = try CompactionFailedPayload.fromJSON(json) + XCTAssertEqual(instance.message, "Summarization prompt exceeded context window") + let reloaded = try CompactionFailedPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.message, "Summarization prompt exceeded context window") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +message: Summarization prompt exceeded context window + +""" + let instance = try CompactionFailedPayload.fromYAML(yaml) + XCTAssertEqual(instance.message, "Summarization prompt exceeded context window") + let reloaded = try CompactionFailedPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.message, "Summarization prompt exceeded context window") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/CompactionStartPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/CompactionStartPayloadTests.swift new file mode 100644 index 000000000..d163b1822 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/CompactionStartPayloadTests.swift @@ -0,0 +1,32 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class CompactionStartPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "droppedCount": 5 +} +""" + let instance = try CompactionStartPayload.fromJSON(json) + XCTAssertEqual(instance.droppedCount, 5) + let reloaded = try CompactionStartPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.droppedCount, 5) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +droppedCount: 5 + +""" + let instance = try CompactionStartPayload.fromYAML(yaml) + XCTAssertEqual(instance.droppedCount, 5) + let reloaded = try CompactionStartPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.droppedCount, 5) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/DoneEventPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/DoneEventPayloadTests.swift new file mode 100644 index 000000000..0c457d27d --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/DoneEventPayloadTests.swift @@ -0,0 +1,34 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class DoneEventPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "response": {}, + "messages": [] +} +""" + let instance = try DoneEventPayload.fromJSON(json) + XCTAssertEqual(instance.messages.count, 0) + let reloaded = try DoneEventPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.messages.count, 0) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +response: {} +messages: [] + +""" + let instance = try DoneEventPayload.fromYAML(yaml) + XCTAssertEqual(instance.messages.count, 0) + let reloaded = try DoneEventPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.messages.count, 0) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ErrorEventPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ErrorEventPayloadTests.swift new file mode 100644 index 000000000..4d13916a9 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ErrorEventPayloadTests.swift @@ -0,0 +1,44 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ErrorEventPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "message": "Rate limit exceeded", + "errorKind": "rate_limit", + "phase": "llm" +} +""" + let instance = try ErrorEventPayload.fromJSON(json) + XCTAssertEqual(instance.message, "Rate limit exceeded") + XCTAssertEqual((try XCTUnwrap(instance.errorKind)), "rate_limit") + XCTAssertEqual((try XCTUnwrap(instance.phase)), "llm") + let reloaded = try ErrorEventPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.message, "Rate limit exceeded") + XCTAssertEqual((try XCTUnwrap(reloaded.errorKind)), "rate_limit") + XCTAssertEqual((try XCTUnwrap(reloaded.phase)), "llm") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +message: Rate limit exceeded +errorKind: rate_limit +phase: llm + +""" + let instance = try ErrorEventPayload.fromYAML(yaml) + XCTAssertEqual(instance.message, "Rate limit exceeded") + XCTAssertEqual((try XCTUnwrap(instance.errorKind)), "rate_limit") + XCTAssertEqual((try XCTUnwrap(instance.phase)), "llm") + let reloaded = try ErrorEventPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.message, "Rate limit exceeded") + XCTAssertEqual((try XCTUnwrap(reloaded.errorKind)), "rate_limit") + XCTAssertEqual((try XCTUnwrap(reloaded.phase)), "llm") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/HarnessContextTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/HarnessContextTests.swift new file mode 100644 index 000000000..02d11a2cb --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/HarnessContextTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class HarnessContextTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "cwd": "/workspace/project", + "gitRoot": "/workspace/project" +} +""" + let instance = try HarnessContext.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.cwd)), "/workspace/project") + XCTAssertEqual((try XCTUnwrap(instance.gitRoot)), "/workspace/project") + let reloaded = try HarnessContext.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.cwd)), "/workspace/project") + XCTAssertEqual((try XCTUnwrap(reloaded.gitRoot)), "/workspace/project") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +cwd: /workspace/project +gitRoot: /workspace/project + +""" + let instance = try HarnessContext.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.cwd)), "/workspace/project") + XCTAssertEqual((try XCTUnwrap(instance.gitRoot)), "/workspace/project") + let reloaded = try HarnessContext.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.cwd)), "/workspace/project") + XCTAssertEqual((try XCTUnwrap(reloaded.gitRoot)), "/workspace/project") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/HookEndPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/HookEndPayloadTests.swift new file mode 100644 index 000000000..8b4b0ba46 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/HookEndPayloadTests.swift @@ -0,0 +1,56 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class HookEndPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "hookInvocationId": "hook_abc123", + "hookType": "preToolUse", + "success": true, + "durationMs": 12, + "error": "hook failed" +} +""" + let instance = try HookEndPayload.fromJSON(json) + XCTAssertEqual(instance.hookInvocationId, "hook_abc123") + XCTAssertEqual(instance.hookType, "preToolUse") + XCTAssertEqual(instance.success, true) + XCTAssertEqual((try XCTUnwrap(instance.durationMs)), 12) + XCTAssertEqual((try XCTUnwrap(instance.error)), "hook failed") + let reloaded = try HookEndPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.hookInvocationId, "hook_abc123") + XCTAssertEqual(reloaded.hookType, "preToolUse") + XCTAssertEqual(reloaded.success, true) + XCTAssertEqual((try XCTUnwrap(reloaded.durationMs)), 12) + XCTAssertEqual((try XCTUnwrap(reloaded.error)), "hook failed") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +hookInvocationId: hook_abc123 +hookType: preToolUse +success: true +durationMs: 12 +error: hook failed + +""" + let instance = try HookEndPayload.fromYAML(yaml) + XCTAssertEqual(instance.hookInvocationId, "hook_abc123") + XCTAssertEqual(instance.hookType, "preToolUse") + XCTAssertEqual(instance.success, true) + XCTAssertEqual((try XCTUnwrap(instance.durationMs)), 12) + XCTAssertEqual((try XCTUnwrap(instance.error)), "hook failed") + let reloaded = try HookEndPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.hookInvocationId, "hook_abc123") + XCTAssertEqual(reloaded.hookType, "preToolUse") + XCTAssertEqual(reloaded.success, true) + XCTAssertEqual((try XCTUnwrap(reloaded.durationMs)), 12) + XCTAssertEqual((try XCTUnwrap(reloaded.error)), "hook failed") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/HookStartPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/HookStartPayloadTests.swift new file mode 100644 index 000000000..d610114b2 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/HookStartPayloadTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class HookStartPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "hookInvocationId": "hook_abc123", + "hookType": "preToolUse" +} +""" + let instance = try HookStartPayload.fromJSON(json) + XCTAssertEqual(instance.hookInvocationId, "hook_abc123") + XCTAssertEqual(instance.hookType, "preToolUse") + let reloaded = try HookStartPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.hookInvocationId, "hook_abc123") + XCTAssertEqual(reloaded.hookType, "preToolUse") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +hookInvocationId: hook_abc123 +hookType: preToolUse + +""" + let instance = try HookStartPayload.fromYAML(yaml) + XCTAssertEqual(instance.hookInvocationId, "hook_abc123") + XCTAssertEqual(instance.hookType, "preToolUse") + let reloaded = try HookStartPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.hookInvocationId, "hook_abc123") + XCTAssertEqual(reloaded.hookType, "preToolUse") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/HostToolRequestTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/HostToolRequestTests.swift new file mode 100644 index 000000000..10bd3b5e9 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/HostToolRequestTests.swift @@ -0,0 +1,50 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class HostToolRequestTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "requestId": "exec_abc123", + "toolCallId": "call_abc123", + "toolName": "powershell", + "workingDirectory": "/workspace/project" +} +""" + let instance = try HostToolRequest.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.requestId)), "exec_abc123") + XCTAssertEqual((try XCTUnwrap(instance.toolCallId)), "call_abc123") + XCTAssertEqual(instance.toolName, "powershell") + XCTAssertEqual((try XCTUnwrap(instance.workingDirectory)), "/workspace/project") + let reloaded = try HostToolRequest.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.requestId)), "exec_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.toolCallId)), "call_abc123") + XCTAssertEqual(reloaded.toolName, "powershell") + XCTAssertEqual((try XCTUnwrap(reloaded.workingDirectory)), "/workspace/project") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +requestId: exec_abc123 +toolCallId: call_abc123 +toolName: powershell +workingDirectory: /workspace/project + +""" + let instance = try HostToolRequest.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.requestId)), "exec_abc123") + XCTAssertEqual((try XCTUnwrap(instance.toolCallId)), "call_abc123") + XCTAssertEqual(instance.toolName, "powershell") + XCTAssertEqual((try XCTUnwrap(instance.workingDirectory)), "/workspace/project") + let reloaded = try HostToolRequest.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.requestId)), "exec_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.toolCallId)), "call_abc123") + XCTAssertEqual(reloaded.toolName, "powershell") + XCTAssertEqual((try XCTUnwrap(reloaded.workingDirectory)), "/workspace/project") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/HostToolResultTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/HostToolResultTests.swift new file mode 100644 index 000000000..a4d1e80d4 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/HostToolResultTests.swift @@ -0,0 +1,68 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class HostToolResultTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "requestId": "exec_abc123", + "toolCallId": "call_abc123", + "toolName": "powershell", + "success": true, + "exitCode": 0, + "durationMs": 250, + "errorKind": "timeout" +} +""" + let instance = try HostToolResult.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.requestId)), "exec_abc123") + XCTAssertEqual((try XCTUnwrap(instance.toolCallId)), "call_abc123") + XCTAssertEqual(instance.toolName, "powershell") + XCTAssertEqual(instance.success, true) + XCTAssertEqual((try XCTUnwrap(instance.exitCode)), 0) + XCTAssertEqual((try XCTUnwrap(instance.durationMs)), 250) + XCTAssertEqual((try XCTUnwrap(instance.errorKind)), "timeout") + let reloaded = try HostToolResult.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.requestId)), "exec_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.toolCallId)), "call_abc123") + XCTAssertEqual(reloaded.toolName, "powershell") + XCTAssertEqual(reloaded.success, true) + XCTAssertEqual((try XCTUnwrap(reloaded.exitCode)), 0) + XCTAssertEqual((try XCTUnwrap(reloaded.durationMs)), 250) + XCTAssertEqual((try XCTUnwrap(reloaded.errorKind)), "timeout") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +requestId: exec_abc123 +toolCallId: call_abc123 +toolName: powershell +success: true +exitCode: 0 +durationMs: 250 +errorKind: timeout + +""" + let instance = try HostToolResult.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.requestId)), "exec_abc123") + XCTAssertEqual((try XCTUnwrap(instance.toolCallId)), "call_abc123") + XCTAssertEqual(instance.toolName, "powershell") + XCTAssertEqual(instance.success, true) + XCTAssertEqual((try XCTUnwrap(instance.exitCode)), 0) + XCTAssertEqual((try XCTUnwrap(instance.durationMs)), 250) + XCTAssertEqual((try XCTUnwrap(instance.errorKind)), "timeout") + let reloaded = try HostToolResult.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.requestId)), "exec_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.toolCallId)), "call_abc123") + XCTAssertEqual(reloaded.toolName, "powershell") + XCTAssertEqual(reloaded.success, true) + XCTAssertEqual((try XCTUnwrap(reloaded.exitCode)), 0) + XCTAssertEqual((try XCTUnwrap(reloaded.durationMs)), 250) + XCTAssertEqual((try XCTUnwrap(reloaded.errorKind)), "timeout") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/LlmCompletePayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/LlmCompletePayloadTests.swift new file mode 100644 index 000000000..15c9bcfe3 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/LlmCompletePayloadTests.swift @@ -0,0 +1,44 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class LlmCompletePayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "requestId": "req_abc123", + "serviceRequestId": "srv_abc123", + "durationMs": 820 +} +""" + let instance = try LlmCompletePayload.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.requestId)), "req_abc123") + XCTAssertEqual((try XCTUnwrap(instance.serviceRequestId)), "srv_abc123") + XCTAssertEqual((try XCTUnwrap(instance.durationMs)), 820) + let reloaded = try LlmCompletePayload.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.requestId)), "req_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.serviceRequestId)), "srv_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.durationMs)), 820) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +requestId: req_abc123 +serviceRequestId: srv_abc123 +durationMs: 820 + +""" + let instance = try LlmCompletePayload.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.requestId)), "req_abc123") + XCTAssertEqual((try XCTUnwrap(instance.serviceRequestId)), "srv_abc123") + XCTAssertEqual((try XCTUnwrap(instance.durationMs)), 820) + let reloaded = try LlmCompletePayload.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.requestId)), "req_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.serviceRequestId)), "srv_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.durationMs)), 820) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/LlmStartPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/LlmStartPayloadTests.swift new file mode 100644 index 000000000..59d2328e9 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/LlmStartPayloadTests.swift @@ -0,0 +1,50 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class LlmStartPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "provider": "openai", + "modelId": "gpt-4o-mini", + "messageCount": 4, + "attempt": 0 +} +""" + let instance = try LlmStartPayload.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.provider)), "openai") + XCTAssertEqual((try XCTUnwrap(instance.modelId)), "gpt-4o-mini") + XCTAssertEqual((try XCTUnwrap(instance.messageCount)), 4) + XCTAssertEqual((try XCTUnwrap(instance.attempt)), 0) + let reloaded = try LlmStartPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.provider)), "openai") + XCTAssertEqual((try XCTUnwrap(reloaded.modelId)), "gpt-4o-mini") + XCTAssertEqual((try XCTUnwrap(reloaded.messageCount)), 4) + XCTAssertEqual((try XCTUnwrap(reloaded.attempt)), 0) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +provider: openai +modelId: gpt-4o-mini +messageCount: 4 +attempt: 0 + +""" + let instance = try LlmStartPayload.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.provider)), "openai") + XCTAssertEqual((try XCTUnwrap(instance.modelId)), "gpt-4o-mini") + XCTAssertEqual((try XCTUnwrap(instance.messageCount)), 4) + XCTAssertEqual((try XCTUnwrap(instance.attempt)), 0) + let reloaded = try LlmStartPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.provider)), "openai") + XCTAssertEqual((try XCTUnwrap(reloaded.modelId)), "gpt-4o-mini") + XCTAssertEqual((try XCTUnwrap(reloaded.messageCount)), 4) + XCTAssertEqual((try XCTUnwrap(reloaded.attempt)), 0) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/MessagesUpdatedPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/MessagesUpdatedPayloadTests.swift new file mode 100644 index 000000000..19bc4a3e2 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/MessagesUpdatedPayloadTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class MessagesUpdatedPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "reason": "tool_results", + "removed": 2 +} +""" + let instance = try MessagesUpdatedPayload.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.reason)), "tool_results") + XCTAssertEqual((try XCTUnwrap(instance.removed)), 2) + let reloaded = try MessagesUpdatedPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.reason)), "tool_results") + XCTAssertEqual((try XCTUnwrap(reloaded.removed)), 2) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +reason: tool_results +removed: 2 + +""" + let instance = try MessagesUpdatedPayload.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.reason)), "tool_results") + XCTAssertEqual((try XCTUnwrap(instance.removed)), 2) + let reloaded = try MessagesUpdatedPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.reason)), "tool_results") + XCTAssertEqual((try XCTUnwrap(reloaded.removed)), 2) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/PermissionCompletedPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/PermissionCompletedPayloadTests.swift new file mode 100644 index 000000000..b5abce4b3 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/PermissionCompletedPayloadTests.swift @@ -0,0 +1,56 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class PermissionCompletedPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "requestId": "perm_abc123", + "toolCallId": "call_abc123", + "permission": "tool.execute", + "approved": true, + "reason": "user_approved" +} +""" + let instance = try PermissionCompletedPayload.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.requestId)), "perm_abc123") + XCTAssertEqual((try XCTUnwrap(instance.toolCallId)), "call_abc123") + XCTAssertEqual(instance.permission, "tool.execute") + XCTAssertEqual(instance.approved, true) + XCTAssertEqual((try XCTUnwrap(instance.reason)), "user_approved") + let reloaded = try PermissionCompletedPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.requestId)), "perm_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.toolCallId)), "call_abc123") + XCTAssertEqual(reloaded.permission, "tool.execute") + XCTAssertEqual(reloaded.approved, true) + XCTAssertEqual((try XCTUnwrap(reloaded.reason)), "user_approved") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +requestId: perm_abc123 +toolCallId: call_abc123 +permission: tool.execute +approved: true +reason: user_approved + +""" + let instance = try PermissionCompletedPayload.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.requestId)), "perm_abc123") + XCTAssertEqual((try XCTUnwrap(instance.toolCallId)), "call_abc123") + XCTAssertEqual(instance.permission, "tool.execute") + XCTAssertEqual(instance.approved, true) + XCTAssertEqual((try XCTUnwrap(instance.reason)), "user_approved") + let reloaded = try PermissionCompletedPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.requestId)), "perm_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.toolCallId)), "call_abc123") + XCTAssertEqual(reloaded.permission, "tool.execute") + XCTAssertEqual(reloaded.approved, true) + XCTAssertEqual((try XCTUnwrap(reloaded.reason)), "user_approved") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/PermissionDecisionTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/PermissionDecisionTests.swift new file mode 100644 index 000000000..a9ac6e1eb --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/PermissionDecisionTests.swift @@ -0,0 +1,56 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class PermissionDecisionTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "requestId": "perm_abc123", + "toolCallId": "call_abc123", + "permission": "tool.execute", + "approved": true, + "reason": "user_approved" +} +""" + let instance = try PermissionDecision.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.requestId)), "perm_abc123") + XCTAssertEqual((try XCTUnwrap(instance.toolCallId)), "call_abc123") + XCTAssertEqual(instance.permission, "tool.execute") + XCTAssertEqual(instance.approved, true) + XCTAssertEqual((try XCTUnwrap(instance.reason)), "user_approved") + let reloaded = try PermissionDecision.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.requestId)), "perm_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.toolCallId)), "call_abc123") + XCTAssertEqual(reloaded.permission, "tool.execute") + XCTAssertEqual(reloaded.approved, true) + XCTAssertEqual((try XCTUnwrap(reloaded.reason)), "user_approved") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +requestId: perm_abc123 +toolCallId: call_abc123 +permission: tool.execute +approved: true +reason: user_approved + +""" + let instance = try PermissionDecision.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.requestId)), "perm_abc123") + XCTAssertEqual((try XCTUnwrap(instance.toolCallId)), "call_abc123") + XCTAssertEqual(instance.permission, "tool.execute") + XCTAssertEqual(instance.approved, true) + XCTAssertEqual((try XCTUnwrap(instance.reason)), "user_approved") + let reloaded = try PermissionDecision.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.requestId)), "perm_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.toolCallId)), "call_abc123") + XCTAssertEqual(reloaded.permission, "tool.execute") + XCTAssertEqual(reloaded.approved, true) + XCTAssertEqual((try XCTUnwrap(reloaded.reason)), "user_approved") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/PermissionRequestTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/PermissionRequestTests.swift new file mode 100644 index 000000000..fec49fc3d --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/PermissionRequestTests.swift @@ -0,0 +1,56 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class PermissionRequestTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "requestId": "perm_abc123", + "toolCallId": "call_abc123", + "permission": "tool.execute", + "target": "shell", + "promptRequest": "Allow shell to run tests?" +} +""" + let instance = try PermissionRequest.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.requestId)), "perm_abc123") + XCTAssertEqual((try XCTUnwrap(instance.toolCallId)), "call_abc123") + XCTAssertEqual(instance.permission, "tool.execute") + XCTAssertEqual((try XCTUnwrap(instance.target)), "shell") + XCTAssertEqual((try XCTUnwrap(instance.promptRequest)), "Allow shell to run tests?") + let reloaded = try PermissionRequest.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.requestId)), "perm_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.toolCallId)), "call_abc123") + XCTAssertEqual(reloaded.permission, "tool.execute") + XCTAssertEqual((try XCTUnwrap(reloaded.target)), "shell") + XCTAssertEqual((try XCTUnwrap(reloaded.promptRequest)), "Allow shell to run tests?") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +requestId: perm_abc123 +toolCallId: call_abc123 +permission: tool.execute +target: shell +promptRequest: Allow shell to run tests? + +""" + let instance = try PermissionRequest.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.requestId)), "perm_abc123") + XCTAssertEqual((try XCTUnwrap(instance.toolCallId)), "call_abc123") + XCTAssertEqual(instance.permission, "tool.execute") + XCTAssertEqual((try XCTUnwrap(instance.target)), "shell") + XCTAssertEqual((try XCTUnwrap(instance.promptRequest)), "Allow shell to run tests?") + let reloaded = try PermissionRequest.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.requestId)), "perm_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.toolCallId)), "call_abc123") + XCTAssertEqual(reloaded.permission, "tool.execute") + XCTAssertEqual((try XCTUnwrap(reloaded.target)), "shell") + XCTAssertEqual((try XCTUnwrap(reloaded.promptRequest)), "Allow shell to run tests?") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/PermissionRequestedPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/PermissionRequestedPayloadTests.swift new file mode 100644 index 000000000..9c9f0c8b3 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/PermissionRequestedPayloadTests.swift @@ -0,0 +1,56 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class PermissionRequestedPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "requestId": "perm_abc123", + "toolCallId": "call_abc123", + "permission": "tool.execute", + "target": "shell", + "promptRequest": "Allow shell to run tests?" +} +""" + let instance = try PermissionRequestedPayload.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.requestId)), "perm_abc123") + XCTAssertEqual((try XCTUnwrap(instance.toolCallId)), "call_abc123") + XCTAssertEqual(instance.permission, "tool.execute") + XCTAssertEqual((try XCTUnwrap(instance.target)), "shell") + XCTAssertEqual((try XCTUnwrap(instance.promptRequest)), "Allow shell to run tests?") + let reloaded = try PermissionRequestedPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.requestId)), "perm_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.toolCallId)), "call_abc123") + XCTAssertEqual(reloaded.permission, "tool.execute") + XCTAssertEqual((try XCTUnwrap(reloaded.target)), "shell") + XCTAssertEqual((try XCTUnwrap(reloaded.promptRequest)), "Allow shell to run tests?") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +requestId: perm_abc123 +toolCallId: call_abc123 +permission: tool.execute +target: shell +promptRequest: Allow shell to run tests? + +""" + let instance = try PermissionRequestedPayload.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.requestId)), "perm_abc123") + XCTAssertEqual((try XCTUnwrap(instance.toolCallId)), "call_abc123") + XCTAssertEqual(instance.permission, "tool.execute") + XCTAssertEqual((try XCTUnwrap(instance.target)), "shell") + XCTAssertEqual((try XCTUnwrap(instance.promptRequest)), "Allow shell to run tests?") + let reloaded = try PermissionRequestedPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.requestId)), "perm_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.toolCallId)), "call_abc123") + XCTAssertEqual(reloaded.permission, "tool.execute") + XCTAssertEqual((try XCTUnwrap(reloaded.target)), "shell") + XCTAssertEqual((try XCTUnwrap(reloaded.promptRequest)), "Allow shell to run tests?") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/RedactedFieldTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/RedactedFieldTests.swift new file mode 100644 index 000000000..233a2fbe5 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/RedactedFieldTests.swift @@ -0,0 +1,44 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class RedactedFieldTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "path": "$.arguments.apiKey", + "mode": "redacted", + "reason": "secret" +} +""" + let instance = try RedactedField.fromJSON(json) + XCTAssertEqual(instance.path, "$.arguments.apiKey") + XCTAssertEqual(instance.mode, RedactionMode.redacted) + XCTAssertEqual((try XCTUnwrap(instance.reason)), "secret") + let reloaded = try RedactedField.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.path, "$.arguments.apiKey") + XCTAssertEqual(reloaded.mode, RedactionMode.redacted) + XCTAssertEqual((try XCTUnwrap(reloaded.reason)), "secret") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +path: $.arguments.apiKey +mode: redacted +reason: secret + +""" + let instance = try RedactedField.fromYAML(yaml) + XCTAssertEqual(instance.path, "$.arguments.apiKey") + XCTAssertEqual(instance.mode, RedactionMode.redacted) + XCTAssertEqual((try XCTUnwrap(instance.reason)), "secret") + let reloaded = try RedactedField.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.path, "$.arguments.apiKey") + XCTAssertEqual(reloaded.mode, RedactionMode.redacted) + XCTAssertEqual((try XCTUnwrap(reloaded.reason)), "secret") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/RedactionMetadataTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/RedactionMetadataTests.swift new file mode 100644 index 000000000..aec38b712 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/RedactionMetadataTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class RedactionMetadataTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "sanitized": true, + "policy": "default-v1" +} +""" + let instance = try RedactionMetadata.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.sanitized)), true) + XCTAssertEqual((try XCTUnwrap(instance.policy)), "default-v1") + let reloaded = try RedactionMetadata.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.sanitized)), true) + XCTAssertEqual((try XCTUnwrap(reloaded.policy)), "default-v1") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +sanitized: true +policy: default-v1 + +""" + let instance = try RedactionMetadata.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.sanitized)), true) + XCTAssertEqual((try XCTUnwrap(instance.policy)), "default-v1") + let reloaded = try RedactionMetadata.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.sanitized)), true) + XCTAssertEqual((try XCTUnwrap(reloaded.policy)), "default-v1") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/RetryPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/RetryPayloadTests.swift new file mode 100644 index 000000000..b6b112200 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/RetryPayloadTests.swift @@ -0,0 +1,56 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class RetryPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "operation": "llm", + "attempt": 2, + "maxAttempts": 3, + "delayMs": 1250, + "reason": "rate_limit" +} +""" + let instance = try RetryPayload.fromJSON(json) + XCTAssertEqual(instance.operation, "llm") + XCTAssertEqual(instance.attempt, 2) + XCTAssertEqual((try XCTUnwrap(instance.maxAttempts)), 3) + XCTAssertEqual((try XCTUnwrap(instance.delayMs)), 1250) + XCTAssertEqual((try XCTUnwrap(instance.reason)), "rate_limit") + let reloaded = try RetryPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.operation, "llm") + XCTAssertEqual(reloaded.attempt, 2) + XCTAssertEqual((try XCTUnwrap(reloaded.maxAttempts)), 3) + XCTAssertEqual((try XCTUnwrap(reloaded.delayMs)), 1250) + XCTAssertEqual((try XCTUnwrap(reloaded.reason)), "rate_limit") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +operation: llm +attempt: 2 +maxAttempts: 3 +delayMs: 1250 +reason: rate_limit + +""" + let instance = try RetryPayload.fromYAML(yaml) + XCTAssertEqual(instance.operation, "llm") + XCTAssertEqual(instance.attempt, 2) + XCTAssertEqual((try XCTUnwrap(instance.maxAttempts)), 3) + XCTAssertEqual((try XCTUnwrap(instance.delayMs)), 1250) + XCTAssertEqual((try XCTUnwrap(instance.reason)), "rate_limit") + let reloaded = try RetryPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.operation, "llm") + XCTAssertEqual(reloaded.attempt, 2) + XCTAssertEqual((try XCTUnwrap(reloaded.maxAttempts)), 3) + XCTAssertEqual((try XCTUnwrap(reloaded.delayMs)), 1250) + XCTAssertEqual((try XCTUnwrap(reloaded.reason)), "rate_limit") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionEndPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionEndPayloadTests.swift new file mode 100644 index 000000000..b31907518 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionEndPayloadTests.swift @@ -0,0 +1,50 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class SessionEndPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "sessionId": "sess_abc123", + "status": "success", + "reason": "complete", + "durationMs": 12500 +} +""" + let instance = try SessionEndPayload.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.sessionId)), "sess_abc123") + XCTAssertEqual((try XCTUnwrap(instance.status)), SessionEndStatus.success) + XCTAssertEqual((try XCTUnwrap(instance.reason)), "complete") + XCTAssertEqual((try XCTUnwrap(instance.durationMs)), 12500) + let reloaded = try SessionEndPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.sessionId)), "sess_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.status)), SessionEndStatus.success) + XCTAssertEqual((try XCTUnwrap(reloaded.reason)), "complete") + XCTAssertEqual((try XCTUnwrap(reloaded.durationMs)), 12500) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +sessionId: sess_abc123 +status: success +reason: complete +durationMs: 12500 + +""" + let instance = try SessionEndPayload.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.sessionId)), "sess_abc123") + XCTAssertEqual((try XCTUnwrap(instance.status)), SessionEndStatus.success) + XCTAssertEqual((try XCTUnwrap(instance.reason)), "complete") + XCTAssertEqual((try XCTUnwrap(instance.durationMs)), 12500) + let reloaded = try SessionEndPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.sessionId)), "sess_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.status)), SessionEndStatus.success) + XCTAssertEqual((try XCTUnwrap(reloaded.reason)), "complete") + XCTAssertEqual((try XCTUnwrap(reloaded.durationMs)), 12500) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionEventTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionEventTests.swift new file mode 100644 index 000000000..a7f0d225b --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionEventTests.swift @@ -0,0 +1,62 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class SessionEventTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "id": "evt_abc123", + "timestamp": "2026-06-09T20:00:00Z", + "sessionId": "sess_abc123", + "turnId": "turn_001", + "parentId": "evt_parent", + "spanId": "span_hook_001" +} +""" + let instance = try SessionEvent.fromJSON(json) + XCTAssertEqual(instance.id, "evt_abc123") + XCTAssertEqual(instance.timestamp, "2026-06-09T20:00:00Z") + XCTAssertEqual((try XCTUnwrap(instance.sessionId)), "sess_abc123") + XCTAssertEqual((try XCTUnwrap(instance.turnId)), "turn_001") + XCTAssertEqual((try XCTUnwrap(instance.parentId)), "evt_parent") + XCTAssertEqual((try XCTUnwrap(instance.spanId)), "span_hook_001") + let reloaded = try SessionEvent.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.id, "evt_abc123") + XCTAssertEqual(reloaded.timestamp, "2026-06-09T20:00:00Z") + XCTAssertEqual((try XCTUnwrap(reloaded.sessionId)), "sess_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.turnId)), "turn_001") + XCTAssertEqual((try XCTUnwrap(reloaded.parentId)), "evt_parent") + XCTAssertEqual((try XCTUnwrap(reloaded.spanId)), "span_hook_001") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +id: evt_abc123 +timestamp: "2026-06-09T20:00:00Z" +sessionId: sess_abc123 +turnId: turn_001 +parentId: evt_parent +spanId: span_hook_001 + +""" + let instance = try SessionEvent.fromYAML(yaml) + XCTAssertEqual(instance.id, "evt_abc123") + XCTAssertEqual(instance.timestamp, "2026-06-09T20:00:00Z") + XCTAssertEqual((try XCTUnwrap(instance.sessionId)), "sess_abc123") + XCTAssertEqual((try XCTUnwrap(instance.turnId)), "turn_001") + XCTAssertEqual((try XCTUnwrap(instance.parentId)), "evt_parent") + XCTAssertEqual((try XCTUnwrap(instance.spanId)), "span_hook_001") + let reloaded = try SessionEvent.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.id, "evt_abc123") + XCTAssertEqual(reloaded.timestamp, "2026-06-09T20:00:00Z") + XCTAssertEqual((try XCTUnwrap(reloaded.sessionId)), "sess_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.turnId)), "turn_001") + XCTAssertEqual((try XCTUnwrap(reloaded.parentId)), "evt_parent") + XCTAssertEqual((try XCTUnwrap(reloaded.spanId)), "span_hook_001") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionFileRefTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionFileRefTests.swift new file mode 100644 index 000000000..9412df739 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionFileRefTests.swift @@ -0,0 +1,56 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class SessionFileRefTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "sessionId": "sess_abc123", + "path": "src/index.ts", + "toolName": "view", + "turnIndex": 2, + "firstSeenAt": "2026-06-09T20:00:00Z" +} +""" + let instance = try SessionFileRef.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.sessionId)), "sess_abc123") + XCTAssertEqual(instance.path, "src/index.ts") + XCTAssertEqual((try XCTUnwrap(instance.toolName)), "view") + XCTAssertEqual((try XCTUnwrap(instance.turnIndex)), 2) + XCTAssertEqual((try XCTUnwrap(instance.firstSeenAt)), "2026-06-09T20:00:00Z") + let reloaded = try SessionFileRef.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.sessionId)), "sess_abc123") + XCTAssertEqual(reloaded.path, "src/index.ts") + XCTAssertEqual((try XCTUnwrap(reloaded.toolName)), "view") + XCTAssertEqual((try XCTUnwrap(reloaded.turnIndex)), 2) + XCTAssertEqual((try XCTUnwrap(reloaded.firstSeenAt)), "2026-06-09T20:00:00Z") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +sessionId: sess_abc123 +path: src/index.ts +toolName: view +turnIndex: 2 +firstSeenAt: "2026-06-09T20:00:00Z" + +""" + let instance = try SessionFileRef.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.sessionId)), "sess_abc123") + XCTAssertEqual(instance.path, "src/index.ts") + XCTAssertEqual((try XCTUnwrap(instance.toolName)), "view") + XCTAssertEqual((try XCTUnwrap(instance.turnIndex)), 2) + XCTAssertEqual((try XCTUnwrap(instance.firstSeenAt)), "2026-06-09T20:00:00Z") + let reloaded = try SessionFileRef.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.sessionId)), "sess_abc123") + XCTAssertEqual(reloaded.path, "src/index.ts") + XCTAssertEqual((try XCTUnwrap(reloaded.toolName)), "view") + XCTAssertEqual((try XCTUnwrap(reloaded.turnIndex)), 2) + XCTAssertEqual((try XCTUnwrap(reloaded.firstSeenAt)), "2026-06-09T20:00:00Z") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionRefTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionRefTests.swift new file mode 100644 index 000000000..98cece169 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionRefTests.swift @@ -0,0 +1,56 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class SessionRefTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "sessionId": "sess_abc123", + "refType": "issue", + "refValue": "owner/repo#123", + "turnIndex": 2, + "createdAt": "2026-06-09T20:00:00Z" +} +""" + let instance = try SessionRef.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.sessionId)), "sess_abc123") + XCTAssertEqual(instance.refType, "issue") + XCTAssertEqual(instance.refValue, "owner/repo#123") + XCTAssertEqual((try XCTUnwrap(instance.turnIndex)), 2) + XCTAssertEqual((try XCTUnwrap(instance.createdAt)), "2026-06-09T20:00:00Z") + let reloaded = try SessionRef.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.sessionId)), "sess_abc123") + XCTAssertEqual(reloaded.refType, "issue") + XCTAssertEqual(reloaded.refValue, "owner/repo#123") + XCTAssertEqual((try XCTUnwrap(reloaded.turnIndex)), 2) + XCTAssertEqual((try XCTUnwrap(reloaded.createdAt)), "2026-06-09T20:00:00Z") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +sessionId: sess_abc123 +refType: issue +refValue: "owner/repo#123" +turnIndex: 2 +createdAt: "2026-06-09T20:00:00Z" + +""" + let instance = try SessionRef.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.sessionId)), "sess_abc123") + XCTAssertEqual(instance.refType, "issue") + XCTAssertEqual(instance.refValue, "owner/repo#123") + XCTAssertEqual((try XCTUnwrap(instance.turnIndex)), 2) + XCTAssertEqual((try XCTUnwrap(instance.createdAt)), "2026-06-09T20:00:00Z") + let reloaded = try SessionRef.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.sessionId)), "sess_abc123") + XCTAssertEqual(reloaded.refType, "issue") + XCTAssertEqual(reloaded.refValue, "owner/repo#123") + XCTAssertEqual((try XCTUnwrap(reloaded.turnIndex)), 2) + XCTAssertEqual((try XCTUnwrap(reloaded.createdAt)), "2026-06-09T20:00:00Z") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionStartPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionStartPayloadTests.swift new file mode 100644 index 000000000..de918bfbb --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionStartPayloadTests.swift @@ -0,0 +1,74 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class SessionStartPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "sessionId": "sess_abc123", + "schemaVersion": "1", + "producer": "prompty-agent", + "runtime": "typescript", + "promptyVersion": "2.0.0", + "startTime": "2026-06-09T20:00:00Z", + "selectedModel": "gpt-4o-mini", + "reasoningEffort": "medium" +} +""" + let instance = try SessionStartPayload.fromJSON(json) + XCTAssertEqual(instance.sessionId, "sess_abc123") + XCTAssertEqual((try XCTUnwrap(instance.schemaVersion)), "1") + XCTAssertEqual((try XCTUnwrap(instance.producer)), "prompty-agent") + XCTAssertEqual((try XCTUnwrap(instance.runtime)), "typescript") + XCTAssertEqual((try XCTUnwrap(instance.promptyVersion)), "2.0.0") + XCTAssertEqual((try XCTUnwrap(instance.startTime)), "2026-06-09T20:00:00Z") + XCTAssertEqual((try XCTUnwrap(instance.selectedModel)), "gpt-4o-mini") + XCTAssertEqual((try XCTUnwrap(instance.reasoningEffort)), "medium") + let reloaded = try SessionStartPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.sessionId, "sess_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.schemaVersion)), "1") + XCTAssertEqual((try XCTUnwrap(reloaded.producer)), "prompty-agent") + XCTAssertEqual((try XCTUnwrap(reloaded.runtime)), "typescript") + XCTAssertEqual((try XCTUnwrap(reloaded.promptyVersion)), "2.0.0") + XCTAssertEqual((try XCTUnwrap(reloaded.startTime)), "2026-06-09T20:00:00Z") + XCTAssertEqual((try XCTUnwrap(reloaded.selectedModel)), "gpt-4o-mini") + XCTAssertEqual((try XCTUnwrap(reloaded.reasoningEffort)), "medium") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +sessionId: sess_abc123 +schemaVersion: "1" +producer: prompty-agent +runtime: typescript +promptyVersion: 2.0.0 +startTime: "2026-06-09T20:00:00Z" +selectedModel: gpt-4o-mini +reasoningEffort: medium + +""" + let instance = try SessionStartPayload.fromYAML(yaml) + XCTAssertEqual(instance.sessionId, "sess_abc123") + XCTAssertEqual((try XCTUnwrap(instance.schemaVersion)), "1") + XCTAssertEqual((try XCTUnwrap(instance.producer)), "prompty-agent") + XCTAssertEqual((try XCTUnwrap(instance.runtime)), "typescript") + XCTAssertEqual((try XCTUnwrap(instance.promptyVersion)), "2.0.0") + XCTAssertEqual((try XCTUnwrap(instance.startTime)), "2026-06-09T20:00:00Z") + XCTAssertEqual((try XCTUnwrap(instance.selectedModel)), "gpt-4o-mini") + XCTAssertEqual((try XCTUnwrap(instance.reasoningEffort)), "medium") + let reloaded = try SessionStartPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.sessionId, "sess_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.schemaVersion)), "1") + XCTAssertEqual((try XCTUnwrap(reloaded.producer)), "prompty-agent") + XCTAssertEqual((try XCTUnwrap(reloaded.runtime)), "typescript") + XCTAssertEqual((try XCTUnwrap(reloaded.promptyVersion)), "2.0.0") + XCTAssertEqual((try XCTUnwrap(reloaded.startTime)), "2026-06-09T20:00:00Z") + XCTAssertEqual((try XCTUnwrap(reloaded.selectedModel)), "gpt-4o-mini") + XCTAssertEqual((try XCTUnwrap(reloaded.reasoningEffort)), "medium") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionSummaryTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionSummaryTests.swift new file mode 100644 index 000000000..8934221be --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionSummaryTests.swift @@ -0,0 +1,56 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class SessionSummaryTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "sessionId": "sess_abc123", + "status": "success", + "turns": 5, + "checkpoints": 2, + "durationMs": 12500 +} +""" + let instance = try SessionSummary.fromJSON(json) + XCTAssertEqual(instance.sessionId, "sess_abc123") + XCTAssertEqual((try XCTUnwrap(instance.status)), SessionSummaryStatus.success) + XCTAssertEqual((try XCTUnwrap(instance.turns)), 5) + XCTAssertEqual((try XCTUnwrap(instance.checkpoints)), 2) + XCTAssertEqual((try XCTUnwrap(instance.durationMs)), 12500) + let reloaded = try SessionSummary.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.sessionId, "sess_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.status)), SessionSummaryStatus.success) + XCTAssertEqual((try XCTUnwrap(reloaded.turns)), 5) + XCTAssertEqual((try XCTUnwrap(reloaded.checkpoints)), 2) + XCTAssertEqual((try XCTUnwrap(reloaded.durationMs)), 12500) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +sessionId: sess_abc123 +status: success +turns: 5 +checkpoints: 2 +durationMs: 12500 + +""" + let instance = try SessionSummary.fromYAML(yaml) + XCTAssertEqual(instance.sessionId, "sess_abc123") + XCTAssertEqual((try XCTUnwrap(instance.status)), SessionSummaryStatus.success) + XCTAssertEqual((try XCTUnwrap(instance.turns)), 5) + XCTAssertEqual((try XCTUnwrap(instance.checkpoints)), 2) + XCTAssertEqual((try XCTUnwrap(instance.durationMs)), 12500) + let reloaded = try SessionSummary.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.sessionId, "sess_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.status)), SessionSummaryStatus.success) + XCTAssertEqual((try XCTUnwrap(reloaded.turns)), 5) + XCTAssertEqual((try XCTUnwrap(reloaded.checkpoints)), 2) + XCTAssertEqual((try XCTUnwrap(reloaded.durationMs)), 12500) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionTraceTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionTraceTests.swift new file mode 100644 index 000000000..d9a7dbd66 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionTraceTests.swift @@ -0,0 +1,75 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class SessionTraceTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "version": "1", + "runtime": "typescript", + "promptyVersion": "2.0.0", + "sessionId": "sess_abc123", + "events": [ + { + "id": "evt_abc123", + "type": "session_start", + "timestamp": "2026-06-09T20:00:00Z", + "sessionId": "sess_abc123", + "turnId": "turn_001", + "parentId": "evt_parent", + "spanId": "span_hook_001", + "payload": {} + } + ] +} +""" + let instance = try SessionTrace.fromJSON(json) + XCTAssertEqual(instance.version, "1") + XCTAssertEqual((try XCTUnwrap(instance.runtime)), "typescript") + XCTAssertEqual((try XCTUnwrap(instance.promptyVersion)), "2.0.0") + XCTAssertEqual((try XCTUnwrap(instance.sessionId)), "sess_abc123") + XCTAssertEqual(instance.events.count, 1) + let reloaded = try SessionTrace.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.version, "1") + XCTAssertEqual((try XCTUnwrap(reloaded.runtime)), "typescript") + XCTAssertEqual((try XCTUnwrap(reloaded.promptyVersion)), "2.0.0") + XCTAssertEqual((try XCTUnwrap(reloaded.sessionId)), "sess_abc123") + XCTAssertEqual(reloaded.events.count, 1) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +version: "1" +runtime: typescript +promptyVersion: 2.0.0 +sessionId: sess_abc123 +events: + - id: evt_abc123 + type: session_start + timestamp: "2026-06-09T20:00:00Z" + sessionId: sess_abc123 + turnId: turn_001 + parentId: evt_parent + spanId: span_hook_001 + payload: {} + +""" + let instance = try SessionTrace.fromYAML(yaml) + XCTAssertEqual(instance.version, "1") + XCTAssertEqual((try XCTUnwrap(instance.runtime)), "typescript") + XCTAssertEqual((try XCTUnwrap(instance.promptyVersion)), "2.0.0") + XCTAssertEqual((try XCTUnwrap(instance.sessionId)), "sess_abc123") + XCTAssertEqual(instance.events.count, 1) + let reloaded = try SessionTrace.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.version, "1") + XCTAssertEqual((try XCTUnwrap(reloaded.runtime)), "typescript") + XCTAssertEqual((try XCTUnwrap(reloaded.promptyVersion)), "2.0.0") + XCTAssertEqual((try XCTUnwrap(reloaded.sessionId)), "sess_abc123") + XCTAssertEqual(reloaded.events.count, 1) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionWarningPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionWarningPayloadTests.swift new file mode 100644 index 000000000..f875703fa --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionWarningPayloadTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class SessionWarningPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "warningType": "remote", + "message": "Remote session disabled" +} +""" + let instance = try SessionWarningPayload.fromJSON(json) + XCTAssertEqual(instance.warningType, "remote") + XCTAssertEqual(instance.message, "Remote session disabled") + let reloaded = try SessionWarningPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.warningType, "remote") + XCTAssertEqual(reloaded.message, "Remote session disabled") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +warningType: remote +message: Remote session disabled + +""" + let instance = try SessionWarningPayload.fromYAML(yaml) + XCTAssertEqual(instance.warningType, "remote") + XCTAssertEqual(instance.message, "Remote session disabled") + let reloaded = try SessionWarningPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.warningType, "remote") + XCTAssertEqual(reloaded.message, "Remote session disabled") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/StatusEventPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/StatusEventPayloadTests.swift new file mode 100644 index 000000000..626586a60 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/StatusEventPayloadTests.swift @@ -0,0 +1,32 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class StatusEventPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "message": "Starting iteration 3" +} +""" + let instance = try StatusEventPayload.fromJSON(json) + XCTAssertEqual(instance.message, "Starting iteration 3") + let reloaded = try StatusEventPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.message, "Starting iteration 3") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +message: Starting iteration 3 + +""" + let instance = try StatusEventPayload.fromYAML(yaml) + XCTAssertEqual(instance.message, "Starting iteration 3") + let reloaded = try StatusEventPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.message, "Starting iteration 3") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/StreamChunkTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/StreamChunkTests.swift new file mode 100644 index 000000000..91b3804ea --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/StreamChunkTests.swift @@ -0,0 +1,50 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class StreamChunkTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "kind": "text", + "value": "Hello" +} +""" + let instance = try StreamChunk.fromJSON(json) + if case .textChunk(let concrete) = instance { + XCTAssertEqual(concrete.value, "Hello") + } else { + XCTFail("Expected TextChunk") + } + let reloaded = try StreamChunk.fromJSON(try instance.toJSON()) + if case .textChunk(let concrete) = reloaded { + XCTAssertEqual(concrete.value, "Hello") + } else { + XCTFail("Expected TextChunk") + } + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +kind: text +value: Hello + +""" + let instance = try StreamChunk.fromYAML(yaml) + if case .textChunk(let concrete) = instance { + XCTAssertEqual(concrete.value, "Hello") + } else { + XCTFail("Expected TextChunk") + } + let reloaded = try StreamChunk.fromYAML(try instance.toYAML()) + if case .textChunk(let concrete) = reloaded { + XCTAssertEqual(concrete.value, "Hello") + } else { + XCTFail("Expected TextChunk") + } + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ThinkingEventPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ThinkingEventPayloadTests.swift new file mode 100644 index 000000000..ea51fa9b8 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ThinkingEventPayloadTests.swift @@ -0,0 +1,32 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ThinkingEventPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "token": "Let me consider..." +} +""" + let instance = try ThinkingEventPayload.fromJSON(json) + XCTAssertEqual(instance.token, "Let me consider...") + let reloaded = try ThinkingEventPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.token, "Let me consider...") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +token: Let me consider... + +""" + let instance = try ThinkingEventPayload.fromYAML(yaml) + XCTAssertEqual(instance.token, "Let me consider...") + let reloaded = try ThinkingEventPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.token, "Let me consider...") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TokenEventPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TokenEventPayloadTests.swift new file mode 100644 index 000000000..ebf580324 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TokenEventPayloadTests.swift @@ -0,0 +1,32 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class TokenEventPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "token": "Hello" +} +""" + let instance = try TokenEventPayload.fromJSON(json) + XCTAssertEqual(instance.token, "Hello") + let reloaded = try TokenEventPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.token, "Hello") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +token: Hello + +""" + let instance = try TokenEventPayload.fromYAML(yaml) + XCTAssertEqual(instance.token, "Hello") + let reloaded = try TokenEventPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.token, "Hello") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ToolCallCompletePayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ToolCallCompletePayloadTests.swift new file mode 100644 index 000000000..d4990408e --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ToolCallCompletePayloadTests.swift @@ -0,0 +1,56 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ToolCallCompletePayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "id": "call_abc123", + "name": "get_weather", + "success": true, + "durationMs": 42, + "errorKind": "timeout" +} +""" + let instance = try ToolCallCompletePayload.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.id)), "call_abc123") + XCTAssertEqual(instance.name, "get_weather") + XCTAssertEqual(instance.success, true) + XCTAssertEqual((try XCTUnwrap(instance.durationMs)), 42) + XCTAssertEqual((try XCTUnwrap(instance.errorKind)), "timeout") + let reloaded = try ToolCallCompletePayload.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.id)), "call_abc123") + XCTAssertEqual(reloaded.name, "get_weather") + XCTAssertEqual(reloaded.success, true) + XCTAssertEqual((try XCTUnwrap(reloaded.durationMs)), 42) + XCTAssertEqual((try XCTUnwrap(reloaded.errorKind)), "timeout") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +id: call_abc123 +name: get_weather +success: true +durationMs: 42 +errorKind: timeout + +""" + let instance = try ToolCallCompletePayload.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.id)), "call_abc123") + XCTAssertEqual(instance.name, "get_weather") + XCTAssertEqual(instance.success, true) + XCTAssertEqual((try XCTUnwrap(instance.durationMs)), 42) + XCTAssertEqual((try XCTUnwrap(instance.errorKind)), "timeout") + let reloaded = try ToolCallCompletePayload.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.id)), "call_abc123") + XCTAssertEqual(reloaded.name, "get_weather") + XCTAssertEqual(reloaded.success, true) + XCTAssertEqual((try XCTUnwrap(reloaded.durationMs)), 42) + XCTAssertEqual((try XCTUnwrap(reloaded.errorKind)), "timeout") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ToolCallStartPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ToolCallStartPayloadTests.swift new file mode 100644 index 000000000..7dda75ba0 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ToolCallStartPayloadTests.swift @@ -0,0 +1,44 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ToolCallStartPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "id": "call_abc123", + "name": "get_weather", + "arguments": "{\\"city\\": \\"Paris\\"}" +} +""" + let instance = try ToolCallStartPayload.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.id)), "call_abc123") + XCTAssertEqual(instance.name, "get_weather") + XCTAssertEqual(instance.arguments, "{\"city\": \"Paris\"}") + let reloaded = try ToolCallStartPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.id)), "call_abc123") + XCTAssertEqual(reloaded.name, "get_weather") + XCTAssertEqual(reloaded.arguments, "{\"city\": \"Paris\"}") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +id: call_abc123 +name: get_weather +arguments: "{\\"city\\": \\"Paris\\"}" + +""" + let instance = try ToolCallStartPayload.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.id)), "call_abc123") + XCTAssertEqual(instance.name, "get_weather") + XCTAssertEqual(instance.arguments, "{\"city\": \"Paris\"}") + let reloaded = try ToolCallStartPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.id)), "call_abc123") + XCTAssertEqual(reloaded.name, "get_weather") + XCTAssertEqual(reloaded.arguments, "{\"city\": \"Paris\"}") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ToolExecutionCompletePayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ToolExecutionCompletePayloadTests.swift new file mode 100644 index 000000000..9fa7a8e79 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ToolExecutionCompletePayloadTests.swift @@ -0,0 +1,68 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ToolExecutionCompletePayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "requestId": "exec_abc123", + "toolCallId": "call_abc123", + "toolName": "powershell", + "success": true, + "exitCode": 0, + "durationMs": 250, + "errorKind": "timeout" +} +""" + let instance = try ToolExecutionCompletePayload.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.requestId)), "exec_abc123") + XCTAssertEqual((try XCTUnwrap(instance.toolCallId)), "call_abc123") + XCTAssertEqual(instance.toolName, "powershell") + XCTAssertEqual(instance.success, true) + XCTAssertEqual((try XCTUnwrap(instance.exitCode)), 0) + XCTAssertEqual((try XCTUnwrap(instance.durationMs)), 250) + XCTAssertEqual((try XCTUnwrap(instance.errorKind)), "timeout") + let reloaded = try ToolExecutionCompletePayload.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.requestId)), "exec_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.toolCallId)), "call_abc123") + XCTAssertEqual(reloaded.toolName, "powershell") + XCTAssertEqual(reloaded.success, true) + XCTAssertEqual((try XCTUnwrap(reloaded.exitCode)), 0) + XCTAssertEqual((try XCTUnwrap(reloaded.durationMs)), 250) + XCTAssertEqual((try XCTUnwrap(reloaded.errorKind)), "timeout") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +requestId: exec_abc123 +toolCallId: call_abc123 +toolName: powershell +success: true +exitCode: 0 +durationMs: 250 +errorKind: timeout + +""" + let instance = try ToolExecutionCompletePayload.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.requestId)), "exec_abc123") + XCTAssertEqual((try XCTUnwrap(instance.toolCallId)), "call_abc123") + XCTAssertEqual(instance.toolName, "powershell") + XCTAssertEqual(instance.success, true) + XCTAssertEqual((try XCTUnwrap(instance.exitCode)), 0) + XCTAssertEqual((try XCTUnwrap(instance.durationMs)), 250) + XCTAssertEqual((try XCTUnwrap(instance.errorKind)), "timeout") + let reloaded = try ToolExecutionCompletePayload.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.requestId)), "exec_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.toolCallId)), "call_abc123") + XCTAssertEqual(reloaded.toolName, "powershell") + XCTAssertEqual(reloaded.success, true) + XCTAssertEqual((try XCTUnwrap(reloaded.exitCode)), 0) + XCTAssertEqual((try XCTUnwrap(reloaded.durationMs)), 250) + XCTAssertEqual((try XCTUnwrap(reloaded.errorKind)), "timeout") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ToolExecutionStartPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ToolExecutionStartPayloadTests.swift new file mode 100644 index 000000000..1d3b45642 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ToolExecutionStartPayloadTests.swift @@ -0,0 +1,50 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ToolExecutionStartPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "requestId": "exec_abc123", + "toolCallId": "call_abc123", + "toolName": "powershell", + "workingDirectory": "/workspace/project" +} +""" + let instance = try ToolExecutionStartPayload.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.requestId)), "exec_abc123") + XCTAssertEqual((try XCTUnwrap(instance.toolCallId)), "call_abc123") + XCTAssertEqual(instance.toolName, "powershell") + XCTAssertEqual((try XCTUnwrap(instance.workingDirectory)), "/workspace/project") + let reloaded = try ToolExecutionStartPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.requestId)), "exec_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.toolCallId)), "call_abc123") + XCTAssertEqual(reloaded.toolName, "powershell") + XCTAssertEqual((try XCTUnwrap(reloaded.workingDirectory)), "/workspace/project") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +requestId: exec_abc123 +toolCallId: call_abc123 +toolName: powershell +workingDirectory: /workspace/project + +""" + let instance = try ToolExecutionStartPayload.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.requestId)), "exec_abc123") + XCTAssertEqual((try XCTUnwrap(instance.toolCallId)), "call_abc123") + XCTAssertEqual(instance.toolName, "powershell") + XCTAssertEqual((try XCTUnwrap(instance.workingDirectory)), "/workspace/project") + let reloaded = try ToolExecutionStartPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.requestId)), "exec_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.toolCallId)), "call_abc123") + XCTAssertEqual(reloaded.toolName, "powershell") + XCTAssertEqual((try XCTUnwrap(reloaded.workingDirectory)), "/workspace/project") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ToolResultPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ToolResultPayloadTests.swift new file mode 100644 index 000000000..3191ed048 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ToolResultPayloadTests.swift @@ -0,0 +1,44 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ToolResultPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } +} +""" + let instance = try ToolResultPayload.fromJSON(json) + XCTAssertEqual(instance.name, "get_weather") + let reloaded = try ToolResultPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.name, "get_weather") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +name: get_weather +result: + parts: + - kind: text + value: 72°F and sunny + +""" + let instance = try ToolResultPayload.fromYAML(yaml) + XCTAssertEqual(instance.name, "get_weather") + let reloaded = try ToolResultPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.name, "get_weather") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TrajectoryEventTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TrajectoryEventTests.swift new file mode 100644 index 000000000..00cd4f8e8 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TrajectoryEventTests.swift @@ -0,0 +1,68 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class TrajectoryEventTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "id": "traj_abc123", + "sessionId": "sess_abc123", + "turnId": "turn_001", + "toolCallId": "call_abc123", + "turnIndex": 4, + "eventType": "command", + "createdAt": "2026-06-09T20:00:00Z" +} +""" + let instance = try TrajectoryEvent.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.id)), "traj_abc123") + XCTAssertEqual((try XCTUnwrap(instance.sessionId)), "sess_abc123") + XCTAssertEqual((try XCTUnwrap(instance.turnId)), "turn_001") + XCTAssertEqual((try XCTUnwrap(instance.toolCallId)), "call_abc123") + XCTAssertEqual((try XCTUnwrap(instance.turnIndex)), 4) + XCTAssertEqual(instance.eventType, "command") + XCTAssertEqual((try XCTUnwrap(instance.createdAt)), "2026-06-09T20:00:00Z") + let reloaded = try TrajectoryEvent.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.id)), "traj_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.sessionId)), "sess_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.turnId)), "turn_001") + XCTAssertEqual((try XCTUnwrap(reloaded.toolCallId)), "call_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.turnIndex)), 4) + XCTAssertEqual(reloaded.eventType, "command") + XCTAssertEqual((try XCTUnwrap(reloaded.createdAt)), "2026-06-09T20:00:00Z") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +id: traj_abc123 +sessionId: sess_abc123 +turnId: turn_001 +toolCallId: call_abc123 +turnIndex: 4 +eventType: command +createdAt: "2026-06-09T20:00:00Z" + +""" + let instance = try TrajectoryEvent.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.id)), "traj_abc123") + XCTAssertEqual((try XCTUnwrap(instance.sessionId)), "sess_abc123") + XCTAssertEqual((try XCTUnwrap(instance.turnId)), "turn_001") + XCTAssertEqual((try XCTUnwrap(instance.toolCallId)), "call_abc123") + XCTAssertEqual((try XCTUnwrap(instance.turnIndex)), 4) + XCTAssertEqual(instance.eventType, "command") + XCTAssertEqual((try XCTUnwrap(instance.createdAt)), "2026-06-09T20:00:00Z") + let reloaded = try TrajectoryEvent.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.id)), "traj_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.sessionId)), "sess_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.turnId)), "turn_001") + XCTAssertEqual((try XCTUnwrap(reloaded.toolCallId)), "call_abc123") + XCTAssertEqual((try XCTUnwrap(reloaded.turnIndex)), 4) + XCTAssertEqual(reloaded.eventType, "command") + XCTAssertEqual((try XCTUnwrap(reloaded.createdAt)), "2026-06-09T20:00:00Z") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TurnEndPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TurnEndPayloadTests.swift new file mode 100644 index 000000000..bacd6d637 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TurnEndPayloadTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class TurnEndPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "iterations": 2, + "durationMs": 1500 +} +""" + let instance = try TurnEndPayload.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.iterations)), 2) + XCTAssertEqual((try XCTUnwrap(instance.durationMs)), 1500) + let reloaded = try TurnEndPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.iterations)), 2) + XCTAssertEqual((try XCTUnwrap(reloaded.durationMs)), 1500) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +iterations: 2 +durationMs: 1500 + +""" + let instance = try TurnEndPayload.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.iterations)), 2) + XCTAssertEqual((try XCTUnwrap(instance.durationMs)), 1500) + let reloaded = try TurnEndPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.iterations)), 2) + XCTAssertEqual((try XCTUnwrap(reloaded.durationMs)), 1500) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TurnEventTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TurnEventTests.swift new file mode 100644 index 000000000..736eb8b3c --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TurnEventTests.swift @@ -0,0 +1,62 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class TurnEventTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "id": "evt_abc123", + "timestamp": "2026-06-09T20:00:00Z", + "turnId": "turn_001", + "iteration": 0, + "parentId": "evt_parent", + "spanId": "span_tool_001" +} +""" + let instance = try TurnEvent.fromJSON(json) + XCTAssertEqual(instance.id, "evt_abc123") + XCTAssertEqual(instance.timestamp, "2026-06-09T20:00:00Z") + XCTAssertEqual((try XCTUnwrap(instance.turnId)), "turn_001") + XCTAssertEqual((try XCTUnwrap(instance.iteration)), 0) + XCTAssertEqual((try XCTUnwrap(instance.parentId)), "evt_parent") + XCTAssertEqual((try XCTUnwrap(instance.spanId)), "span_tool_001") + let reloaded = try TurnEvent.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.id, "evt_abc123") + XCTAssertEqual(reloaded.timestamp, "2026-06-09T20:00:00Z") + XCTAssertEqual((try XCTUnwrap(reloaded.turnId)), "turn_001") + XCTAssertEqual((try XCTUnwrap(reloaded.iteration)), 0) + XCTAssertEqual((try XCTUnwrap(reloaded.parentId)), "evt_parent") + XCTAssertEqual((try XCTUnwrap(reloaded.spanId)), "span_tool_001") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +id: evt_abc123 +timestamp: "2026-06-09T20:00:00Z" +turnId: turn_001 +iteration: 0 +parentId: evt_parent +spanId: span_tool_001 + +""" + let instance = try TurnEvent.fromYAML(yaml) + XCTAssertEqual(instance.id, "evt_abc123") + XCTAssertEqual(instance.timestamp, "2026-06-09T20:00:00Z") + XCTAssertEqual((try XCTUnwrap(instance.turnId)), "turn_001") + XCTAssertEqual((try XCTUnwrap(instance.iteration)), 0) + XCTAssertEqual((try XCTUnwrap(instance.parentId)), "evt_parent") + XCTAssertEqual((try XCTUnwrap(instance.spanId)), "span_tool_001") + let reloaded = try TurnEvent.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.id, "evt_abc123") + XCTAssertEqual(reloaded.timestamp, "2026-06-09T20:00:00Z") + XCTAssertEqual((try XCTUnwrap(reloaded.turnId)), "turn_001") + XCTAssertEqual((try XCTUnwrap(reloaded.iteration)), 0) + XCTAssertEqual((try XCTUnwrap(reloaded.parentId)), "evt_parent") + XCTAssertEqual((try XCTUnwrap(reloaded.spanId)), "span_tool_001") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TurnStartPayloadTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TurnStartPayloadTests.swift new file mode 100644 index 000000000..759aa99f1 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TurnStartPayloadTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class TurnStartPayloadTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "agent": "weather-agent", + "maxIterations": 10 +} +""" + let instance = try TurnStartPayload.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.agent)), "weather-agent") + XCTAssertEqual((try XCTUnwrap(instance.maxIterations)), 10) + let reloaded = try TurnStartPayload.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.agent)), "weather-agent") + XCTAssertEqual((try XCTUnwrap(reloaded.maxIterations)), 10) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +agent: weather-agent +maxIterations: 10 + +""" + let instance = try TurnStartPayload.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.agent)), "weather-agent") + XCTAssertEqual((try XCTUnwrap(instance.maxIterations)), 10) + let reloaded = try TurnStartPayload.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.agent)), "weather-agent") + XCTAssertEqual((try XCTUnwrap(reloaded.maxIterations)), 10) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TurnSummaryTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TurnSummaryTests.swift new file mode 100644 index 000000000..1bb142fc0 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TurnSummaryTests.swift @@ -0,0 +1,68 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class TurnSummaryTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "turnId": "turn_001", + "status": "success", + "iterations": 2, + "llmCalls": 3, + "toolCalls": 2, + "retries": 1, + "durationMs": 2500 +} +""" + let instance = try TurnSummary.fromJSON(json) + XCTAssertEqual(instance.turnId, "turn_001") + XCTAssertEqual(instance.status, "success") + XCTAssertEqual(instance.iterations, 2) + XCTAssertEqual((try XCTUnwrap(instance.llmCalls)), 3) + XCTAssertEqual((try XCTUnwrap(instance.toolCalls)), 2) + XCTAssertEqual((try XCTUnwrap(instance.retries)), 1) + XCTAssertEqual((try XCTUnwrap(instance.durationMs)), 2500) + let reloaded = try TurnSummary.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.turnId, "turn_001") + XCTAssertEqual(reloaded.status, "success") + XCTAssertEqual(reloaded.iterations, 2) + XCTAssertEqual((try XCTUnwrap(reloaded.llmCalls)), 3) + XCTAssertEqual((try XCTUnwrap(reloaded.toolCalls)), 2) + XCTAssertEqual((try XCTUnwrap(reloaded.retries)), 1) + XCTAssertEqual((try XCTUnwrap(reloaded.durationMs)), 2500) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +turnId: turn_001 +status: success +iterations: 2 +llmCalls: 3 +toolCalls: 2 +retries: 1 +durationMs: 2500 + +""" + let instance = try TurnSummary.fromYAML(yaml) + XCTAssertEqual(instance.turnId, "turn_001") + XCTAssertEqual(instance.status, "success") + XCTAssertEqual(instance.iterations, 2) + XCTAssertEqual((try XCTUnwrap(instance.llmCalls)), 3) + XCTAssertEqual((try XCTUnwrap(instance.toolCalls)), 2) + XCTAssertEqual((try XCTUnwrap(instance.retries)), 1) + XCTAssertEqual((try XCTUnwrap(instance.durationMs)), 2500) + let reloaded = try TurnSummary.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.turnId, "turn_001") + XCTAssertEqual(reloaded.status, "success") + XCTAssertEqual(reloaded.iterations, 2) + XCTAssertEqual((try XCTUnwrap(reloaded.llmCalls)), 3) + XCTAssertEqual((try XCTUnwrap(reloaded.toolCalls)), 2) + XCTAssertEqual((try XCTUnwrap(reloaded.retries)), 1) + XCTAssertEqual((try XCTUnwrap(reloaded.durationMs)), 2500) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TurnTraceTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TurnTraceTests.swift new file mode 100644 index 000000000..96d3f5f5e --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TurnTraceTests.swift @@ -0,0 +1,69 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class TurnTraceTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "version": "1", + "runtime": "typescript", + "promptyVersion": "2.0.0", + "events": [ + { + "id": "evt_abc123", + "type": "turn_start", + "timestamp": "2026-06-09T20:00:00Z", + "turnId": "turn_001", + "iteration": 0, + "parentId": "evt_parent", + "spanId": "span_tool_001", + "payload": {} + } + ] +} +""" + let instance = try TurnTrace.fromJSON(json) + XCTAssertEqual(instance.version, "1") + XCTAssertEqual((try XCTUnwrap(instance.runtime)), "typescript") + XCTAssertEqual((try XCTUnwrap(instance.promptyVersion)), "2.0.0") + XCTAssertEqual(instance.events.count, 1) + let reloaded = try TurnTrace.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.version, "1") + XCTAssertEqual((try XCTUnwrap(reloaded.runtime)), "typescript") + XCTAssertEqual((try XCTUnwrap(reloaded.promptyVersion)), "2.0.0") + XCTAssertEqual(reloaded.events.count, 1) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +version: "1" +runtime: typescript +promptyVersion: 2.0.0 +events: + - id: evt_abc123 + type: turn_start + timestamp: "2026-06-09T20:00:00Z" + turnId: turn_001 + iteration: 0 + parentId: evt_parent + spanId: span_tool_001 + payload: {} + +""" + let instance = try TurnTrace.fromYAML(yaml) + XCTAssertEqual(instance.version, "1") + XCTAssertEqual((try XCTUnwrap(instance.runtime)), "typescript") + XCTAssertEqual((try XCTUnwrap(instance.promptyVersion)), "2.0.0") + XCTAssertEqual(instance.events.count, 1) + let reloaded = try TurnTrace.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.version, "1") + XCTAssertEqual((try XCTUnwrap(reloaded.runtime)), "typescript") + XCTAssertEqual((try XCTUnwrap(reloaded.promptyVersion)), "2.0.0") + XCTAssertEqual(reloaded.events.count, 1) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Guardrails/GuardrailResultTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Guardrails/GuardrailResultTests.swift new file mode 100644 index 000000000..6645221a7 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Guardrails/GuardrailResultTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class GuardrailResultTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "allowed": true, + "reason": "Content is safe" +} +""" + let instance = try GuardrailResult.fromJSON(json) + XCTAssertEqual(instance.allowed, true) + XCTAssertEqual((try XCTUnwrap(instance.reason)), "Content is safe") + let reloaded = try GuardrailResult.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.allowed, true) + XCTAssertEqual((try XCTUnwrap(reloaded.reason)), "Content is safe") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +allowed: true +reason: Content is safe + +""" + let instance = try GuardrailResult.fromYAML(yaml) + XCTAssertEqual(instance.allowed, true) + XCTAssertEqual((try XCTUnwrap(instance.reason)), "Content is safe") + let reloaded = try GuardrailResult.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.allowed, true) + XCTAssertEqual((try XCTUnwrap(reloaded.reason)), "Content is safe") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Memory/MemoryEntryTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Memory/MemoryEntryTests.swift new file mode 100644 index 000000000..0c0fc952a --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Memory/MemoryEntryTests.swift @@ -0,0 +1,55 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class MemoryEntryTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "content": "The user prefers concise answers.", + "category": "core", + "createdAt": "2026-06-09T20:00:00Z", + "tags": [ + "preference", + "tone" + ] +} +""" + let instance = try MemoryEntry.fromJSON(json) + XCTAssertEqual(instance.content, "The user prefers concise answers.") + XCTAssertEqual(instance.category, MemoryCategory.core) + XCTAssertEqual((try XCTUnwrap(instance.createdAt)), "2026-06-09T20:00:00Z") + XCTAssertEqual((try XCTUnwrap(instance.tags)).count, 2) + let reloaded = try MemoryEntry.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.content, "The user prefers concise answers.") + XCTAssertEqual(reloaded.category, MemoryCategory.core) + XCTAssertEqual((try XCTUnwrap(reloaded.createdAt)), "2026-06-09T20:00:00Z") + XCTAssertEqual((try XCTUnwrap(reloaded.tags)).count, 2) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +content: The user prefers concise answers. +category: core +createdAt: "2026-06-09T20:00:00Z" +tags: + - preference + - tone + +""" + let instance = try MemoryEntry.fromYAML(yaml) + XCTAssertEqual(instance.content, "The user prefers concise answers.") + XCTAssertEqual(instance.category, MemoryCategory.core) + XCTAssertEqual((try XCTUnwrap(instance.createdAt)), "2026-06-09T20:00:00Z") + XCTAssertEqual((try XCTUnwrap(instance.tags)).count, 2) + let reloaded = try MemoryEntry.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.content, "The user prefers concise answers.") + XCTAssertEqual(reloaded.category, MemoryCategory.core) + XCTAssertEqual((try XCTUnwrap(reloaded.createdAt)), "2026-06-09T20:00:00Z") + XCTAssertEqual((try XCTUnwrap(reloaded.tags)).count, 2) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Memory/MemoryStoreTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Memory/MemoryStoreTests.swift new file mode 100644 index 000000000..6c997447f --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Memory/MemoryStoreTests.swift @@ -0,0 +1,32 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class MemoryStoreTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "entries": [] +} +""" + let instance = try MemoryStore.fromJSON(json) + XCTAssertEqual(instance.entries.count, 0) + let reloaded = try MemoryStore.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.entries.count, 0) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +entries: [] + +""" + let instance = try MemoryStore.fromYAML(yaml) + XCTAssertEqual(instance.entries.count, 0) + let reloaded = try MemoryStore.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.entries.count, 0) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/AiResourceInfoTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/AiResourceInfoTests.swift new file mode 100644 index 000000000..39e03f06f --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/AiResourceInfoTests.swift @@ -0,0 +1,32 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class AiResourceInfoTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "serviceUrl": "https://my-resource.services.ai.azure.com" +} +""" + let instance = try AiResourceInfo.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.serviceUrl)), "https://my-resource.services.ai.azure.com") + let reloaded = try AiResourceInfo.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.serviceUrl)), "https://my-resource.services.ai.azure.com") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +serviceUrl: "https://my-resource.services.ai.azure.com" + +""" + let instance = try AiResourceInfo.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.serviceUrl)), "https://my-resource.services.ai.azure.com") + let reloaded = try AiResourceInfo.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.serviceUrl)), "https://my-resource.services.ai.azure.com") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/InvocationUsageTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/InvocationUsageTests.swift new file mode 100644 index 000000000..e8878d0c4 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/InvocationUsageTests.swift @@ -0,0 +1,44 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class InvocationUsageTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "inputTokens": 150, + "outputTokens": 42, + "totalTokens": 192 +} +""" + let instance = try InvocationUsage.fromJSON(json) + XCTAssertEqual(instance.inputTokens, 150) + XCTAssertEqual(instance.outputTokens, 42) + XCTAssertEqual(instance.totalTokens, 192) + let reloaded = try InvocationUsage.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.inputTokens, 150) + XCTAssertEqual(reloaded.outputTokens, 42) + XCTAssertEqual(reloaded.totalTokens, 192) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +inputTokens: 150 +outputTokens: 42 +totalTokens: 192 + +""" + let instance = try InvocationUsage.fromYAML(yaml) + XCTAssertEqual(instance.inputTokens, 150) + XCTAssertEqual(instance.outputTokens, 42) + XCTAssertEqual(instance.totalTokens, 192) + let reloaded = try InvocationUsage.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.inputTokens, 150) + XCTAssertEqual(reloaded.outputTokens, 42) + XCTAssertEqual(reloaded.totalTokens, 192) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/ModelInfoTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/ModelInfoTests.swift new file mode 100644 index 000000000..3fe35f3be --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/ModelInfoTests.swift @@ -0,0 +1,79 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ModelInfoTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "id": "gpt-4o", + "displayName": "GPT-4o", + "ownedBy": "openai", + "contextWindow": 128000, + "inputModalities": [ + "text", + "image" + ], + "outputModalities": [ + "text" + ], + "additionalProperties": { + "supportsStreaming": true + } +} +""" + let instance = try ModelInfo.fromJSON(json) + XCTAssertEqual(instance.id, "gpt-4o") + XCTAssertEqual((try XCTUnwrap(instance.displayName)), "GPT-4o") + XCTAssertEqual((try XCTUnwrap(instance.ownedBy)), "openai") + XCTAssertEqual((try XCTUnwrap(instance.contextWindow)), 128000) + XCTAssertEqual((try XCTUnwrap(instance.inputModalities)).count, 2) + XCTAssertEqual((try XCTUnwrap(instance.outputModalities)).count, 1) + XCTAssertNotNil(instance.additionalProperties) + let reloaded = try ModelInfo.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.id, "gpt-4o") + XCTAssertEqual((try XCTUnwrap(reloaded.displayName)), "GPT-4o") + XCTAssertEqual((try XCTUnwrap(reloaded.ownedBy)), "openai") + XCTAssertEqual((try XCTUnwrap(reloaded.contextWindow)), 128000) + XCTAssertEqual((try XCTUnwrap(reloaded.inputModalities)).count, 2) + XCTAssertEqual((try XCTUnwrap(reloaded.outputModalities)).count, 1) + XCTAssertNotNil(reloaded.additionalProperties) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +id: gpt-4o +displayName: GPT-4o +ownedBy: openai +contextWindow: 128000 +inputModalities: + - text + - image +outputModalities: + - text +additionalProperties: + supportsStreaming: true + +""" + let instance = try ModelInfo.fromYAML(yaml) + XCTAssertEqual(instance.id, "gpt-4o") + XCTAssertEqual((try XCTUnwrap(instance.displayName)), "GPT-4o") + XCTAssertEqual((try XCTUnwrap(instance.ownedBy)), "openai") + XCTAssertEqual((try XCTUnwrap(instance.contextWindow)), 128000) + XCTAssertEqual((try XCTUnwrap(instance.inputModalities)).count, 2) + XCTAssertEqual((try XCTUnwrap(instance.outputModalities)).count, 1) + XCTAssertNotNil(instance.additionalProperties) + let reloaded = try ModelInfo.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.id, "gpt-4o") + XCTAssertEqual((try XCTUnwrap(reloaded.displayName)), "GPT-4o") + XCTAssertEqual((try XCTUnwrap(reloaded.ownedBy)), "openai") + XCTAssertEqual((try XCTUnwrap(reloaded.contextWindow)), 128000) + XCTAssertEqual((try XCTUnwrap(reloaded.inputModalities)).count, 2) + XCTAssertEqual((try XCTUnwrap(reloaded.outputModalities)).count, 1) + XCTAssertNotNil(reloaded.additionalProperties) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/ModelOptionsTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/ModelOptionsTests.swift new file mode 100644 index 000000000..4bb6fd443 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/ModelOptionsTests.swift @@ -0,0 +1,96 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ModelOptionsTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "frequencyPenalty": 0.5, + "maxOutputTokens": 2048, + "presencePenalty": 0.3, + "seed": 42, + "temperature": 0.7, + "topK": 40, + "topP": 0.9, + "stopSequences": [ + "\\n", + "###" + ], + "allowMultipleToolCalls": true, + "additionalProperties": { + "customProperty": "value", + "anotherProperty": "anotherValue" + } +} +""" + let instance = try ModelOptions.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.frequencyPenalty)), 0.5) + XCTAssertEqual((try XCTUnwrap(instance.maxOutputTokens)), 2048) + XCTAssertEqual((try XCTUnwrap(instance.presencePenalty)), 0.3) + XCTAssertEqual((try XCTUnwrap(instance.seed)), 42) + XCTAssertEqual((try XCTUnwrap(instance.temperature)), 0.7) + XCTAssertEqual((try XCTUnwrap(instance.topK)), 40) + XCTAssertEqual((try XCTUnwrap(instance.topP)), 0.9) + XCTAssertEqual((try XCTUnwrap(instance.allowMultipleToolCalls)), true) + XCTAssertEqual((try XCTUnwrap(instance.stopSequences)).count, 2) + XCTAssertNotNil(instance.additionalProperties) + let reloaded = try ModelOptions.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.frequencyPenalty)), 0.5) + XCTAssertEqual((try XCTUnwrap(reloaded.maxOutputTokens)), 2048) + XCTAssertEqual((try XCTUnwrap(reloaded.presencePenalty)), 0.3) + XCTAssertEqual((try XCTUnwrap(reloaded.seed)), 42) + XCTAssertEqual((try XCTUnwrap(reloaded.temperature)), 0.7) + XCTAssertEqual((try XCTUnwrap(reloaded.topK)), 40) + XCTAssertEqual((try XCTUnwrap(reloaded.topP)), 0.9) + XCTAssertEqual((try XCTUnwrap(reloaded.allowMultipleToolCalls)), true) + XCTAssertEqual((try XCTUnwrap(reloaded.stopSequences)).count, 2) + XCTAssertNotNil(reloaded.additionalProperties) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +frequencyPenalty: 0.5 +maxOutputTokens: 2048 +presencePenalty: 0.3 +seed: 42 +temperature: 0.7 +topK: 40 +topP: 0.9 +stopSequences: + - "\\n" + - "###" +allowMultipleToolCalls: true +additionalProperties: + customProperty: value + anotherProperty: anotherValue + +""" + let instance = try ModelOptions.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.frequencyPenalty)), 0.5) + XCTAssertEqual((try XCTUnwrap(instance.maxOutputTokens)), 2048) + XCTAssertEqual((try XCTUnwrap(instance.presencePenalty)), 0.3) + XCTAssertEqual((try XCTUnwrap(instance.seed)), 42) + XCTAssertEqual((try XCTUnwrap(instance.temperature)), 0.7) + XCTAssertEqual((try XCTUnwrap(instance.topK)), 40) + XCTAssertEqual((try XCTUnwrap(instance.topP)), 0.9) + XCTAssertEqual((try XCTUnwrap(instance.allowMultipleToolCalls)), true) + XCTAssertEqual((try XCTUnwrap(instance.stopSequences)).count, 2) + XCTAssertNotNil(instance.additionalProperties) + let reloaded = try ModelOptions.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.frequencyPenalty)), 0.5) + XCTAssertEqual((try XCTUnwrap(reloaded.maxOutputTokens)), 2048) + XCTAssertEqual((try XCTUnwrap(reloaded.presencePenalty)), 0.3) + XCTAssertEqual((try XCTUnwrap(reloaded.seed)), 42) + XCTAssertEqual((try XCTUnwrap(reloaded.temperature)), 0.7) + XCTAssertEqual((try XCTUnwrap(reloaded.topK)), 40) + XCTAssertEqual((try XCTUnwrap(reloaded.topP)), 0.9) + XCTAssertEqual((try XCTUnwrap(reloaded.allowMultipleToolCalls)), true) + XCTAssertEqual((try XCTUnwrap(reloaded.stopSequences)).count, 2) + XCTAssertNotNil(reloaded.additionalProperties) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/ModelTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/ModelTests.swift new file mode 100644 index 000000000..0e9189e8f --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/ModelTests.swift @@ -0,0 +1,95 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ModelTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "id": "gpt-35-turbo", + "provider": "foundry", + "apiType": "chat", + "connection": { + "kind": "key", + "endpoint": "https://{your-custom-endpoint}.openai.azure.com/", + "key": "{your-api-key}" + }, + "options": { + "type": "chat", + "temperature": 0.7, + "maxOutputTokens": 1000 + } +} +""" + let instance = try Model.fromJSON(json) + XCTAssertEqual(instance.id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(instance.provider)), "foundry") + XCTAssertEqual((try XCTUnwrap(instance.apiType)), ApiType(rawValue: "chat")) + if case .apiKeyConnection(let concrete) = (try XCTUnwrap(instance.connection)) { + XCTAssertEqual(concrete.endpoint, "https://{your-custom-endpoint}.openai.azure.com/") + } else { + XCTFail("Expected ApiKeyConnection") + } + XCTAssertEqual((try XCTUnwrap((try XCTUnwrap(instance.options)).temperature)), 0.7) + XCTAssertEqual((try XCTUnwrap((try XCTUnwrap(instance.options)).maxOutputTokens)), 1000) + let reloaded = try Model.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(reloaded.provider)), "foundry") + XCTAssertEqual((try XCTUnwrap(reloaded.apiType)), ApiType(rawValue: "chat")) + if case .apiKeyConnection(let concrete) = (try XCTUnwrap(reloaded.connection)) { + XCTAssertEqual(concrete.endpoint, "https://{your-custom-endpoint}.openai.azure.com/") + } else { + XCTFail("Expected ApiKeyConnection") + } + XCTAssertEqual((try XCTUnwrap((try XCTUnwrap(reloaded.options)).temperature)), 0.7) + XCTAssertEqual((try XCTUnwrap((try XCTUnwrap(reloaded.options)).maxOutputTokens)), 1000) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +id: gpt-35-turbo +provider: foundry +apiType: chat +connection: + kind: key + endpoint: "https://{your-custom-endpoint}.openai.azure.com/" + key: "{your-api-key}" +options: + type: chat + temperature: 0.7 + maxOutputTokens: 1000 + +""" + let instance = try Model.fromYAML(yaml) + XCTAssertEqual(instance.id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(instance.provider)), "foundry") + XCTAssertEqual((try XCTUnwrap(instance.apiType)), ApiType(rawValue: "chat")) + if case .apiKeyConnection(let concrete) = (try XCTUnwrap(instance.connection)) { + XCTAssertEqual(concrete.endpoint, "https://{your-custom-endpoint}.openai.azure.com/") + } else { + XCTFail("Expected ApiKeyConnection") + } + XCTAssertEqual((try XCTUnwrap((try XCTUnwrap(instance.options)).temperature)), 0.7) + XCTAssertEqual((try XCTUnwrap((try XCTUnwrap(instance.options)).maxOutputTokens)), 1000) + let reloaded = try Model.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.id, "gpt-35-turbo") + XCTAssertEqual((try XCTUnwrap(reloaded.provider)), "foundry") + XCTAssertEqual((try XCTUnwrap(reloaded.apiType)), ApiType(rawValue: "chat")) + if case .apiKeyConnection(let concrete) = (try XCTUnwrap(reloaded.connection)) { + XCTAssertEqual(concrete.endpoint, "https://{your-custom-endpoint}.openai.azure.com/") + } else { + XCTFail("Expected ApiKeyConnection") + } + XCTAssertEqual((try XCTUnwrap((try XCTUnwrap(reloaded.options)).temperature)), 0.7) + XCTAssertEqual((try XCTUnwrap((try XCTUnwrap(reloaded.options)).maxOutputTokens)), 1000) + } + + func testScalarCoercion1() throws { + let instance = try Model.load("example") + XCTAssertEqual(instance.id, "example") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/ProjectInfoTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/ProjectInfoTests.swift new file mode 100644 index 000000000..8d4d8dbe4 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/ProjectInfoTests.swift @@ -0,0 +1,44 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ProjectInfoTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "name": "sample", + "displayName": "sample", + "endpoint": "sample" +} +""" + let instance = try ProjectInfo.fromJSON(json) + XCTAssertEqual(instance.name, "sample") + XCTAssertEqual(instance.displayName, "sample") + XCTAssertEqual(instance.endpoint, "sample") + let reloaded = try ProjectInfo.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.name, "sample") + XCTAssertEqual(reloaded.displayName, "sample") + XCTAssertEqual(reloaded.endpoint, "sample") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +name: sample +displayName: sample +endpoint: sample + +""" + let instance = try ProjectInfo.fromYAML(yaml) + XCTAssertEqual(instance.name, "sample") + XCTAssertEqual(instance.displayName, "sample") + XCTAssertEqual(instance.endpoint, "sample") + let reloaded = try ProjectInfo.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.name, "sample") + XCTAssertEqual(reloaded.displayName, "sample") + XCTAssertEqual(reloaded.endpoint, "sample") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/SubscriptionInfoTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/SubscriptionInfoTests.swift new file mode 100644 index 000000000..2eb6ab635 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/SubscriptionInfoTests.swift @@ -0,0 +1,44 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class SubscriptionInfoTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "subscriptionId": "sample", + "displayName": "sample", + "state": "sample" +} +""" + let instance = try SubscriptionInfo.fromJSON(json) + XCTAssertEqual(instance.subscriptionId, "sample") + XCTAssertEqual(instance.displayName, "sample") + XCTAssertEqual(instance.state, "sample") + let reloaded = try SubscriptionInfo.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.subscriptionId, "sample") + XCTAssertEqual(reloaded.displayName, "sample") + XCTAssertEqual(reloaded.state, "sample") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +subscriptionId: sample +displayName: sample +state: sample + +""" + let instance = try SubscriptionInfo.fromYAML(yaml) + XCTAssertEqual(instance.subscriptionId, "sample") + XCTAssertEqual(instance.displayName, "sample") + XCTAssertEqual(instance.state, "sample") + let reloaded = try SubscriptionInfo.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.subscriptionId, "sample") + XCTAssertEqual(reloaded.displayName, "sample") + XCTAssertEqual(reloaded.state, "sample") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/TokenUsageTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/TokenUsageTests.swift new file mode 100644 index 000000000..78ce11d28 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/TokenUsageTests.swift @@ -0,0 +1,44 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class TokenUsageTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "promptTokens": 150, + "completionTokens": 42, + "totalTokens": 192 +} +""" + let instance = try TokenUsage.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.promptTokens)), 150) + XCTAssertEqual((try XCTUnwrap(instance.completionTokens)), 42) + XCTAssertEqual((try XCTUnwrap(instance.totalTokens)), 192) + let reloaded = try TokenUsage.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.promptTokens)), 150) + XCTAssertEqual((try XCTUnwrap(reloaded.completionTokens)), 42) + XCTAssertEqual((try XCTUnwrap(reloaded.totalTokens)), 192) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +promptTokens: 150 +completionTokens: 42 +totalTokens: 192 + +""" + let instance = try TokenUsage.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.promptTokens)), 150) + XCTAssertEqual((try XCTUnwrap(instance.completionTokens)), 42) + XCTAssertEqual((try XCTUnwrap(instance.totalTokens)), 192) + let reloaded = try TokenUsage.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.promptTokens)), 150) + XCTAssertEqual((try XCTUnwrap(reloaded.completionTokens)), 42) + XCTAssertEqual((try XCTUnwrap(reloaded.totalTokens)), 192) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/CompactionConfigTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/CompactionConfigTests.swift new file mode 100644 index 000000000..1617c6b92 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/CompactionConfigTests.swift @@ -0,0 +1,47 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class CompactionConfigTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "strategy": "summarize", + "budget": 50000, + "options": { + "preserveSystemMessages": true + } +} +""" + let instance = try CompactionConfig.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.strategy)), "summarize") + XCTAssertEqual((try XCTUnwrap(instance.budget)), 50000) + XCTAssertNotNil(instance.options) + let reloaded = try CompactionConfig.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.strategy)), "summarize") + XCTAssertEqual((try XCTUnwrap(reloaded.budget)), 50000) + XCTAssertNotNil(reloaded.options) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +strategy: summarize +budget: 50000 +options: + preserveSystemMessages: true + +""" + let instance = try CompactionConfig.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.strategy)), "summarize") + XCTAssertEqual((try XCTUnwrap(instance.budget)), 50000) + XCTAssertNotNil(instance.options) + let reloaded = try CompactionConfig.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.strategy)), "summarize") + XCTAssertEqual((try XCTUnwrap(reloaded.budget)), 50000) + XCTAssertNotNil(reloaded.options) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ContextCandidateTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ContextCandidateTests.swift new file mode 100644 index 000000000..b07c872ad --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ContextCandidateTests.swift @@ -0,0 +1,63 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ContextCandidateTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "id": "memory:project-plan", + "source": "memory", + "messages": [ + { + "role": "user", + "parts": [ + { + "kind": "text", + "value": "Hello!" + } + ], + "metadata": { + "source": "user-input" + } + } + ] +} +""" + let instance = try ContextCandidate.fromJSON(json) + XCTAssertEqual(instance.id, "memory:project-plan") + XCTAssertEqual(instance.source, "memory") + XCTAssertEqual(instance.messages.count, 1) + let reloaded = try ContextCandidate.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.id, "memory:project-plan") + XCTAssertEqual(reloaded.source, "memory") + XCTAssertEqual(reloaded.messages.count, 1) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +id: "memory:project-plan" +source: memory +messages: + - role: user + parts: + - kind: text + value: Hello! + metadata: + source: user-input + +""" + let instance = try ContextCandidate.fromYAML(yaml) + XCTAssertEqual(instance.id, "memory:project-plan") + XCTAssertEqual(instance.source, "memory") + XCTAssertEqual(instance.messages.count, 1) + let reloaded = try ContextCandidate.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.id, "memory:project-plan") + XCTAssertEqual(reloaded.source, "memory") + XCTAssertEqual(reloaded.messages.count, 1) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ContextRequestTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ContextRequestTests.swift new file mode 100644 index 000000000..da2efe8b0 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ContextRequestTests.swift @@ -0,0 +1,58 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ContextRequestTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "sessionId": "sample", + "turnId": "sample", + "invocationId": "sample", + "iteration": 1, + "messages": [], + "contextState": {} +} +""" + let instance = try ContextRequest.fromJSON(json) + XCTAssertEqual(instance.sessionId, "sample") + XCTAssertEqual(instance.turnId, "sample") + XCTAssertEqual(instance.invocationId, "sample") + XCTAssertEqual(instance.iteration, 1) + XCTAssertEqual(instance.messages.count, 0) + let reloaded = try ContextRequest.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.sessionId, "sample") + XCTAssertEqual(reloaded.turnId, "sample") + XCTAssertEqual(reloaded.invocationId, "sample") + XCTAssertEqual(reloaded.iteration, 1) + XCTAssertEqual(reloaded.messages.count, 0) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +sessionId: sample +turnId: sample +invocationId: sample +iteration: 1 +messages: [] +contextState: {} + +""" + let instance = try ContextRequest.fromYAML(yaml) + XCTAssertEqual(instance.sessionId, "sample") + XCTAssertEqual(instance.turnId, "sample") + XCTAssertEqual(instance.invocationId, "sample") + XCTAssertEqual(instance.iteration, 1) + XCTAssertEqual(instance.messages.count, 0) + let reloaded = try ContextRequest.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.sessionId, "sample") + XCTAssertEqual(reloaded.turnId, "sample") + XCTAssertEqual(reloaded.invocationId, "sample") + XCTAssertEqual(reloaded.iteration, 1) + XCTAssertEqual(reloaded.messages.count, 0) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/DelegatedStateReferenceTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/DelegatedStateReferenceTests.swift new file mode 100644 index 000000000..4d6d78022 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/DelegatedStateReferenceTests.swift @@ -0,0 +1,44 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class DelegatedStateReferenceTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "provider": "openai", + "kind": "response", + "id": "resp_abc123" +} +""" + let instance = try DelegatedStateReference.fromJSON(json) + XCTAssertEqual(instance.provider, "openai") + XCTAssertEqual(instance.kind, "response") + XCTAssertEqual(instance.id, "resp_abc123") + let reloaded = try DelegatedStateReference.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.provider, "openai") + XCTAssertEqual(reloaded.kind, "response") + XCTAssertEqual(reloaded.id, "resp_abc123") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +provider: openai +kind: response +id: resp_abc123 + +""" + let instance = try DelegatedStateReference.fromYAML(yaml) + XCTAssertEqual(instance.provider, "openai") + XCTAssertEqual(instance.kind, "response") + XCTAssertEqual(instance.id, "resp_abc123") + let reloaded = try DelegatedStateReference.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.provider, "openai") + XCTAssertEqual(reloaded.kind, "response") + XCTAssertEqual(reloaded.id, "resp_abc123") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/EngineCheckpointTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/EngineCheckpointTests.swift new file mode 100644 index 000000000..bf45f9759 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/EngineCheckpointTests.swift @@ -0,0 +1,77 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class EngineCheckpointTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "id": "ckpt_abc123", + "sessionId": "sess_abc123", + "turnId": "turn_abc123", + "runId": "run_abc123", + "messages": [ + { + "role": "user", + "parts": [ + { + "kind": "text", + "value": "Hello!" + } + ], + "metadata": { + "source": "user-input" + } + } + ], + "contextState": {} +} +""" + let instance = try EngineCheckpoint.fromJSON(json) + XCTAssertEqual(instance.id, "ckpt_abc123") + XCTAssertEqual(instance.sessionId, "sess_abc123") + XCTAssertEqual(instance.turnId, "turn_abc123") + XCTAssertEqual(instance.runId, "run_abc123") + XCTAssertEqual(instance.messages.count, 1) + let reloaded = try EngineCheckpoint.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.id, "ckpt_abc123") + XCTAssertEqual(reloaded.sessionId, "sess_abc123") + XCTAssertEqual(reloaded.turnId, "turn_abc123") + XCTAssertEqual(reloaded.runId, "run_abc123") + XCTAssertEqual(reloaded.messages.count, 1) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +id: ckpt_abc123 +sessionId: sess_abc123 +turnId: turn_abc123 +runId: run_abc123 +messages: + - role: user + parts: + - kind: text + value: Hello! + metadata: + source: user-input +contextState: {} + +""" + let instance = try EngineCheckpoint.fromYAML(yaml) + XCTAssertEqual(instance.id, "ckpt_abc123") + XCTAssertEqual(instance.sessionId, "sess_abc123") + XCTAssertEqual(instance.turnId, "turn_abc123") + XCTAssertEqual(instance.runId, "run_abc123") + XCTAssertEqual(instance.messages.count, 1) + let reloaded = try EngineCheckpoint.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.id, "ckpt_abc123") + XCTAssertEqual(reloaded.sessionId, "sess_abc123") + XCTAssertEqual(reloaded.turnId, "turn_abc123") + XCTAssertEqual(reloaded.runId, "run_abc123") + XCTAssertEqual(reloaded.messages.count, 1) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/EngineEventTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/EngineEventTests.swift new file mode 100644 index 000000000..7f17caacf --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/EngineEventTests.swift @@ -0,0 +1,56 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class EngineEventTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "id": "evt_abc123", + "timestamp": "2025-01-01T00:00:00Z", + "sessionId": "sess_abc123", + "turnId": "turn_abc123", + "runId": "run_abc123" +} +""" + let instance = try EngineEvent.fromJSON(json) + XCTAssertEqual(instance.id, "evt_abc123") + XCTAssertEqual(instance.timestamp, "2025-01-01T00:00:00Z") + XCTAssertEqual(instance.sessionId, "sess_abc123") + XCTAssertEqual(instance.turnId, "turn_abc123") + XCTAssertEqual(instance.runId, "run_abc123") + let reloaded = try EngineEvent.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.id, "evt_abc123") + XCTAssertEqual(reloaded.timestamp, "2025-01-01T00:00:00Z") + XCTAssertEqual(reloaded.sessionId, "sess_abc123") + XCTAssertEqual(reloaded.turnId, "turn_abc123") + XCTAssertEqual(reloaded.runId, "run_abc123") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +id: evt_abc123 +timestamp: "2025-01-01T00:00:00Z" +sessionId: sess_abc123 +turnId: turn_abc123 +runId: run_abc123 + +""" + let instance = try EngineEvent.fromYAML(yaml) + XCTAssertEqual(instance.id, "evt_abc123") + XCTAssertEqual(instance.timestamp, "2025-01-01T00:00:00Z") + XCTAssertEqual(instance.sessionId, "sess_abc123") + XCTAssertEqual(instance.turnId, "turn_abc123") + XCTAssertEqual(instance.runId, "run_abc123") + let reloaded = try EngineEvent.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.id, "evt_abc123") + XCTAssertEqual(reloaded.timestamp, "2025-01-01T00:00:00Z") + XCTAssertEqual(reloaded.sessionId, "sess_abc123") + XCTAssertEqual(reloaded.turnId, "turn_abc123") + XCTAssertEqual(reloaded.runId, "run_abc123") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/EnginePermissionDecisionTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/EnginePermissionDecisionTests.swift new file mode 100644 index 000000000..510d0aa0e --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/EnginePermissionDecisionTests.swift @@ -0,0 +1,32 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class EnginePermissionDecisionTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "approved": true +} +""" + let instance = try EnginePermissionDecision.fromJSON(json) + XCTAssertEqual(instance.approved, true) + let reloaded = try EnginePermissionDecision.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.approved, true) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +approved: true + +""" + let instance = try EnginePermissionDecision.fromYAML(yaml) + XCTAssertEqual(instance.approved, true) + let reloaded = try EnginePermissionDecision.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.approved, true) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/FinalOutputPolicyRequestTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/FinalOutputPolicyRequestTests.swift new file mode 100644 index 000000000..9ec7d27c0 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/FinalOutputPolicyRequestTests.swift @@ -0,0 +1,50 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class FinalOutputPolicyRequestTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "sessionId": "sample", + "turnId": "sample", + "iteration": 1, + "messages": [] +} +""" + let instance = try FinalOutputPolicyRequest.fromJSON(json) + XCTAssertEqual(instance.sessionId, "sample") + XCTAssertEqual(instance.turnId, "sample") + XCTAssertEqual(instance.iteration, 1) + XCTAssertEqual(instance.messages.count, 0) + let reloaded = try FinalOutputPolicyRequest.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.sessionId, "sample") + XCTAssertEqual(reloaded.turnId, "sample") + XCTAssertEqual(reloaded.iteration, 1) + XCTAssertEqual(reloaded.messages.count, 0) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +sessionId: sample +turnId: sample +iteration: 1 +messages: [] + +""" + let instance = try FinalOutputPolicyRequest.fromYAML(yaml) + XCTAssertEqual(instance.sessionId, "sample") + XCTAssertEqual(instance.turnId, "sample") + XCTAssertEqual(instance.iteration, 1) + XCTAssertEqual(instance.messages.count, 0) + let reloaded = try FinalOutputPolicyRequest.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.sessionId, "sample") + XCTAssertEqual(reloaded.turnId, "sample") + XCTAssertEqual(reloaded.iteration, 1) + XCTAssertEqual(reloaded.messages.count, 0) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/FinalOutputPolicyResultTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/FinalOutputPolicyResultTests.swift new file mode 100644 index 000000000..9d78b75d4 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/FinalOutputPolicyResultTests.swift @@ -0,0 +1,28 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class FinalOutputPolicyResultTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{} +""" + let instance = try FinalOutputPolicyResult.fromJSON(json) + let reloaded = try FinalOutputPolicyResult.fromJSON(try instance.toJSON()) + _ = reloaded + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +{} + +""" + let instance = try FinalOutputPolicyResult.fromYAML(yaml) + let reloaded = try FinalOutputPolicyResult.fromYAML(try instance.toYAML()) + _ = reloaded + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/HostPolicyRequestTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/HostPolicyRequestTests.swift new file mode 100644 index 000000000..b4ecc8291 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/HostPolicyRequestTests.swift @@ -0,0 +1,50 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class HostPolicyRequestTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "sessionId": "sample", + "turnId": "sample", + "iteration": 1, + "messages": [] +} +""" + let instance = try HostPolicyRequest.fromJSON(json) + XCTAssertEqual(instance.sessionId, "sample") + XCTAssertEqual(instance.turnId, "sample") + XCTAssertEqual(instance.iteration, 1) + XCTAssertEqual(instance.messages.count, 0) + let reloaded = try HostPolicyRequest.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.sessionId, "sample") + XCTAssertEqual(reloaded.turnId, "sample") + XCTAssertEqual(reloaded.iteration, 1) + XCTAssertEqual(reloaded.messages.count, 0) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +sessionId: sample +turnId: sample +iteration: 1 +messages: [] + +""" + let instance = try HostPolicyRequest.fromYAML(yaml) + XCTAssertEqual(instance.sessionId, "sample") + XCTAssertEqual(instance.turnId, "sample") + XCTAssertEqual(instance.iteration, 1) + XCTAssertEqual(instance.messages.count, 0) + let reloaded = try HostPolicyRequest.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.sessionId, "sample") + XCTAssertEqual(reloaded.turnId, "sample") + XCTAssertEqual(reloaded.iteration, 1) + XCTAssertEqual(reloaded.messages.count, 0) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/HostPolicyResultTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/HostPolicyResultTests.swift new file mode 100644 index 000000000..740f722e5 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/HostPolicyResultTests.swift @@ -0,0 +1,32 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class HostPolicyResultTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "messages": [] +} +""" + let instance = try HostPolicyResult.fromJSON(json) + XCTAssertEqual(instance.messages.count, 0) + let reloaded = try HostPolicyResult.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.messages.count, 0) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +messages: [] + +""" + let instance = try HostPolicyResult.fromYAML(yaml) + XCTAssertEqual(instance.messages.count, 0) + let reloaded = try HostPolicyResult.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.messages.count, 0) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/InvocationContextDecisionTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/InvocationContextDecisionTests.swift new file mode 100644 index 000000000..ca9fa4c87 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/InvocationContextDecisionTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class InvocationContextDecisionTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "candidateId": "memory:project-plan", + "reason": "included by relevance ranking" +} +""" + let instance = try InvocationContextDecision.fromJSON(json) + XCTAssertEqual(instance.candidateId, "memory:project-plan") + XCTAssertEqual(instance.reason, "included by relevance ranking") + let reloaded = try InvocationContextDecision.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.candidateId, "memory:project-plan") + XCTAssertEqual(reloaded.reason, "included by relevance ranking") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +candidateId: "memory:project-plan" +reason: included by relevance ranking + +""" + let instance = try InvocationContextDecision.fromYAML(yaml) + XCTAssertEqual(instance.candidateId, "memory:project-plan") + XCTAssertEqual(instance.reason, "included by relevance ranking") + let reloaded = try InvocationContextDecision.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.candidateId, "memory:project-plan") + XCTAssertEqual(reloaded.reason, "included by relevance ranking") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/InvocationContextStateTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/InvocationContextStateTests.swift new file mode 100644 index 000000000..496d69d0f --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/InvocationContextStateTests.swift @@ -0,0 +1,28 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class InvocationContextStateTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{} +""" + let instance = try InvocationContextState.fromJSON(json) + let reloaded = try InvocationContextState.fromJSON(try instance.toJSON()) + _ = reloaded + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +{} + +""" + let instance = try InvocationContextState.fromYAML(yaml) + let reloaded = try InvocationContextState.fromYAML(try instance.toYAML()) + _ = reloaded + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelInvocationContextSnapshotTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelInvocationContextSnapshotTests.swift new file mode 100644 index 000000000..23f086eff --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelInvocationContextSnapshotTests.swift @@ -0,0 +1,77 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ModelInvocationContextSnapshotTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "id": "context:inv_abc123", + "sessionId": "sess_abc123", + "turnId": "turn_abc123", + "invocationId": "inv_abc123", + "messages": [ + { + "role": "user", + "parts": [ + { + "kind": "text", + "value": "Hello!" + } + ], + "metadata": { + "source": "user-input" + } + } + ], + "contextState": {} +} +""" + let instance = try ModelInvocationContextSnapshot.fromJSON(json) + XCTAssertEqual(instance.id, "context:inv_abc123") + XCTAssertEqual(instance.sessionId, "sess_abc123") + XCTAssertEqual(instance.turnId, "turn_abc123") + XCTAssertEqual(instance.invocationId, "inv_abc123") + XCTAssertEqual(instance.messages.count, 1) + let reloaded = try ModelInvocationContextSnapshot.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.id, "context:inv_abc123") + XCTAssertEqual(reloaded.sessionId, "sess_abc123") + XCTAssertEqual(reloaded.turnId, "turn_abc123") + XCTAssertEqual(reloaded.invocationId, "inv_abc123") + XCTAssertEqual(reloaded.messages.count, 1) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +id: "context:inv_abc123" +sessionId: sess_abc123 +turnId: turn_abc123 +invocationId: inv_abc123 +messages: + - role: user + parts: + - kind: text + value: Hello! + metadata: + source: user-input +contextState: {} + +""" + let instance = try ModelInvocationContextSnapshot.fromYAML(yaml) + XCTAssertEqual(instance.id, "context:inv_abc123") + XCTAssertEqual(instance.sessionId, "sess_abc123") + XCTAssertEqual(instance.turnId, "turn_abc123") + XCTAssertEqual(instance.invocationId, "inv_abc123") + XCTAssertEqual(instance.messages.count, 1) + let reloaded = try ModelInvocationContextSnapshot.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.id, "context:inv_abc123") + XCTAssertEqual(reloaded.sessionId, "sess_abc123") + XCTAssertEqual(reloaded.turnId, "turn_abc123") + XCTAssertEqual(reloaded.invocationId, "inv_abc123") + XCTAssertEqual(reloaded.messages.count, 1) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelInvocationRequestTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelInvocationRequestTests.swift new file mode 100644 index 000000000..754c80c3c --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelInvocationRequestTests.swift @@ -0,0 +1,63 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ModelInvocationRequestTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "context": { + "id": "context:inv_abc123", + "sessionId": "sess_abc123", + "turnId": "turn_abc123", + "invocationId": "inv_abc123", + "iteration": 1, + "messages": [], + "contextState": {} + } +} +""" + let instance = try ModelInvocationRequest.fromJSON(json) + XCTAssertEqual(instance.context.id, "context:inv_abc123") + XCTAssertEqual(instance.context.sessionId, "sess_abc123") + XCTAssertEqual(instance.context.turnId, "turn_abc123") + XCTAssertEqual(instance.context.invocationId, "inv_abc123") + XCTAssertEqual(instance.context.iteration, 1) + let reloaded = try ModelInvocationRequest.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.context.id, "context:inv_abc123") + XCTAssertEqual(reloaded.context.sessionId, "sess_abc123") + XCTAssertEqual(reloaded.context.turnId, "turn_abc123") + XCTAssertEqual(reloaded.context.invocationId, "inv_abc123") + XCTAssertEqual(reloaded.context.iteration, 1) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +context: + id: "context:inv_abc123" + sessionId: sess_abc123 + turnId: turn_abc123 + invocationId: inv_abc123 + iteration: 1 + messages: [] + contextState: {} + +""" + let instance = try ModelInvocationRequest.fromYAML(yaml) + XCTAssertEqual(instance.context.id, "context:inv_abc123") + XCTAssertEqual(instance.context.sessionId, "sess_abc123") + XCTAssertEqual(instance.context.turnId, "turn_abc123") + XCTAssertEqual(instance.context.invocationId, "inv_abc123") + XCTAssertEqual(instance.context.iteration, 1) + let reloaded = try ModelInvocationRequest.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.context.id, "context:inv_abc123") + XCTAssertEqual(reloaded.context.sessionId, "sess_abc123") + XCTAssertEqual(reloaded.context.turnId, "turn_abc123") + XCTAssertEqual(reloaded.context.invocationId, "inv_abc123") + XCTAssertEqual(reloaded.context.iteration, 1) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelInvocationResponseTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelInvocationResponseTests.swift new file mode 100644 index 000000000..e861e6606 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelInvocationResponseTests.swift @@ -0,0 +1,28 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ModelInvocationResponseTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{} +""" + let instance = try ModelInvocationResponse.fromJSON(json) + let reloaded = try ModelInvocationResponse.fromJSON(try instance.toJSON()) + _ = reloaded + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +{} + +""" + let instance = try ModelInvocationResponse.fromYAML(yaml) + let reloaded = try ModelInvocationResponse.fromYAML(try instance.toYAML()) + _ = reloaded + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelReconciliationStateTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelReconciliationStateTests.swift new file mode 100644 index 000000000..9d032e8ce --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelReconciliationStateTests.swift @@ -0,0 +1,58 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ModelReconciliationStateTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "invocationId": "inv_abc123", + "message": "provider connection dropped after request was sent", + "request": { + "context": { + "id": "context:inv_abc123", + "sessionId": "sess_abc123", + "turnId": "turn_abc123", + "invocationId": "inv_abc123", + "iteration": 1, + "messages": [], + "contextState": {} + } + } +} +""" + let instance = try ModelReconciliationState.fromJSON(json) + XCTAssertEqual(instance.invocationId, "inv_abc123") + XCTAssertEqual(instance.message, "provider connection dropped after request was sent") + let reloaded = try ModelReconciliationState.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.invocationId, "inv_abc123") + XCTAssertEqual(reloaded.message, "provider connection dropped after request was sent") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +invocationId: inv_abc123 +message: provider connection dropped after request was sent +request: + context: + id: "context:inv_abc123" + sessionId: sess_abc123 + turnId: turn_abc123 + invocationId: inv_abc123 + iteration: 1 + messages: [] + contextState: {} + +""" + let instance = try ModelReconciliationState.fromYAML(yaml) + XCTAssertEqual(instance.invocationId, "inv_abc123") + XCTAssertEqual(instance.message, "provider connection dropped after request was sent") + let reloaded = try ModelReconciliationState.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.invocationId, "inv_abc123") + XCTAssertEqual(reloaded.message, "provider connection dropped after request was sent") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelToolRequestTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelToolRequestTests.swift new file mode 100644 index 000000000..d22276ff8 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelToolRequestTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ModelToolRequestTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "id": "call_abc123", + "name": "get_weather" +} +""" + let instance = try ModelToolRequest.fromJSON(json) + XCTAssertEqual(instance.id, "call_abc123") + XCTAssertEqual(instance.name, "get_weather") + let reloaded = try ModelToolRequest.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.id, "call_abc123") + XCTAssertEqual(reloaded.name, "get_weather") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +id: call_abc123 +name: get_weather + +""" + let instance = try ModelToolRequest.fromYAML(yaml) + XCTAssertEqual(instance.id, "call_abc123") + XCTAssertEqual(instance.name, "get_weather") + let reloaded = try ModelToolRequest.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.id, "call_abc123") + XCTAssertEqual(reloaded.name, "get_weather") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelToolResultTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelToolResultTests.swift new file mode 100644 index 000000000..1e7f4aa53 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelToolResultTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ModelToolResultTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "requestId": "call_abc123", + "name": "get_weather" +} +""" + let instance = try ModelToolResult.fromJSON(json) + XCTAssertEqual(instance.requestId, "call_abc123") + XCTAssertEqual(instance.name, "get_weather") + let reloaded = try ModelToolResult.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.requestId, "call_abc123") + XCTAssertEqual(reloaded.name, "get_weather") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +requestId: call_abc123 +name: get_weather + +""" + let instance = try ModelToolResult.fromYAML(yaml) + XCTAssertEqual(instance.requestId, "call_abc123") + XCTAssertEqual(instance.name, "get_weather") + let reloaded = try ModelToolResult.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.requestId, "call_abc123") + XCTAssertEqual(reloaded.name, "get_weather") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ReplayJournalRecordTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ReplayJournalRecordTests.swift new file mode 100644 index 000000000..0c9d5cdaa --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ReplayJournalRecordTests.swift @@ -0,0 +1,32 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ReplayJournalRecordTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "kind": "session" +} +""" + let instance = try ReplayJournalRecord.fromJSON(json) + XCTAssertEqual(instance.kind, ReplayRecordKind.session) + let reloaded = try ReplayJournalRecord.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.kind, ReplayRecordKind.session) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +kind: session + +""" + let instance = try ReplayJournalRecord.fromYAML(yaml) + XCTAssertEqual(instance.kind, ReplayRecordKind.session) + let reloaded = try ReplayJournalRecord.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.kind, ReplayRecordKind.session) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ReplayMismatchTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ReplayMismatchTests.swift new file mode 100644 index 000000000..05e1f7416 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ReplayMismatchTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ReplayMismatchTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "index": 1, + "message": "sample" +} +""" + let instance = try ReplayMismatch.fromJSON(json) + XCTAssertEqual(instance.index, 1) + XCTAssertEqual(instance.message, "sample") + let reloaded = try ReplayMismatch.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.index, 1) + XCTAssertEqual(reloaded.message, "sample") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +index: 1 +message: sample + +""" + let instance = try ReplayMismatch.fromYAML(yaml) + XCTAssertEqual(instance.index, 1) + XCTAssertEqual(instance.message, "sample") + let reloaded = try ReplayMismatch.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.index, 1) + XCTAssertEqual(reloaded.message, "sample") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ReplayVerificationRequestTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ReplayVerificationRequestTests.swift new file mode 100644 index 000000000..9382772cf --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ReplayVerificationRequestTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ReplayVerificationRequestTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "expected": [], + "actual": [] +} +""" + let instance = try ReplayVerificationRequest.fromJSON(json) + XCTAssertEqual(instance.expected.count, 0) + XCTAssertEqual(instance.actual.count, 0) + let reloaded = try ReplayVerificationRequest.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.expected.count, 0) + XCTAssertEqual(reloaded.actual.count, 0) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +expected: [] +actual: [] + +""" + let instance = try ReplayVerificationRequest.fromYAML(yaml) + XCTAssertEqual(instance.expected.count, 0) + XCTAssertEqual(instance.actual.count, 0) + let reloaded = try ReplayVerificationRequest.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.expected.count, 0) + XCTAssertEqual(reloaded.actual.count, 0) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ReplayVerificationResultTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ReplayVerificationResultTests.swift new file mode 100644 index 000000000..f7eb3d768 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ReplayVerificationResultTests.swift @@ -0,0 +1,44 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ReplayVerificationResultTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "status": "passed", + "expectedCount": 1, + "actualCount": 1 +} +""" + let instance = try ReplayVerificationResult.fromJSON(json) + XCTAssertEqual(instance.status, ReplayVerificationStatus.passed) + XCTAssertEqual(instance.expectedCount, 1) + XCTAssertEqual(instance.actualCount, 1) + let reloaded = try ReplayVerificationResult.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.status, ReplayVerificationStatus.passed) + XCTAssertEqual(reloaded.expectedCount, 1) + XCTAssertEqual(reloaded.actualCount, 1) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +status: passed +expectedCount: 1 +actualCount: 1 + +""" + let instance = try ReplayVerificationResult.fromYAML(yaml) + XCTAssertEqual(instance.status, ReplayVerificationStatus.passed) + XCTAssertEqual(instance.expectedCount, 1) + XCTAssertEqual(instance.actualCount, 1) + let reloaded = try ReplayVerificationResult.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.status, ReplayVerificationStatus.passed) + XCTAssertEqual(reloaded.expectedCount, 1) + XCTAssertEqual(reloaded.actualCount, 1) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ResumeContextTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ResumeContextTests.swift new file mode 100644 index 000000000..544a93c6d --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ResumeContextTests.swift @@ -0,0 +1,75 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ResumeContextTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "lastJournalSequence": 12, + "checkpoint": { + "id": "ckpt_abc123", + "sessionId": "sess_abc123", + "turnId": "turn_abc123", + "runId": "run_abc123", + "iteration": 1, + "lastSequence": 1, + "messages": [], + "contextState": {} + } +} +""" + let instance = try ResumeContext.fromJSON(json) + XCTAssertEqual(instance.lastJournalSequence, 12) + XCTAssertEqual(instance.checkpoint.id, "ckpt_abc123") + XCTAssertEqual(instance.checkpoint.sessionId, "sess_abc123") + XCTAssertEqual(instance.checkpoint.turnId, "turn_abc123") + XCTAssertEqual(instance.checkpoint.runId, "run_abc123") + XCTAssertEqual(instance.checkpoint.iteration, 1) + XCTAssertEqual(instance.checkpoint.lastSequence, 1) + let reloaded = try ResumeContext.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.lastJournalSequence, 12) + XCTAssertEqual(reloaded.checkpoint.id, "ckpt_abc123") + XCTAssertEqual(reloaded.checkpoint.sessionId, "sess_abc123") + XCTAssertEqual(reloaded.checkpoint.turnId, "turn_abc123") + XCTAssertEqual(reloaded.checkpoint.runId, "run_abc123") + XCTAssertEqual(reloaded.checkpoint.iteration, 1) + XCTAssertEqual(reloaded.checkpoint.lastSequence, 1) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +lastJournalSequence: 12 +checkpoint: + id: ckpt_abc123 + sessionId: sess_abc123 + turnId: turn_abc123 + runId: run_abc123 + iteration: 1 + lastSequence: 1 + messages: [] + contextState: {} + +""" + let instance = try ResumeContext.fromYAML(yaml) + XCTAssertEqual(instance.lastJournalSequence, 12) + XCTAssertEqual(instance.checkpoint.id, "ckpt_abc123") + XCTAssertEqual(instance.checkpoint.sessionId, "sess_abc123") + XCTAssertEqual(instance.checkpoint.turnId, "turn_abc123") + XCTAssertEqual(instance.checkpoint.runId, "run_abc123") + XCTAssertEqual(instance.checkpoint.iteration, 1) + XCTAssertEqual(instance.checkpoint.lastSequence, 1) + let reloaded = try ResumeContext.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.lastJournalSequence, 12) + XCTAssertEqual(reloaded.checkpoint.id, "ckpt_abc123") + XCTAssertEqual(reloaded.checkpoint.sessionId, "sess_abc123") + XCTAssertEqual(reloaded.checkpoint.turnId, "turn_abc123") + XCTAssertEqual(reloaded.checkpoint.runId, "run_abc123") + XCTAssertEqual(reloaded.checkpoint.iteration, 1) + XCTAssertEqual(reloaded.checkpoint.lastSequence, 1) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/RetryPolicyRequestTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/RetryPolicyRequestTests.swift new file mode 100644 index 000000000..8718a1a0e --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/RetryPolicyRequestTests.swift @@ -0,0 +1,50 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class RetryPolicyRequestTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "failedAttempts": 1, + "nextAttempt": 1, + "maxAttempts": 1, + "reason": "sample" +} +""" + let instance = try RetryPolicyRequest.fromJSON(json) + XCTAssertEqual(instance.failedAttempts, 1) + XCTAssertEqual(instance.nextAttempt, 1) + XCTAssertEqual(instance.maxAttempts, 1) + XCTAssertEqual(instance.reason, "sample") + let reloaded = try RetryPolicyRequest.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.failedAttempts, 1) + XCTAssertEqual(reloaded.nextAttempt, 1) + XCTAssertEqual(reloaded.maxAttempts, 1) + XCTAssertEqual(reloaded.reason, "sample") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +failedAttempts: 1 +nextAttempt: 1 +maxAttempts: 1 +reason: sample + +""" + let instance = try RetryPolicyRequest.fromYAML(yaml) + XCTAssertEqual(instance.failedAttempts, 1) + XCTAssertEqual(instance.nextAttempt, 1) + XCTAssertEqual(instance.maxAttempts, 1) + XCTAssertEqual(instance.reason, "sample") + let reloaded = try RetryPolicyRequest.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.failedAttempts, 1) + XCTAssertEqual(reloaded.nextAttempt, 1) + XCTAssertEqual(reloaded.maxAttempts, 1) + XCTAssertEqual(reloaded.reason, "sample") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/RunTurnRequestTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/RunTurnRequestTests.swift new file mode 100644 index 000000000..61e276e47 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/RunTurnRequestTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class RunTurnRequestTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "sessionId": "sess_abc123", + "turnId": "turn_abc123" +} +""" + let instance = try RunTurnRequest.fromJSON(json) + XCTAssertEqual(instance.sessionId, "sess_abc123") + XCTAssertEqual(instance.turnId, "turn_abc123") + let reloaded = try RunTurnRequest.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.sessionId, "sess_abc123") + XCTAssertEqual(reloaded.turnId, "turn_abc123") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +sessionId: sess_abc123 +turnId: turn_abc123 + +""" + let instance = try RunTurnRequest.fromYAML(yaml) + XCTAssertEqual(instance.sessionId, "sess_abc123") + XCTAssertEqual(instance.turnId, "turn_abc123") + let reloaded = try RunTurnRequest.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.sessionId, "sess_abc123") + XCTAssertEqual(reloaded.turnId, "turn_abc123") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/RunTurnResultTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/RunTurnResultTests.swift new file mode 100644 index 000000000..1a891c7dd --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/RunTurnResultTests.swift @@ -0,0 +1,44 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class RunTurnResultTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "sessionId": "sess_abc123", + "turnId": "turn_abc123", + "iterations": 1 +} +""" + let instance = try RunTurnResult.fromJSON(json) + XCTAssertEqual(instance.sessionId, "sess_abc123") + XCTAssertEqual(instance.turnId, "turn_abc123") + XCTAssertEqual(instance.iterations, 1) + let reloaded = try RunTurnResult.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.sessionId, "sess_abc123") + XCTAssertEqual(reloaded.turnId, "turn_abc123") + XCTAssertEqual(reloaded.iterations, 1) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +sessionId: sess_abc123 +turnId: turn_abc123 +iterations: 1 + +""" + let instance = try RunTurnResult.fromYAML(yaml) + XCTAssertEqual(instance.sessionId, "sess_abc123") + XCTAssertEqual(instance.turnId, "turn_abc123") + XCTAssertEqual(instance.iterations, 1) + let reloaded = try RunTurnResult.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.sessionId, "sess_abc123") + XCTAssertEqual(reloaded.turnId, "turn_abc123") + XCTAssertEqual(reloaded.iterations, 1) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/TurnCommitTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/TurnCommitTests.swift new file mode 100644 index 000000000..92ca8ee5f --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/TurnCommitTests.swift @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class TurnCommitTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "sessionId": "sess_abc123", + "turnId": "turn_abc123", + "messages": [ + { + "role": "user", + "parts": [ + { + "kind": "text", + "value": "Hello!" + } + ], + "metadata": { + "source": "user-input" + } + } + ], + "contextState": {} +} +""" + let instance = try TurnCommit.fromJSON(json) + XCTAssertEqual(instance.sessionId, "sess_abc123") + XCTAssertEqual(instance.turnId, "turn_abc123") + XCTAssertEqual(instance.messages.count, 1) + let reloaded = try TurnCommit.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.sessionId, "sess_abc123") + XCTAssertEqual(reloaded.turnId, "turn_abc123") + XCTAssertEqual(reloaded.messages.count, 1) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +sessionId: sess_abc123 +turnId: turn_abc123 +messages: + - role: user + parts: + - kind: text + value: Hello! + metadata: + source: user-input +contextState: {} + +""" + let instance = try TurnCommit.fromYAML(yaml) + XCTAssertEqual(instance.sessionId, "sess_abc123") + XCTAssertEqual(instance.turnId, "turn_abc123") + XCTAssertEqual(instance.messages.count, 1) + let reloaded = try TurnCommit.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.sessionId, "sess_abc123") + XCTAssertEqual(reloaded.turnId, "turn_abc123") + XCTAssertEqual(reloaded.messages.count, 1) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/TurnEngineResultTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/TurnEngineResultTests.swift new file mode 100644 index 000000000..094c1f59f --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/TurnEngineResultTests.swift @@ -0,0 +1,63 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class TurnEngineResultTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "commit": { + "sessionId": "sess_abc123", + "turnId": "turn_abc123", + "status": "success", + "messages": [], + "iterations": 1, + "lastSequence": 1, + "contextState": {} + } +} +""" + let instance = try TurnEngineResult.fromJSON(json) + XCTAssertEqual(instance.commit.sessionId, "sess_abc123") + XCTAssertEqual(instance.commit.turnId, "turn_abc123") + XCTAssertEqual(instance.commit.status.rawValue, "success") + XCTAssertEqual(instance.commit.iterations, 1) + XCTAssertEqual(instance.commit.lastSequence, 1) + let reloaded = try TurnEngineResult.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.commit.sessionId, "sess_abc123") + XCTAssertEqual(reloaded.commit.turnId, "turn_abc123") + XCTAssertEqual(reloaded.commit.status.rawValue, "success") + XCTAssertEqual(reloaded.commit.iterations, 1) + XCTAssertEqual(reloaded.commit.lastSequence, 1) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +commit: + sessionId: sess_abc123 + turnId: turn_abc123 + status: success + messages: [] + iterations: 1 + lastSequence: 1 + contextState: {} + +""" + let instance = try TurnEngineResult.fromYAML(yaml) + XCTAssertEqual(instance.commit.sessionId, "sess_abc123") + XCTAssertEqual(instance.commit.turnId, "turn_abc123") + XCTAssertEqual(instance.commit.status.rawValue, "success") + XCTAssertEqual(instance.commit.iterations, 1) + XCTAssertEqual(instance.commit.lastSequence, 1) + let reloaded = try TurnEngineResult.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.commit.sessionId, "sess_abc123") + XCTAssertEqual(reloaded.commit.turnId, "turn_abc123") + XCTAssertEqual(reloaded.commit.status.rawValue, "success") + XCTAssertEqual(reloaded.commit.iterations, 1) + XCTAssertEqual(reloaded.commit.lastSequence, 1) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/TurnModelRequestTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/TurnModelRequestTests.swift new file mode 100644 index 000000000..5c66b6a35 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/TurnModelRequestTests.swift @@ -0,0 +1,44 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class TurnModelRequestTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "sessionId": "sess_abc123", + "turnId": "turn_abc123", + "iteration": 0 +} +""" + let instance = try TurnModelRequest.fromJSON(json) + XCTAssertEqual(instance.sessionId, "sess_abc123") + XCTAssertEqual(instance.turnId, "turn_abc123") + XCTAssertEqual(instance.iteration, 0) + let reloaded = try TurnModelRequest.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.sessionId, "sess_abc123") + XCTAssertEqual(reloaded.turnId, "turn_abc123") + XCTAssertEqual(reloaded.iteration, 0) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +sessionId: sess_abc123 +turnId: turn_abc123 +iteration: 0 + +""" + let instance = try TurnModelRequest.fromYAML(yaml) + XCTAssertEqual(instance.sessionId, "sess_abc123") + XCTAssertEqual(instance.turnId, "turn_abc123") + XCTAssertEqual(instance.iteration, 0) + let reloaded = try TurnModelRequest.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.sessionId, "sess_abc123") + XCTAssertEqual(reloaded.turnId, "turn_abc123") + XCTAssertEqual(reloaded.iteration, 0) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/TurnModelResponseTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/TurnModelResponseTests.swift new file mode 100644 index 000000000..2592942d3 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/TurnModelResponseTests.swift @@ -0,0 +1,28 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class TurnModelResponseTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{} +""" + let instance = try TurnModelResponse.fromJSON(json) + let reloaded = try TurnModelResponse.fromJSON(try instance.toJSON()) + _ = reloaded + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +{} + +""" + let instance = try TurnModelResponse.fromYAML(yaml) + let reloaded = try TurnModelResponse.fromYAML(try instance.toYAML()) + _ = reloaded + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/TurnOptionsTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/TurnOptionsTests.swift new file mode 100644 index 000000000..ff75ad7c8 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/TurnOptionsTests.swift @@ -0,0 +1,71 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class TurnOptionsTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "maxIterations": 10, + "maxLlmRetries": 3, + "contextBudget": 100000, + "parallelToolCalls": true, + "raw": false, + "turn": 1, + "compaction": { + "strategy": "summarize" + } +} +""" + let instance = try TurnOptions.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.maxIterations)), 10) + XCTAssertEqual((try XCTUnwrap(instance.maxLlmRetries)), 3) + XCTAssertEqual((try XCTUnwrap(instance.contextBudget)), 100000) + XCTAssertEqual((try XCTUnwrap(instance.parallelToolCalls)), true) + XCTAssertEqual((try XCTUnwrap(instance.raw)), false) + XCTAssertEqual((try XCTUnwrap(instance.turn)), 1) + XCTAssertEqual((try XCTUnwrap((try XCTUnwrap(instance.compaction)).strategy)), "summarize") + let reloaded = try TurnOptions.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.maxIterations)), 10) + XCTAssertEqual((try XCTUnwrap(reloaded.maxLlmRetries)), 3) + XCTAssertEqual((try XCTUnwrap(reloaded.contextBudget)), 100000) + XCTAssertEqual((try XCTUnwrap(reloaded.parallelToolCalls)), true) + XCTAssertEqual((try XCTUnwrap(reloaded.raw)), false) + XCTAssertEqual((try XCTUnwrap(reloaded.turn)), 1) + XCTAssertEqual((try XCTUnwrap((try XCTUnwrap(reloaded.compaction)).strategy)), "summarize") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +maxIterations: 10 +maxLlmRetries: 3 +contextBudget: 100000 +parallelToolCalls: true +raw: false +turn: 1 +compaction: + strategy: summarize + +""" + let instance = try TurnOptions.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.maxIterations)), 10) + XCTAssertEqual((try XCTUnwrap(instance.maxLlmRetries)), 3) + XCTAssertEqual((try XCTUnwrap(instance.contextBudget)), 100000) + XCTAssertEqual((try XCTUnwrap(instance.parallelToolCalls)), true) + XCTAssertEqual((try XCTUnwrap(instance.raw)), false) + XCTAssertEqual((try XCTUnwrap(instance.turn)), 1) + XCTAssertEqual((try XCTUnwrap((try XCTUnwrap(instance.compaction)).strategy)), "summarize") + let reloaded = try TurnOptions.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.maxIterations)), 10) + XCTAssertEqual((try XCTUnwrap(reloaded.maxLlmRetries)), 3) + XCTAssertEqual((try XCTUnwrap(reloaded.contextBudget)), 100000) + XCTAssertEqual((try XCTUnwrap(reloaded.parallelToolCalls)), true) + XCTAssertEqual((try XCTUnwrap(reloaded.raw)), false) + XCTAssertEqual((try XCTUnwrap(reloaded.turn)), 1) + XCTAssertEqual((try XCTUnwrap((try XCTUnwrap(reloaded.compaction)).strategy)), "summarize") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Streaming/StreamOptionsTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Streaming/StreamOptionsTests.swift new file mode 100644 index 000000000..ba5aa1aa1 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Streaming/StreamOptionsTests.swift @@ -0,0 +1,32 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class StreamOptionsTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "includeUsage": true +} +""" + let instance = try StreamOptions.fromJSON(json) + XCTAssertEqual((try XCTUnwrap(instance.includeUsage)), true) + let reloaded = try StreamOptions.fromJSON(try instance.toJSON()) + XCTAssertEqual((try XCTUnwrap(reloaded.includeUsage)), true) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +includeUsage: true + +""" + let instance = try StreamOptions.fromYAML(yaml) + XCTAssertEqual((try XCTUnwrap(instance.includeUsage)), true) + let reloaded = try StreamOptions.fromYAML(try instance.toYAML()) + XCTAssertEqual((try XCTUnwrap(reloaded.includeUsage)), true) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Templates/FormatConfigTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Templates/FormatConfigTests.swift new file mode 100644 index 000000000..77a103d8c --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Templates/FormatConfigTests.swift @@ -0,0 +1,52 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class FormatConfigTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "kind": "mustache", + "strict": true, + "options": { + "key": "value" + } +} +""" + let instance = try FormatConfig.fromJSON(json) + XCTAssertEqual(instance.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(instance.strict)), true) + XCTAssertNotNil(instance.options) + let reloaded = try FormatConfig.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(reloaded.strict)), true) + XCTAssertNotNil(reloaded.options) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +kind: mustache +strict: true +options: + key: value + +""" + let instance = try FormatConfig.fromYAML(yaml) + XCTAssertEqual(instance.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(instance.strict)), true) + XCTAssertNotNil(instance.options) + let reloaded = try FormatConfig.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.kind, "mustache") + XCTAssertEqual((try XCTUnwrap(reloaded.strict)), true) + XCTAssertNotNil(reloaded.options) + } + + func testScalarCoercion1() throws { + let instance = try FormatConfig.load("example") + XCTAssertEqual(instance.kind, "example") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Templates/ParserConfigTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Templates/ParserConfigTests.swift new file mode 100644 index 000000000..87899183f --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Templates/ParserConfigTests.swift @@ -0,0 +1,46 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ParserConfigTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "kind": "prompty", + "options": { + "key": "value" + } +} +""" + let instance = try ParserConfig.fromJSON(json) + XCTAssertEqual(instance.kind, "prompty") + XCTAssertNotNil(instance.options) + let reloaded = try ParserConfig.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.kind, "prompty") + XCTAssertNotNil(reloaded.options) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +kind: prompty +options: + key: value + +""" + let instance = try ParserConfig.fromYAML(yaml) + XCTAssertEqual(instance.kind, "prompty") + XCTAssertNotNil(instance.options) + let reloaded = try ParserConfig.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.kind, "prompty") + XCTAssertNotNil(reloaded.options) + } + + func testScalarCoercion1() throws { + let instance = try ParserConfig.load("example") + XCTAssertEqual(instance.kind, "example") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Templates/TemplateTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Templates/TemplateTests.swift new file mode 100644 index 000000000..c2f4dcb33 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Templates/TemplateTests.swift @@ -0,0 +1,44 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class TemplateTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "format": { + "kind": "mustache" + }, + "parser": { + "kind": "mustache" + } +} +""" + let instance = try Template.fromJSON(json) + XCTAssertEqual(instance.format.kind, "mustache") + XCTAssertEqual(instance.parser.kind, "mustache") + let reloaded = try Template.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.format.kind, "mustache") + XCTAssertEqual(reloaded.parser.kind, "mustache") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +format: + kind: mustache +parser: + kind: mustache + +""" + let instance = try Template.fromYAML(yaml) + XCTAssertEqual(instance.format.kind, "mustache") + XCTAssertEqual(instance.parser.kind, "mustache") + let reloaded = try Template.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.format.kind, "mustache") + XCTAssertEqual(reloaded.parser.kind, "mustache") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tooling/BindingTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tooling/BindingTests.swift new file mode 100644 index 000000000..18ac1b08e --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tooling/BindingTests.swift @@ -0,0 +1,43 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class BindingTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "name": "my-tool", + "input": "input-variable" +} +""" + let instance = try Binding.fromJSON(json) + XCTAssertEqual(instance.name, "my-tool") + XCTAssertEqual(instance.input, "input-variable") + let reloaded = try Binding.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.name, "my-tool") + XCTAssertEqual(reloaded.input, "input-variable") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +name: my-tool +input: input-variable + +""" + let instance = try Binding.fromYAML(yaml) + XCTAssertEqual(instance.name, "my-tool") + XCTAssertEqual(instance.input, "input-variable") + let reloaded = try Binding.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.name, "my-tool") + XCTAssertEqual(reloaded.input, "input-variable") + } + + func testScalarCoercion1() throws { + let instance = try Binding.load("example") + XCTAssertEqual(instance.input, "example") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tooling/McpApprovalModeTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tooling/McpApprovalModeTests.swift new file mode 100644 index 000000000..027598e6c --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tooling/McpApprovalModeTests.swift @@ -0,0 +1,55 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class McpApprovalModeTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "kind": "never", + "alwaysRequireApprovalTools": [ + "operation1" + ], + "neverRequireApprovalTools": [ + "operation2" + ] +} +""" + let instance = try McpApprovalMode.fromJSON(json) + XCTAssertEqual(instance.kind, McpApprovalModeKind.never) + XCTAssertEqual((try XCTUnwrap(instance.alwaysRequireApprovalTools)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.neverRequireApprovalTools)).count, 1) + let reloaded = try McpApprovalMode.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.kind, McpApprovalModeKind.never) + XCTAssertEqual((try XCTUnwrap(reloaded.alwaysRequireApprovalTools)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.neverRequireApprovalTools)).count, 1) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +kind: never +alwaysRequireApprovalTools: + - operation1 +neverRequireApprovalTools: + - operation2 + +""" + let instance = try McpApprovalMode.fromYAML(yaml) + XCTAssertEqual(instance.kind, McpApprovalModeKind.never) + XCTAssertEqual((try XCTUnwrap(instance.alwaysRequireApprovalTools)).count, 1) + XCTAssertEqual((try XCTUnwrap(instance.neverRequireApprovalTools)).count, 1) + let reloaded = try McpApprovalMode.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.kind, McpApprovalModeKind.never) + XCTAssertEqual((try XCTUnwrap(reloaded.alwaysRequireApprovalTools)).count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.neverRequireApprovalTools)).count, 1) + } + + func testScalarCoercion1() throws { + let instance = try McpApprovalMode.load("never") + XCTAssertEqual(instance.kind, McpApprovalModeKind.never) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tooling/ToolContextTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tooling/ToolContextTests.swift new file mode 100644 index 000000000..20144b57d --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tooling/ToolContextTests.swift @@ -0,0 +1,60 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ToolContextTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "metadata": { + "userId": "user-123" + }, + "messages": [ + { + "role": "user", + "parts": [ + { + "kind": "text", + "value": "Hello!" + } + ], + "metadata": { + "source": "user-input" + } + } + ] +} +""" + let instance = try ToolContext.fromJSON(json) + XCTAssertEqual(instance.messages.count, 1) + XCTAssertNotNil(instance.metadata) + let reloaded = try ToolContext.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.messages.count, 1) + XCTAssertNotNil(reloaded.metadata) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +metadata: + userId: user-123 +messages: + - role: user + parts: + - kind: text + value: Hello! + metadata: + source: user-input + +""" + let instance = try ToolContext.fromYAML(yaml) + XCTAssertEqual(instance.messages.count, 1) + XCTAssertNotNil(instance.metadata) + let reloaded = try ToolContext.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.messages.count, 1) + XCTAssertNotNil(reloaded.metadata) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tooling/ToolDispatchResultTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tooling/ToolDispatchResultTests.swift new file mode 100644 index 000000000..776f7a4ac --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tooling/ToolDispatchResultTests.swift @@ -0,0 +1,50 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ToolDispatchResultTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "toolCallId": "call_abc123", + "name": "get_weather", + "result": { + "parts": [ + { + "kind": "text", + "value": "72°F and sunny" + } + ] + } +} +""" + let instance = try ToolDispatchResult.fromJSON(json) + XCTAssertEqual(instance.toolCallId, "call_abc123") + XCTAssertEqual(instance.name, "get_weather") + let reloaded = try ToolDispatchResult.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.toolCallId, "call_abc123") + XCTAssertEqual(reloaded.name, "get_weather") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +toolCallId: call_abc123 +name: get_weather +result: + parts: + - kind: text + value: 72°F and sunny + +""" + let instance = try ToolDispatchResult.fromYAML(yaml) + XCTAssertEqual(instance.toolCallId, "call_abc123") + XCTAssertEqual(instance.name, "get_weather") + let reloaded = try ToolDispatchResult.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.toolCallId, "call_abc123") + XCTAssertEqual(reloaded.name, "get_weather") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tooling/ToolTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tooling/ToolTests.swift new file mode 100644 index 000000000..584430980 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tooling/ToolTests.swift @@ -0,0 +1,61 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class ToolTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "name": "my-tool", + "kind": "function", + "description": "A description of the tool", + "bindings": { + "input": "value" + } +} +""" + let instance = try Tool.fromJSON(json) + if case .functionTool(let concrete) = instance { + XCTAssertEqual(concrete.name, "my-tool") + XCTAssertEqual((try XCTUnwrap(concrete.description)), "A description of the tool") + } else { + XCTFail("Expected FunctionTool") + } + let reloaded = try Tool.fromJSON(try instance.toJSON()) + if case .functionTool(let concrete) = reloaded { + XCTAssertEqual(concrete.name, "my-tool") + XCTAssertEqual((try XCTUnwrap(concrete.description)), "A description of the tool") + } else { + XCTFail("Expected FunctionTool") + } + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +name: my-tool +kind: function +description: A description of the tool +bindings: + input: value + +""" + let instance = try Tool.fromYAML(yaml) + if case .functionTool(let concrete) = instance { + XCTAssertEqual(concrete.name, "my-tool") + XCTAssertEqual((try XCTUnwrap(concrete.description)), "A description of the tool") + } else { + XCTFail("Expected FunctionTool") + } + let reloaded = try Tool.fromYAML(try instance.toYAML()) + if case .functionTool(let concrete) = reloaded { + XCTAssertEqual(concrete.name, "my-tool") + XCTAssertEqual((try XCTUnwrap(concrete.description)), "A description of the tool") + } else { + XCTFail("Expected FunctionTool") + } + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tracing/TraceFileTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tracing/TraceFileTests.swift new file mode 100644 index 000000000..18115bae8 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tracing/TraceFileTests.swift @@ -0,0 +1,68 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class TraceFileTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "runtime": "python", + "version": "2.0.0", + "trace": { + "name": "prompty.core.pipeline.run", + "__time": { + "start": "2026-04-04T12:00:00Z", + "end": "2026-04-04T12:00:01Z", + "duration": 1000 + }, + "signature": "prompty.core.pipeline.run", + "error": "Connection refused" + } +} +""" + let instance = try TraceFile.fromJSON(json) + XCTAssertEqual(instance.runtime, "python") + XCTAssertEqual(instance.version, "2.0.0") + XCTAssertEqual(instance.trace.name, "prompty.core.pipeline.run") + XCTAssertEqual((try XCTUnwrap(instance.trace.signature)), "prompty.core.pipeline.run") + XCTAssertEqual((try XCTUnwrap(instance.trace.error)), "Connection refused") + let reloaded = try TraceFile.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.runtime, "python") + XCTAssertEqual(reloaded.version, "2.0.0") + XCTAssertEqual(reloaded.trace.name, "prompty.core.pipeline.run") + XCTAssertEqual((try XCTUnwrap(reloaded.trace.signature)), "prompty.core.pipeline.run") + XCTAssertEqual((try XCTUnwrap(reloaded.trace.error)), "Connection refused") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +runtime: python +version: 2.0.0 +trace: + name: prompty.core.pipeline.run + __time: + start: "2026-04-04T12:00:00Z" + end: "2026-04-04T12:00:01Z" + duration: 1000 + signature: prompty.core.pipeline.run + error: Connection refused + +""" + let instance = try TraceFile.fromYAML(yaml) + XCTAssertEqual(instance.runtime, "python") + XCTAssertEqual(instance.version, "2.0.0") + XCTAssertEqual(instance.trace.name, "prompty.core.pipeline.run") + XCTAssertEqual((try XCTUnwrap(instance.trace.signature)), "prompty.core.pipeline.run") + XCTAssertEqual((try XCTUnwrap(instance.trace.error)), "Connection refused") + let reloaded = try TraceFile.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.runtime, "python") + XCTAssertEqual(reloaded.version, "2.0.0") + XCTAssertEqual(reloaded.trace.name, "prompty.core.pipeline.run") + XCTAssertEqual((try XCTUnwrap(reloaded.trace.signature)), "prompty.core.pipeline.run") + XCTAssertEqual((try XCTUnwrap(reloaded.trace.error)), "Connection refused") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tracing/TraceSpanTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tracing/TraceSpanTests.swift new file mode 100644 index 000000000..a0821d5db --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tracing/TraceSpanTests.swift @@ -0,0 +1,65 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class TraceSpanTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "name": "prompty.core.pipeline.run", + "signature": "prompty.core.pipeline.run", + "error": "Connection refused", + "__time": { + "start": "2026-04-04T12:00:00Z", + "end": "2026-04-04T12:00:01Z", + "duration": 1000 + } +} +""" + let instance = try TraceSpan.fromJSON(json) + XCTAssertEqual(instance.name, "prompty.core.pipeline.run") + XCTAssertEqual((try XCTUnwrap(instance.signature)), "prompty.core.pipeline.run") + XCTAssertEqual((try XCTUnwrap(instance.error)), "Connection refused") + XCTAssertEqual(instance.time.start, "2026-04-04T12:00:00Z") + XCTAssertEqual(instance.time.end, "2026-04-04T12:00:01Z") + XCTAssertEqual(instance.time.duration, 1000) + let reloaded = try TraceSpan.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.name, "prompty.core.pipeline.run") + XCTAssertEqual((try XCTUnwrap(reloaded.signature)), "prompty.core.pipeline.run") + XCTAssertEqual((try XCTUnwrap(reloaded.error)), "Connection refused") + XCTAssertEqual(reloaded.time.start, "2026-04-04T12:00:00Z") + XCTAssertEqual(reloaded.time.end, "2026-04-04T12:00:01Z") + XCTAssertEqual(reloaded.time.duration, 1000) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +name: prompty.core.pipeline.run +signature: prompty.core.pipeline.run +error: Connection refused +__time: + start: "2026-04-04T12:00:00Z" + end: "2026-04-04T12:00:01Z" + duration: 1000 + +""" + let instance = try TraceSpan.fromYAML(yaml) + XCTAssertEqual(instance.name, "prompty.core.pipeline.run") + XCTAssertEqual((try XCTUnwrap(instance.signature)), "prompty.core.pipeline.run") + XCTAssertEqual((try XCTUnwrap(instance.error)), "Connection refused") + XCTAssertEqual(instance.time.start, "2026-04-04T12:00:00Z") + XCTAssertEqual(instance.time.end, "2026-04-04T12:00:01Z") + XCTAssertEqual(instance.time.duration, 1000) + let reloaded = try TraceSpan.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.name, "prompty.core.pipeline.run") + XCTAssertEqual((try XCTUnwrap(reloaded.signature)), "prompty.core.pipeline.run") + XCTAssertEqual((try XCTUnwrap(reloaded.error)), "Connection refused") + XCTAssertEqual(reloaded.time.start, "2026-04-04T12:00:00Z") + XCTAssertEqual(reloaded.time.end, "2026-04-04T12:00:01Z") + XCTAssertEqual(reloaded.time.duration, 1000) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tracing/TraceTimeTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tracing/TraceTimeTests.swift new file mode 100644 index 000000000..df701f423 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tracing/TraceTimeTests.swift @@ -0,0 +1,44 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class TraceTimeTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "start": "2026-04-04T12:00:00Z", + "end": "2026-04-04T12:00:01Z", + "duration": 1000 +} +""" + let instance = try TraceTime.fromJSON(json) + XCTAssertEqual(instance.start, "2026-04-04T12:00:00Z") + XCTAssertEqual(instance.end, "2026-04-04T12:00:01Z") + XCTAssertEqual(instance.duration, 1000) + let reloaded = try TraceTime.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.start, "2026-04-04T12:00:00Z") + XCTAssertEqual(reloaded.end, "2026-04-04T12:00:01Z") + XCTAssertEqual(reloaded.duration, 1000) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +start: "2026-04-04T12:00:00Z" +end: "2026-04-04T12:00:01Z" +duration: 1000 + +""" + let instance = try TraceTime.fromYAML(yaml) + XCTAssertEqual(instance.start, "2026-04-04T12:00:00Z") + XCTAssertEqual(instance.end, "2026-04-04T12:00:01Z") + XCTAssertEqual(instance.duration, 1000) + let reloaded = try TraceTime.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.start, "2026-04-04T12:00:00Z") + XCTAssertEqual(reloaded.end, "2026-04-04T12:00:01Z") + XCTAssertEqual(reloaded.duration, 1000) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicImageBlockTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicImageBlockTests.swift new file mode 100644 index 000000000..0c3ea5747 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicImageBlockTests.swift @@ -0,0 +1,53 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class AnthropicImageBlockTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": "iVBORw0KGgo..." + } +} +""" + let instance = try AnthropicImageBlock.fromJSON(json) + XCTAssertEqual(instance.type, "image") + XCTAssertEqual(instance.source.type, "base64") + XCTAssertEqual(instance.source.mediaType, "image/png") + XCTAssertEqual(instance.source.data, "iVBORw0KGgo...") + let reloaded = try AnthropicImageBlock.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.type, "image") + XCTAssertEqual(reloaded.source.type, "base64") + XCTAssertEqual(reloaded.source.mediaType, "image/png") + XCTAssertEqual(reloaded.source.data, "iVBORw0KGgo...") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +type: image +source: + type: base64 + media_type: image/png + data: iVBORw0KGgo... + +""" + let instance = try AnthropicImageBlock.fromYAML(yaml) + XCTAssertEqual(instance.type, "image") + XCTAssertEqual(instance.source.type, "base64") + XCTAssertEqual(instance.source.mediaType, "image/png") + XCTAssertEqual(instance.source.data, "iVBORw0KGgo...") + let reloaded = try AnthropicImageBlock.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.type, "image") + XCTAssertEqual(reloaded.source.type, "base64") + XCTAssertEqual(reloaded.source.mediaType, "image/png") + XCTAssertEqual(reloaded.source.data, "iVBORw0KGgo...") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicImageSourceTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicImageSourceTests.swift new file mode 100644 index 000000000..21975a7e9 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicImageSourceTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class AnthropicImageSourceTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "media_type": "image/png", + "data": "iVBORw0KGgo..." +} +""" + let instance = try AnthropicImageSource.fromJSON(json) + XCTAssertEqual(instance.mediaType, "image/png") + XCTAssertEqual(instance.data, "iVBORw0KGgo...") + let reloaded = try AnthropicImageSource.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.mediaType, "image/png") + XCTAssertEqual(reloaded.data, "iVBORw0KGgo...") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +media_type: image/png +data: iVBORw0KGgo... + +""" + let instance = try AnthropicImageSource.fromYAML(yaml) + XCTAssertEqual(instance.mediaType, "image/png") + XCTAssertEqual(instance.data, "iVBORw0KGgo...") + let reloaded = try AnthropicImageSource.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.mediaType, "image/png") + XCTAssertEqual(reloaded.data, "iVBORw0KGgo...") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicMessagesRequestTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicMessagesRequestTests.swift new file mode 100644 index 000000000..34eef52b6 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicMessagesRequestTests.swift @@ -0,0 +1,84 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class AnthropicMessagesRequestTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "model": "claude-sonnet-4-20250514", + "max_tokens": 4096, + "system": "You are a helpful assistant.", + "temperature": 0.7, + "top_p": 0.9, + "top_k": 40, + "stop_sequences": [ + "\\n\\nHuman:" + ], + "messages": [ + { + "role": "user", + "content": [] + } + ] +} +""" + let instance = try AnthropicMessagesRequest.fromJSON(json) + XCTAssertEqual(instance.model, "claude-sonnet-4-20250514") + XCTAssertEqual(instance.maxTokens, 4096) + XCTAssertEqual((try XCTUnwrap(instance.system)), "You are a helpful assistant.") + XCTAssertEqual((try XCTUnwrap(instance.temperature)), 0.7) + XCTAssertEqual((try XCTUnwrap(instance.topP)), 0.9) + XCTAssertEqual((try XCTUnwrap(instance.topK)), 40) + XCTAssertEqual(instance.messages.count, 1) + XCTAssertEqual((try XCTUnwrap(instance.stopSequences)).count, 1) + let reloaded = try AnthropicMessagesRequest.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.model, "claude-sonnet-4-20250514") + XCTAssertEqual(reloaded.maxTokens, 4096) + XCTAssertEqual((try XCTUnwrap(reloaded.system)), "You are a helpful assistant.") + XCTAssertEqual((try XCTUnwrap(reloaded.temperature)), 0.7) + XCTAssertEqual((try XCTUnwrap(reloaded.topP)), 0.9) + XCTAssertEqual((try XCTUnwrap(reloaded.topK)), 40) + XCTAssertEqual(reloaded.messages.count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.stopSequences)).count, 1) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +model: claude-sonnet-4-20250514 +max_tokens: 4096 +system: You are a helpful assistant. +temperature: 0.7 +top_p: 0.9 +top_k: 40 +stop_sequences: + - "\\n\\nHuman:" +messages: + - role: user + content: [] + +""" + let instance = try AnthropicMessagesRequest.fromYAML(yaml) + XCTAssertEqual(instance.model, "claude-sonnet-4-20250514") + XCTAssertEqual(instance.maxTokens, 4096) + XCTAssertEqual((try XCTUnwrap(instance.system)), "You are a helpful assistant.") + XCTAssertEqual((try XCTUnwrap(instance.temperature)), 0.7) + XCTAssertEqual((try XCTUnwrap(instance.topP)), 0.9) + XCTAssertEqual((try XCTUnwrap(instance.topK)), 40) + XCTAssertEqual(instance.messages.count, 1) + XCTAssertEqual((try XCTUnwrap(instance.stopSequences)).count, 1) + let reloaded = try AnthropicMessagesRequest.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.model, "claude-sonnet-4-20250514") + XCTAssertEqual(reloaded.maxTokens, 4096) + XCTAssertEqual((try XCTUnwrap(reloaded.system)), "You are a helpful assistant.") + XCTAssertEqual((try XCTUnwrap(reloaded.temperature)), 0.7) + XCTAssertEqual((try XCTUnwrap(reloaded.topP)), 0.9) + XCTAssertEqual((try XCTUnwrap(reloaded.topK)), 40) + XCTAssertEqual(reloaded.messages.count, 1) + XCTAssertEqual((try XCTUnwrap(reloaded.stopSequences)).count, 1) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicMessagesResponseTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicMessagesResponseTests.swift new file mode 100644 index 000000000..9b3bf72b3 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicMessagesResponseTests.swift @@ -0,0 +1,59 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class AnthropicMessagesResponseTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "id": "msg_01XFDUDYJgAACzvnptvVoYEL", + "model": "claude-sonnet-4-20250514", + "stop_reason": "end_turn", + "usage": { + "input_tokens": 150, + "output_tokens": 42 + } +} +""" + let instance = try AnthropicMessagesResponse.fromJSON(json) + XCTAssertEqual(instance.id, "msg_01XFDUDYJgAACzvnptvVoYEL") + XCTAssertEqual(instance.model, "claude-sonnet-4-20250514") + XCTAssertEqual(instance.stopReason, "end_turn") + XCTAssertEqual(instance.usage.inputTokens, 150) + XCTAssertEqual(instance.usage.outputTokens, 42) + let reloaded = try AnthropicMessagesResponse.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.id, "msg_01XFDUDYJgAACzvnptvVoYEL") + XCTAssertEqual(reloaded.model, "claude-sonnet-4-20250514") + XCTAssertEqual(reloaded.stopReason, "end_turn") + XCTAssertEqual(reloaded.usage.inputTokens, 150) + XCTAssertEqual(reloaded.usage.outputTokens, 42) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +id: msg_01XFDUDYJgAACzvnptvVoYEL +model: claude-sonnet-4-20250514 +stop_reason: end_turn +usage: + input_tokens: 150 + output_tokens: 42 + +""" + let instance = try AnthropicMessagesResponse.fromYAML(yaml) + XCTAssertEqual(instance.id, "msg_01XFDUDYJgAACzvnptvVoYEL") + XCTAssertEqual(instance.model, "claude-sonnet-4-20250514") + XCTAssertEqual(instance.stopReason, "end_turn") + XCTAssertEqual(instance.usage.inputTokens, 150) + XCTAssertEqual(instance.usage.outputTokens, 42) + let reloaded = try AnthropicMessagesResponse.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.id, "msg_01XFDUDYJgAACzvnptvVoYEL") + XCTAssertEqual(reloaded.model, "claude-sonnet-4-20250514") + XCTAssertEqual(reloaded.stopReason, "end_turn") + XCTAssertEqual(reloaded.usage.inputTokens, 150) + XCTAssertEqual(reloaded.usage.outputTokens, 42) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicTextBlockTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicTextBlockTests.swift new file mode 100644 index 000000000..0cfbbd25c --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicTextBlockTests.swift @@ -0,0 +1,32 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class AnthropicTextBlockTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "text": "Hello, how can I help?" +} +""" + let instance = try AnthropicTextBlock.fromJSON(json) + XCTAssertEqual(instance.text, "Hello, how can I help?") + let reloaded = try AnthropicTextBlock.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.text, "Hello, how can I help?") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +text: Hello, how can I help? + +""" + let instance = try AnthropicTextBlock.fromYAML(yaml) + XCTAssertEqual(instance.text, "Hello, how can I help?") + let reloaded = try AnthropicTextBlock.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.text, "Hello, how can I help?") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicToolDefinitionTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicToolDefinitionTests.swift new file mode 100644 index 000000000..ac08db3fc --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicToolDefinitionTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class AnthropicToolDefinitionTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "name": "get_weather", + "description": "Get the current weather for a city" +} +""" + let instance = try AnthropicToolDefinition.fromJSON(json) + XCTAssertEqual(instance.name, "get_weather") + XCTAssertEqual((try XCTUnwrap(instance.description)), "Get the current weather for a city") + let reloaded = try AnthropicToolDefinition.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.name, "get_weather") + XCTAssertEqual((try XCTUnwrap(reloaded.description)), "Get the current weather for a city") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +name: get_weather +description: Get the current weather for a city + +""" + let instance = try AnthropicToolDefinition.fromYAML(yaml) + XCTAssertEqual(instance.name, "get_weather") + XCTAssertEqual((try XCTUnwrap(instance.description)), "Get the current weather for a city") + let reloaded = try AnthropicToolDefinition.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.name, "get_weather") + XCTAssertEqual((try XCTUnwrap(reloaded.description)), "Get the current weather for a city") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicToolResultBlockTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicToolResultBlockTests.swift new file mode 100644 index 000000000..77f05565d --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicToolResultBlockTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class AnthropicToolResultBlockTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "tool_use_id": "toolu_01A09q90qw90lq917835lq9", + "content": "72°F and sunny in Paris" +} +""" + let instance = try AnthropicToolResultBlock.fromJSON(json) + XCTAssertEqual(instance.toolUseId, "toolu_01A09q90qw90lq917835lq9") + XCTAssertEqual(instance.content, "72°F and sunny in Paris") + let reloaded = try AnthropicToolResultBlock.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.toolUseId, "toolu_01A09q90qw90lq917835lq9") + XCTAssertEqual(reloaded.content, "72°F and sunny in Paris") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +tool_use_id: toolu_01A09q90qw90lq917835lq9 +content: 72°F and sunny in Paris + +""" + let instance = try AnthropicToolResultBlock.fromYAML(yaml) + XCTAssertEqual(instance.toolUseId, "toolu_01A09q90qw90lq917835lq9") + XCTAssertEqual(instance.content, "72°F and sunny in Paris") + let reloaded = try AnthropicToolResultBlock.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.toolUseId, "toolu_01A09q90qw90lq917835lq9") + XCTAssertEqual(reloaded.content, "72°F and sunny in Paris") + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicToolUseBlockTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicToolUseBlockTests.swift new file mode 100644 index 000000000..c2f38eb46 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicToolUseBlockTests.swift @@ -0,0 +1,47 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class AnthropicToolUseBlockTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "id": "toolu_01A09q90qw90lq917835lq9", + "name": "get_weather", + "input": { + "city": "Paris" + } +} +""" + let instance = try AnthropicToolUseBlock.fromJSON(json) + XCTAssertEqual(instance.id, "toolu_01A09q90qw90lq917835lq9") + XCTAssertEqual(instance.name, "get_weather") + XCTAssertNotNil(instance.input) + let reloaded = try AnthropicToolUseBlock.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.id, "toolu_01A09q90qw90lq917835lq9") + XCTAssertEqual(reloaded.name, "get_weather") + XCTAssertNotNil(reloaded.input) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +id: toolu_01A09q90qw90lq917835lq9 +name: get_weather +input: + city: Paris + +""" + let instance = try AnthropicToolUseBlock.fromYAML(yaml) + XCTAssertEqual(instance.id, "toolu_01A09q90qw90lq917835lq9") + XCTAssertEqual(instance.name, "get_weather") + XCTAssertNotNil(instance.input) + let reloaded = try AnthropicToolUseBlock.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.id, "toolu_01A09q90qw90lq917835lq9") + XCTAssertEqual(reloaded.name, "get_weather") + XCTAssertNotNil(reloaded.input) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicUsageTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicUsageTests.swift new file mode 100644 index 000000000..febfd9dee --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicUsageTests.swift @@ -0,0 +1,38 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class AnthropicUsageTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "input_tokens": 150, + "output_tokens": 42 +} +""" + let instance = try AnthropicUsage.fromJSON(json) + XCTAssertEqual(instance.inputTokens, 150) + XCTAssertEqual(instance.outputTokens, 42) + let reloaded = try AnthropicUsage.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.inputTokens, 150) + XCTAssertEqual(reloaded.outputTokens, 42) + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +input_tokens: 150 +output_tokens: 42 + +""" + let instance = try AnthropicUsage.fromYAML(yaml) + XCTAssertEqual(instance.inputTokens, 150) + XCTAssertEqual(instance.outputTokens, 42) + let reloaded = try AnthropicUsage.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.inputTokens, 150) + XCTAssertEqual(reloaded.outputTokens, 42) + } + +} diff --git a/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicWireMessageTests.swift b/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicWireMessageTests.swift new file mode 100644 index 000000000..90285f8d5 --- /dev/null +++ b/runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicWireMessageTests.swift @@ -0,0 +1,32 @@ +// +// Code generated by Typra emitter; DO NOT EDIT. + +import Foundation +import XCTest +@testable import PromptyModel + +final class AnthropicWireMessageTests: XCTestCase { + func testJSONRoundTrip1() throws { + let json = """ +{ + "role": "user" +} +""" + let instance = try AnthropicWireMessage.fromJSON(json) + XCTAssertEqual(instance.role, "user") + let reloaded = try AnthropicWireMessage.fromJSON(try instance.toJSON()) + XCTAssertEqual(reloaded.role, "user") + } + + func testYAMLRoundTrip1() throws { + let yaml = """ +role: user + +""" + let instance = try AnthropicWireMessage.fromYAML(yaml) + XCTAssertEqual(instance.role, "user") + let reloaded = try AnthropicWireMessage.fromYAML(try instance.toYAML()) + XCTAssertEqual(reloaded.role, "user") + } + +} diff --git a/runtime/swift/prompty/Package.swift b/runtime/swift/prompty/Package.swift new file mode 100644 index 000000000..a9ba41f70 --- /dev/null +++ b/runtime/swift/prompty/Package.swift @@ -0,0 +1,36 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "Prompty", + platforms: [.macOS(.v12), .iOS(.v15)], + products: [ + .library(name: "Prompty", targets: ["Prompty"]), + .library(name: "PromptyOpenAI", targets: ["PromptyOpenAI"]), + ], + dependencies: [ + .package(path: "../prompty-model"), + .package(url: "https://github.com/jpsim/Yams.git", from: "5.1.3"), + ], + targets: [ + .target( + name: "Prompty", + dependencies: [ + .product(name: "PromptyModel", package: "prompty-model"), + .product(name: "Yams", package: "Yams"), + ] + ), + .target( + name: "PromptyOpenAI", + dependencies: ["Prompty", .product(name: "PromptyModel", package: "prompty-model")] + ), + .testTarget( + name: "PromptyTests", + dependencies: [ + "Prompty", "PromptyOpenAI", + .product(name: "PromptyModel", package: "prompty-model"), + .product(name: "Yams", package: "Yams"), + ] + ), + ] +) diff --git a/runtime/swift/prompty/Sources/Prompty/Defaults.swift b/runtime/swift/prompty/Sources/Prompty/Defaults.swift new file mode 100644 index 000000000..684777e1e --- /dev/null +++ b/runtime/swift/prompty/Sources/Prompty/Defaults.swift @@ -0,0 +1,30 @@ +/// Cross-runtime constants. These values are part of the Agent spec and must +/// match every other runtime exactly. +public enum Defaults { + /// Renderer key used when `template.format.kind` is absent or empty. + public static let templateFormat = "jinja2" + + /// Parser key used when `template.parser.kind` is absent or empty. + public static let parser = "prompty" + + /// Provider key used when `model.provider` is absent or empty. + public static let provider = "openai" + + /// Injected discriminator — a `.prompty` file is always a prompt. + public static let kind = "prompt" + + /// Metadata key holding the file a prompt was loaded from. + public static let sourcePathKey = "__source_path" + + /// Marker key identifying a wrapped structured result. + public static let structuredMarker = "__prompty_structured" + + /// Prefix of a rich-input nonce placeholder. + public static let threadNoncePrefix = "__PROMPTY_THREAD_" + + /// Input kinds replaced by nonce placeholders during rendering. + public static let richKinds: Set = ["thread", "image", "file", "audio"] + + /// Role markers the Agent parser recognizes. + public static let roleMarkers: Set = ["system", "user", "assistant"] +} diff --git a/runtime/swift/prompty/Sources/Prompty/Errors.swift b/runtime/swift/prompty/Sources/Prompty/Errors.swift new file mode 100644 index 000000000..dba5c6fef --- /dev/null +++ b/runtime/swift/prompty/Sources/Prompty/Errors.swift @@ -0,0 +1,65 @@ +/// Errors raised while loading a `.prompty` file. +/// +/// Message formats are shared across every Agent runtime so that host +/// applications can match on them consistently. + +/// Errors raised while resolving or running a pipeline stage. +public enum LoadError: Error, CustomStringConvertible, Equatable { + /// The `.prompty` file could not be read. + case fileNotFound(path: String, detail: String) + /// The YAML frontmatter was absent, unterminated, or not a mapping. + case invalidFrontmatter(String) + /// A `${env:VAR}` reference had no value and no default. + case envVarNotSet(varName: String, key: String) + /// A `${file:path}` reference could not be read, parsed, or was out of bounds. + case fileReference(path: String, detail: String) + /// The frontmatter did not satisfy the Agent model schema. + case invalidModel(String) + /// A name-keyed collection (`inputs`, `outputs`, nested `properties`) held a + /// value that can never be a property. `path` is the full dotted path to the + /// offending entry and `valueCategory` names the rejected shape. + case invalidNamedCollectionEntry(path: String, valueCategory: String) + + public var description: String { + switch self { + case .fileNotFound(let path, let detail): + return "File not found: \(path): \(detail)" + case .invalidFrontmatter(let message): + return "Invalid frontmatter: \(message)" + case .envVarNotSet(let varName, let key): + return "Environment variable '\(varName)' not set for key '\(key)'" + case .fileReference(let path, let detail): + return "File reference error: \(path): \(detail)" + case .invalidModel(let message): + return "Invalid prompty model: \(message)" + case .invalidNamedCollectionEntry(let path, let valueCategory): + return + "invalid-named-collection-entry: \(path) holds a \(valueCategory), " + + "which is not a valid named collection entry. Declare the array inside " + + "a property instead, for example `\(path): { kind: array, default: [...] }`" + } + } +} +public enum InvokerError: Error, CustomStringConvertible, Equatable { + /// No implementation is registered under `key` for the given `group`. + case notFound(group: String, key: String) + /// The parser rejected the rendered text. + case parse(String) + /// A required input was missing, or an input failed validation. + case validation(String) + /// A provider call failed. + case execution(String) + /// A response could not be turned into a result. + case processing(String) + + public var description: String { + switch self { + case .notFound(let group, let key): + return "no \(group) registered for key '\(key)'" + case .parse(let message): return message + case .validation(let message): return message + case .execution(let message): return message + case .processing(let message): return message + } + } +} diff --git a/runtime/swift/prompty/Sources/Prompty/ExpressionParser.swift b/runtime/swift/prompty/Sources/Prompty/ExpressionParser.swift new file mode 100644 index 000000000..e1f0474e9 --- /dev/null +++ b/runtime/swift/prompty/Sources/Prompty/ExpressionParser.swift @@ -0,0 +1,495 @@ +/// Evaluates the expression grammar used inside `{{ … }}` and `{% if … %}`. +/// +/// Prompt templates routinely branch on comparisons (`{% if score > 3 %}`), +/// membership (`{% if role in allowed %}`) and negation (`{% if not draft %}`). +/// Treating those as opaque variable names resolves them to `nil`, which is +/// falsy — so the template silently renders the wrong branch instead of +/// failing. This parser exists to make that class of bug impossible: anything +/// it cannot evaluate is rejected loudly. +/// +/// Precedence, lowest to highest, follows Jinja: +/// `or` → `and` → `not` → comparison / `in` / `is` → `~` → `+ -` → `* / // %` +/// → unary `-` → filters, member access, indexing. +import Foundation + +enum ExpressionParser { + + static func evaluate( + _ source: String, + scope: [String: Any], + filter: @escaping (String, [Any?], Any?) throws -> Any? + ) throws -> Any? { + var parser = Parser(source: source, scope: scope, filter: filter) + let value = try parser.parseExpression() + try parser.expectEnd() + return value + } + + // MARK: - Tokenizer + + enum Token: Equatable { + case identifier(String) + case string(String) + case number(Double, isInteger: Bool) + case symbol(String) + } + + static func tokenize(_ source: String) throws -> [Token] { + var tokens: [Token] = [] + let characters = Array(source) + var index = 0 + + // Multi-character operators must be matched before their prefixes, or + // `<=` would tokenize as `<` followed by `=`. + let operators = [ + "//", "==", "!=", "<=", ">=", "**", "|", "(", ")", "[", "]", ",", ".", + "+", "-", "*", "/", "%", "<", ">", "~", + ] + + while index < characters.count { + let character = characters[index] + + if character.isWhitespace { + index += 1 + continue + } + + if character == "\"" || character == "'" { + let quote = character + index += 1 + var value = "" + while index < characters.count, characters[index] != quote { + // Backslash escapes keep quoted punctuation out of the operator path. + if characters[index] == "\\", index + 1 < characters.count { + index += 1 + } + value.append(characters[index]) + index += 1 + } + guard index < characters.count else { + throw InvokerError.parse("unterminated string in expression '\(source)'") + } + index += 1 + tokens.append(.string(value)) + continue + } + + if character.isNumber { + var text = "" + var isInteger = true + while index < characters.count, characters[index].isNumber || characters[index] == "." { + // A trailing dot is member access on a number literal, not a decimal + // point, so only consume it when a digit follows. + if characters[index] == "." { + guard isInteger, index + 1 < characters.count, characters[index + 1].isNumber else { + break + } + isInteger = false + } + text.append(characters[index]) + index += 1 + } + guard let number = Double(text) else { + throw InvokerError.parse("invalid number '\(text)' in expression '\(source)'") + } + tokens.append(.number(number, isInteger: isInteger)) + continue + } + + if character.isLetter || character == "_" { + var text = "" + while index < characters.count, + characters[index].isLetter || characters[index].isNumber || characters[index] == "_" + { + text.append(characters[index]) + index += 1 + } + tokens.append(.identifier(text)) + continue + } + + if let match = operators.first(where: { matches($0, characters, at: index) }) { + tokens.append(.symbol(match)) + index += match.count + continue + } + + throw InvokerError.parse("unexpected character '\(character)' in expression '\(source)'") + } + + return tokens + } + + private static func matches(_ candidate: String, _ characters: [Character], at index: Int) -> Bool + { + let symbol = Array(candidate) + guard index + symbol.count <= characters.count else { return false } + for offset in 0.. Any? + var position = 0 + + init( + source: String, scope: [String: Any], + filter: @escaping (String, [Any?], Any?) throws -> Any? + ) { + self.tokens = (try? ExpressionParser.tokenize(source)) ?? [] + self.source = source + self.scope = scope + self.filter = filter + } + + var current: Token? { position < tokens.count ? tokens[position] : nil } + + mutating func expectEnd() throws { + guard position >= tokens.count else { + throw InvokerError.parse("unparsed trailing input in expression '\(source)'") + } + } + + mutating func consume(symbol: String) -> Bool { + guard current == .symbol(symbol) else { return false } + position += 1 + return true + } + + mutating func consume(identifier: String) -> Bool { + guard current == .identifier(identifier) else { return false } + position += 1 + return true + } + + // MARK: Precedence levels + + mutating func parseExpression() throws -> Any? { try parseOr() } + + mutating func parseOr() throws -> Any? { + var left = try parseAnd() + while consume(identifier: "or") { + let right = try parseAnd() + // Jinja's `or` yields the operand, not a boolean. + left = JSONSupport.isTruthy(left) ? left : right + } + return left + } + + mutating func parseAnd() throws -> Any? { + var left = try parseNot() + while consume(identifier: "and") { + let right = try parseNot() + left = JSONSupport.isTruthy(left) ? right : left + } + return left + } + + mutating func parseNot() throws -> Any? { + if consume(identifier: "not") { + return !JSONSupport.isTruthy(try parseNot()) + } + return try parseComparison() + } + + mutating func parseComparison() throws -> Any? { + let left = try parseConcat() + + if consume(identifier: "is") { + let negated = consume(identifier: "not") + guard case .identifier(let test)? = current else { + throw InvokerError.parse("expected a test after 'is' in '\(source)'") + } + position += 1 + let result = try applyTest(test, to: left) + return negated ? !result : result + } + + if consume(identifier: "in") { + return contains(try parseConcat(), left) + } + if consume(identifier: "not") { + guard consume(identifier: "in") else { + throw InvokerError.parse("expected 'in' after 'not' in '\(source)'") + } + return !contains(try parseConcat(), left) + } + + for symbol in ["==", "!=", "<=", ">=", "<", ">"] where consume(symbol: symbol) { + return try compare(symbol, left, try parseConcat()) + } + return left + } + + mutating func parseConcat() throws -> Any? { + var left = try parseAdditive() + while consume(symbol: "~") { + left = JSONSupport.stringify(left) + JSONSupport.stringify(try parseAdditive()) + } + return left + } + + mutating func parseAdditive() throws -> Any? { + var left = try parseMultiplicative() + while true { + if consume(symbol: "+") { + let right = try parseMultiplicative() + // `+` concatenates when either side is a string, matching Jinja. + if left is String || right is String { + left = JSONSupport.stringify(left) + JSONSupport.stringify(right) + } else if let a = left as? [Any], let b = right as? [Any] { + left = a + b + } else { + left = try arithmetic("+", left, right) + } + } else if consume(symbol: "-") { + left = try arithmetic("-", left, try parseMultiplicative()) + } else { + return left + } + } + } + + mutating func parseMultiplicative() throws -> Any? { + var left = try parseUnary() + while true { + if let symbol = ["*", "//", "/", "%"].first(where: { current == .symbol($0) }) { + position += 1 + left = try arithmetic(symbol, left, try parseUnary()) + } else { + return left + } + } + } + + mutating func parseUnary() throws -> Any? { + if consume(symbol: "-") { + return try arithmetic("-", 0, try parseUnary()) + } + if consume(symbol: "+") { + return try parseUnary() + } + return try parsePostfix() + } + + /// Member access, indexing, and filters all bind tighter than operators. + mutating func parsePostfix() throws -> Any? { + var value = try parsePrimary() + + while let token = current { + if token == .symbol(".") { + position += 1 + guard case .identifier(let name)? = current else { + throw InvokerError.parse("expected a property name after '.' in '\(source)'") + } + position += 1 + value = member(name, of: value) + } else if token == .symbol("[") { + position += 1 + let index = try parseExpression() + guard consume(symbol: "]") else { + throw InvokerError.parse("expected ']' in '\(source)'") + } + value = subscriptValue(value, by: index) + } else if token == .symbol("|") { + position += 1 + guard case .identifier(let name)? = current else { + throw InvokerError.parse("expected a filter name after '|' in '\(source)'") + } + position += 1 + var arguments: [Any?] = [] + if consume(symbol: "(") { + if !consume(symbol: ")") { + repeat { arguments.append(try parseExpression()) } while consume(symbol: ",") + guard consume(symbol: ")") else { + throw InvokerError.parse("expected ')' after filter arguments in '\(source)'") + } + } + } + value = try filter(name, arguments, value) + } else { + break + } + } + return value + } + + mutating func parsePrimary() throws -> Any? { + guard let token = current else { + throw InvokerError.parse("unexpected end of expression '\(source)'") + } + + switch token { + case .number(let value, let isInteger): + position += 1 + return isInteger ? Int(value) : value + + case .string(let value): + position += 1 + return value + + case .identifier(let name): + position += 1 + switch name { + case "true", "True": return true + case "false", "False": return false + case "none", "None", "null": return nil + default: return scope[name] + } + + case .symbol("("): + position += 1 + let value = try parseExpression() + guard consume(symbol: ")") else { + throw InvokerError.parse("expected ')' in '\(source)'") + } + return value + + case .symbol("["): + position += 1 + var elements: [Any] = [] + if !consume(symbol: "]") { + repeat { elements.append(try parseExpression() ?? NSNull()) } while consume(symbol: ",") + guard consume(symbol: "]") else { + throw InvokerError.parse("expected ']' in '\(source)'") + } + } + return elements + + case .symbol(let symbol): + throw InvokerError.parse("unexpected '\(symbol)' in expression '\(source)'") + } + } + + // MARK: Operations + + func member(_ name: String, of value: Any?) -> Any? { + if let dictionary = value as? [String: Any] { return dictionary[name] } + // Jinja allows `list.0`; mirror that for numeric members. + if let array = value as? [Any], let index = Int(name) { + return index >= 0 && index < array.count ? array[index] : nil + } + return nil + } + + func subscriptValue(_ value: Any?, by index: Any?) -> Any? { + if let key = index as? String { return member(key, of: value) } + guard let array = value as? [Any], let position = index as? Int else { return nil } + // Negative indices count from the end, as in Python. + let resolved = position < 0 ? array.count + position : position + return resolved >= 0 && resolved < array.count ? array[resolved] : nil + } + + func applyTest(_ name: String, to value: Any?) throws -> Bool { + switch name { + case "defined": return value != nil && !(value is NSNull) + case "undefined": return value == nil || value is NSNull + case "none", "null": return value == nil || value is NSNull + case "string": return value is String + case "number": return value is Int || value is Double + case "boolean": return value is Bool + case "sequence", "iterable": return value is [Any] + case "mapping": return value is [String: Any] + case "even", "odd": + guard let int = value as? Int else { return false } + return name == "even" ? int % 2 == 0 : int % 2 != 0 + default: + throw InvokerError.parse("unsupported template test '\(name)'") + } + } + + func contains(_ container: Any?, _ needle: Any?) -> Bool { + if let string = container as? String { + return string.contains(JSONSupport.stringify(needle)) + } + if let dictionary = container as? [String: Any], let key = needle as? String { + return dictionary[key] != nil + } + if let array = container as? [Any] { + return array.contains { JSONSupport.equals($0, needle) } + } + return false + } + + func compare(_ symbol: String, _ left: Any?, _ right: Any?) throws -> Bool { + if symbol == "==" { return JSONSupport.equals(left, right) } + if symbol == "!=" { return !JSONSupport.equals(left, right) } + + // Ordering is defined for numbers and strings only; anything else is a + // template bug worth surfacing rather than silently reporting `false`. + if let a = numeric(left), let b = numeric(right) { + switch symbol { + case "<": return a < b + case "<=": return a <= b + case ">": return a > b + default: return a >= b + } + } + if let a = left as? String, let b = right as? String { + switch symbol { + case "<": return a < b + case "<=": return a <= b + case ">": return a > b + default: return a >= b + } + } + throw InvokerError.parse( + "cannot compare \(JSONSupport.stringify(left)) \(symbol) \(JSONSupport.stringify(right))" + + " in '\(source)'") + } + + func arithmetic(_ symbol: String, _ left: Any?, _ right: Any?) throws -> Any? { + guard let a = numeric(left), let b = numeric(right) else { + throw InvokerError.parse( + "cannot apply '\(symbol)' to \(JSONSupport.stringify(left))" + + " and \(JSONSupport.stringify(right)) in '\(source)'") + } + + let bothIntegers = isInteger(left) && isInteger(right) + switch symbol { + case "+": return bothIntegers ? Int(a + b) as Any : a + b + case "-": return bothIntegers ? Int(a - b) as Any : a - b + case "*": return bothIntegers ? Int(a * b) as Any : a * b + case "/": + guard b != 0 else { throw InvokerError.parse("division by zero in '\(source)'") } + return a / b + case "//": + guard b != 0 else { throw InvokerError.parse("division by zero in '\(source)'") } + return Int((a / b).rounded(.down)) + case "%": + guard b != 0 else { throw InvokerError.parse("division by zero in '\(source)'") } + return bothIntegers + ? Int(a.truncatingRemainder(dividingBy: b)) as Any + : a.truncatingRemainder(dividingBy: b) + default: + throw InvokerError.parse("unsupported operator '\(symbol)' in '\(source)'") + } + } + + func numeric(_ value: Any?) -> Double? { + switch value { + case let int as Int: return Double(int) + case let double as Double: return double + case let number as NSNumber: return number.doubleValue + default: return nil + } + } + + func isInteger(_ value: Any?) -> Bool { + if value is Int { return true } + if let number = value as? NSNumber { + return + !(number.doubleValue.truncatingRemainder( + dividingBy: 1) != 0) && !(value is Double) + } + return false + } + } +} diff --git a/runtime/swift/prompty/Sources/Prompty/Frontmatter.swift b/runtime/swift/prompty/Sources/Prompty/Frontmatter.swift new file mode 100644 index 000000000..1c50f0fc1 --- /dev/null +++ b/runtime/swift/prompty/Sources/Prompty/Frontmatter.swift @@ -0,0 +1,88 @@ +import Foundation +/// Splits a `.prompty` file into YAML frontmatter and a markdown body. +/// +/// Frontmatter is delimited by a leading `---` or `+++` line and closed by a +/// line whose trimmed content is exactly `---` or `+++`. +import Yams + +public enum Frontmatter { + + /// Split raw file contents. + /// + /// Returns the parsed frontmatter mapping and the untrimmed body. A file with + /// no opening delimiter is treated as body-only with empty frontmatter. + /// + /// Scanning walks `Lines` rather than `firstIndex(of: "\n")`. Swift clusters a + /// CRLF pair into a single `Character`, so a character search for `"\n"` finds + /// nothing in a Windows-authored file: the opening delimiter looks unterminated + /// and the whole document is silently discarded. + /// + /// This is the one and only line-ending normalization in the load path — the + /// returned body always uses LF. Normalizing again upstream would be lossy: + /// `a\r\r\nb` collapses to `a\r\nb` on the first pass, and a second pass would + /// then read that residual CR + LF as a terminator and delete the lone CR that + /// `Lines` and the Rust reference both preserve. + public static func split(_ raw: String) throws -> (frontmatter: [String: Any], body: String) { + let lines = Lines.splitLineFeeds(raw) + let normalizedRaw = lines.joined(separator: "\n") + + // Leading blank lines are insignificant, and the delimiter may be indented. + // `isWhitespace` — not `.whitespaces` — so vertical tab, form feed, NEL, and + // the Unicode separators count as blank, matching the previous behavior. + var index = 0 + while index < lines.count, lines[index].allSatisfy({ $0.isWhitespace }) { + index += 1 + } + guard index < lines.count else { return ([:], normalizedRaw) } + + let opener = String(lines[index].drop(while: { $0.isWhitespace })) + guard opener.hasPrefix("---") || opener.hasPrefix("+++") else { + return ([:], normalizedRaw) + } + + // An opening delimiter with nothing after it: empty frontmatter, empty body. + guard index + 1 < lines.count else { return ([:], "") } + + var cursor = index + 1 + var yamlLines: [String] = [] + while cursor < lines.count { + let line = lines[cursor].trimmingCharacters(in: .whitespaces) + if line == "---" || line == "+++" { break } + yamlLines.append(lines[cursor]) + cursor += 1 + } + + guard cursor < lines.count else { + throw LoadError.invalidFrontmatter("Opening delimiter without closing match") + } + + // Rejoining with LF keeps the trailing terminator the character-index + // version produced, so YAML parsing sees byte-identical text. + let yamlText = yamlLines.isEmpty ? "" : yamlLines.joined(separator: "\n") + "\n" + let body = + cursor + 1 < lines.count + ? lines[(cursor + 1)...].joined(separator: "\n") + : "" + + return (try parseYAML(yamlText), body) + } + + /// Parse a YAML mapping into normalized `Any` values. + public static func parseYAML(_ yaml: String) throws -> [String: Any] { + let trimmed = yaml.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.isEmpty { return [:] } + + let parsed: Any? + do { + parsed = try Yams.load(yaml: trimmed) + } catch { + throw LoadError.invalidFrontmatter(String(describing: error)) + } + + guard let normalized = JSONSupport.normalize(parsed) else { return [:] } + guard let mapping = normalized as? [String: Any] else { + throw LoadError.invalidFrontmatter("Frontmatter must be a YAML mapping") + } + return mapping + } +} diff --git a/runtime/swift/prompty/Sources/Prompty/Harness.swift b/runtime/swift/prompty/Sources/Prompty/Harness.swift new file mode 100644 index 000000000..f88593b8c --- /dev/null +++ b/runtime/swift/prompty/Sources/Prompty/Harness.swift @@ -0,0 +1,322 @@ +import Foundation + +/// Durable harness adapters: event capture, journaling, replay verification, +/// checkpoint storage, permission resolution and host tool dispatch. +/// +/// These mirror `runtime/rust/prompty/src/harness.rs` so a session recorded by +/// one runtime replays identically in another. The protocols themselves are +/// owned by the generated model (`PromptyModel/pipeline/`); this file only +/// supplies reference implementations. + +// MARK: - Event capture + +/// Collects emitted events in memory. +/// +/// Reference implementations are handed to engines that may fan out across +/// tasks, so the buffers are lock-guarded and the type is `Sendable`. +import PromptyModel + +// MARK: - Journaling + +/// Appends replayable journal records as newline-delimited JSON. +/// +/// Every write is an append to a closed file handle rather than a retained one, +/// which is what makes the journal durable across a crash mid-turn. + +// MARK: - Replay verification + +/// Compares an expected journal against an actual one, position by position. + +// MARK: - Checkpoint storage + +/// Stores checkpoints in memory, keyed by session and checkpoint id. + +// MARK: - Permission resolution + +/// Approves every permission request. + +/// Denies every permission request. + +// MARK: - Host tool dispatch + +/// Dispatches host tool requests to registered local functions. +public final class CollectingEventSink: EventSink, @unchecked Sendable { + private let lock = NSLock() + private var turn: [TurnEvent] = [] + private var session: [SessionEvent] = [] + + public init() {} + + public var turnEvents: [TurnEvent] { + lock.lock() + defer { lock.unlock() } + return turn + } + + public var sessionEvents: [SessionEvent] { + lock.lock() + defer { lock.unlock() } + return session + } + + public func emitTurn(turnEvent: TurnEvent) throws -> Bool { + lock.lock() + defer { lock.unlock() } + turn.append(turnEvent) + return true + } + + public func emitSession(sessionEvent: SessionEvent) throws -> Bool { + lock.lock() + defer { lock.unlock() } + session.append(sessionEvent) + return true + } +} +public final class JsonlEventJournalWriter: EventJournalWriter, @unchecked Sendable { + private let lock = NSLock() + private let url: URL + private var closed = false + + public init(path: String) { + self.url = URL(fileURLWithPath: path) + try? FileManager.default.createDirectory( + at: url.deletingLastPathComponent(), withIntermediateDirectories: true) + } + + public var path: String { url.path } + + public func appendTurn(turnEvent: TurnEvent) throws -> Bool { + try append(["kind": "turn", "event": turnEvent.save()]) + } + + public func appendSession(sessionEvent: SessionEvent) throws -> Bool { + try append(["kind": "session", "event": sessionEvent.save()]) + } + + /// Writes the summary (when present) and seals the journal. A second close is + /// reported as `false` rather than silently succeeding, matching Rust. + public func close(summary: SessionSummary?) throws -> Bool { + lock.lock() + defer { lock.unlock() } + if closed { return false } + + if let summary { + guard try appendLocked(["kind": "summary", "summary": summary.save()]) else { return false } + } + closed = true + return true + } + + private func append(_ record: [String: Any]) throws -> Bool { + lock.lock() + defer { lock.unlock() } + if closed { return false } + return try appendLocked(record) + } + + private func appendLocked(_ record: [String: Any]) throws -> Bool { + guard let line = (JSONSupport.toJSON(record) + "\n").data(using: .utf8) else { return false } + + if !FileManager.default.fileExists(atPath: url.path) { + return FileManager.default.createFile(atPath: url.path, contents: line) + } + guard let handle = try? FileHandle(forWritingTo: url) else { return false } + defer { try? handle.close() } + try handle.seekToEnd() + try handle.write(contentsOf: line) + return true + } + + /// Read a journal back as records, dropping the trailing summary line. + /// + /// Useful for replay verification against a journal written by any runtime. + public static func readRecords(path: String) throws -> [[String: Any]] { + let contents = try String(contentsOfFile: path, encoding: .utf8) + return + Lines.split(contents) + .compactMap { JSONSupport.parse(json: String($0)) as? [String: Any] } + } +} +public struct ReferenceReplayVerifier: Sendable { + public init() {} + + public func verify(_ request: ReplayVerificationRequest) throws -> ReplayVerificationResult { + let expected = request.expected + let actual = request.actual + var mismatches: [ReplayMismatch] = [] + + for index in 0.. String? { + guard let record else { return nil } + return JSONSupport.toJSON(try record.save()) + } +} +public final class InMemoryCheckpointStore: CheckpointStore, @unchecked Sendable { + private let lock = NSLock() + private var checkpoints: [String: Checkpoint] = [:] + + public init() {} + + public func save(checkpoint: Checkpoint) async throws -> Checkpoint { + guard let sessionId = checkpoint.sessionId, let id = checkpoint.id else { + throw InvokerError.execution("Checkpoint requires both sessionId and id to be stored") + } + store(checkpoint, at: Self.key(sessionId, id)) + return checkpoint + } + + public func load(sessionId: String, checkpointId: String) async throws -> Checkpoint? { + read(Self.key(sessionId, checkpointId)) + } + + /// Ordered by id so replay and resume see a stable sequence. + public func listCheckpoints(sessionId: String) async throws -> [Checkpoint] { + all(for: sessionId) + } + + // Locking lives in synchronous helpers: holding a lock across a suspension + // point is unavailable from async contexts and is an error in Swift 6. + + private func store(_ checkpoint: Checkpoint, at key: String) { + lock.lock() + defer { lock.unlock() } + checkpoints[key] = checkpoint + } + + private func read(_ key: String) -> Checkpoint? { + lock.lock() + defer { lock.unlock() } + return checkpoints[key] + } + + private func all(for sessionId: String) -> [Checkpoint] { + lock.lock() + defer { lock.unlock() } + return + checkpoints.values + .filter { $0.sessionId == sessionId } + .sorted { ($0.id ?? "") < ($1.id ?? "") } + } + + private static func key(_ sessionId: String, _ checkpointId: String) -> String { + // Escape the separator so ids containing it cannot collide. + "\(sessionId.replacingOccurrences(of: "\u{1}", with: ""))\u{1}\(checkpointId)" + } +} +public struct AllowAllPermissionResolver: PermissionResolver, Sendable { + public init() {} + + public func request(request: PermissionRequest) async throws -> PermissionDecision { + PermissionDecision( + requestId: request.requestId, + toolCallId: request.toolCallId, + permission: request.permission, + approved: true, + reason: "allow_all" + ) + } +} +public struct DenyAllPermissionResolver: PermissionResolver, Sendable { + public init() {} + + public func request(request: PermissionRequest) async throws -> PermissionDecision { + PermissionDecision( + requestId: request.requestId, + toolCallId: request.toolCallId, + permission: request.permission, + approved: false, + reason: "deny_all" + ) + } +} +public struct FunctionHostToolExecutor: HostToolExecutor, Sendable { + public typealias Handler = @Sendable ([String: Any]) async throws -> Any + + private let handlers: [String: Handler] + + public init(handlers: [String: Handler]) { + self.handlers = handlers + } + + /// A missing handler or a throwing handler is reported as an unsuccessful + /// result rather than propagating, so one bad tool cannot abort the turn. + public func execute(request: HostToolRequest) async throws -> HostToolResult { + let started = Date() + + guard let handler = handlers[request.toolName] else { + return Self.failure( + request, + errorKind: "not_found", + message: "No host tool registered for '\(request.toolName)'", + started: started + ) + } + + do { + let value = try await handler(request.arguments ?? [:]) + var result = HostToolResult( + requestId: request.requestId, + toolCallId: request.toolCallId, + toolName: request.toolName, + success: true + ) + result.result = value + result.durationMs = Self.elapsedMs(started) + return result + } catch { + return Self.failure( + request, errorKind: "exception", message: "\(error)", started: started) + } + } + + private static func failure( + _ request: HostToolRequest, errorKind: String, message: String, started: Date + ) -> HostToolResult { + var result = HostToolResult( + requestId: request.requestId, + toolCallId: request.toolCallId, + toolName: request.toolName, + success: false + ) + result.result = ["message": message] + result.errorKind = errorKind + result.durationMs = elapsedMs(started) + return result + } + + private static func elapsedMs(_ started: Date) -> Double { + Date().timeIntervalSince(started) * 1000 + } +} diff --git a/runtime/swift/prompty/Sources/Prompty/JSONSchema.swift b/runtime/swift/prompty/Sources/Prompty/JSONSchema.swift new file mode 100644 index 000000000..2523b95ab --- /dev/null +++ b/runtime/swift/prompty/Sources/Prompty/JSONSchema.swift @@ -0,0 +1,179 @@ +import Foundation + +/// Raised when a portable `Property` cannot be represented safely by the +/// provider's JSON Schema subset. +import PromptyModel + +/// Projection of the portable `Property` model onto JSON Schema. +/// +/// This mirrors the Rust reference implementation exactly so that every runtime +/// produces byte-identical request bodies for the shared wire vectors. +public struct SchemaError: Error, CustomStringConvertible, Equatable { + public let message: String + + public init(_ message: String) { + self.message = message + } + + public var description: String { message } + + static let invalidUnion = SchemaError( + "UnionProperty must contain exactly one non-empty `oneOf` or `anyOf` array" + ) + + static let unsupportedOneOf = SchemaError( + "OpenAI schemas do not support UnionProperty.oneOf; use the provider-supported anyOf composition" + ) +} +public enum JSONSchema { + + /// Map a Agent property kind onto its JSON Schema type. + /// + /// Unrecognized kinds return `nil`; the caller then emits a bare `{}` so + /// provider-specific extension kinds degrade to "any value" rather than being + /// silently coerced to a string. + public static func jsonType(forKind kind: String) -> String? { + switch kind { + case "string": return "string" + case "integer": return "integer" + case "float", "number": return "number" + case "boolean": return "boolean" + case "array": return "array" + case "object": return "object" + default: return nil + } + } + + /// Convert one property into a recursive JSON Schema definition. + public static func schema(for property: Property, strict: Bool) throws -> [String: Any] { + var schema: [String: Any] = [:] + + if let type = jsonType(forKind: property.kindName) { + schema["type"] = type + } + if let description = property.propertyDescription, !description.isEmpty { + schema["description"] = description + } + if let values = property.enumValues { + schema["enum"] = values + } + + switch property.kindName { + case "array": + if let items = property.arrayItems, !(items.raw.isEmpty) { + schema["items"] = try JSONSchema.schema(for: items, strict: strict) + } + + case "object": + let children = property.objectProperties + if !children.isEmpty { + var nested: [String: Any] = [:] + var required: [String] = [] + for child in children where !child.name.isEmpty { + nested[child.name] = try JSONSchema.schema( + for: child, optional: !child.isRequired, strict: strict) + if strict || child.isRequired { required.append(child.name) } + } + schema["properties"] = nested + if !required.isEmpty { schema["required"] = required } + schema["additionalProperties"] = false + } + + case "union": + let oneOf = property.unionOneOf + let anyOf = property.unionAnyOf + switch (!oneOf.isEmpty, !anyOf.isEmpty) { + case (true, false): + throw SchemaError.unsupportedOneOf + case (false, true): + schema["anyOf"] = try anyOf.map { try JSONSchema.schema(for: $0, strict: strict) } + default: + throw SchemaError.invalidUnion + } + + default: + break + } + + if property.isNullable { + addNullability(&schema) + } + return schema + } + + /// Convert a property that sits in an optional position. + /// + /// In strict mode every declared key must appear in `required`, so optional + /// members express their optionality through a nullable type instead. + public static func schema( + for property: Property, optional: Bool, strict: Bool + ) throws -> [String: Any] { + var result = try schema(for: property, strict: strict) + if strict, optional, !property.isNullable { + addNullability(&result) + } + return result + } + + /// Widen a schema so it also accepts JSON `null`. + public static func addNullability(_ schema: inout [String: Any]) { + if let type = schema["type"] as? String { + schema["type"] = [type, "null"] + } else if var branches = schema["anyOf"] as? [Any] { + branches.append(["type": "null"]) + schema["anyOf"] = branches + } else if !schema.isEmpty { + // The branch is built from a snapshot taken *before* insertion, and the + // `anyOf` key is added to the existing map rather than replacing it, so + // siblings such as `description` survive and the `enum` widening below + // still sees the original values. + let snapshot = schema + schema["anyOf"] = [snapshot, ["type": "null"]] + } + + if var values = schema["enum"] as? [Any] { + if !values.contains(where: { $0 is NSNull }) { + values.append(NSNull()) + schema["enum"] = values + } + } + } + + /// Build the `parameters` object schema for a tool. + public static func parameters(_ properties: [Property], strict: Bool) throws -> [String: Any] { + var fields: [String: Any] = [:] + var required: [String] = [] + + for property in properties { + fields[property.name] = try schema( + for: property, optional: !property.isRequired, strict: strict) + if strict || property.isRequired { + required.append(property.name) + } + } + + var schema: [String: Any] = ["type": "object", "properties": fields] + if !required.isEmpty { schema["required"] = required } + return schema + } + + /// Build the strict object schema used for structured output. + /// + /// Structured output is always strict: every declared output is listed in + /// `required` and additional keys are rejected. + public static func outputs(_ properties: [Property]) throws -> [String: Any] { + var fields: [String: Any] = [:] + var required: [String] = [] + + for property in properties { + fields[property.name] = try schema( + for: property, optional: !property.isRequired, strict: true) + required.append(property.name) + } + + var schema: [String: Any] = ["type": "object", "properties": fields] + if !required.isEmpty { schema["required"] = required } + schema["additionalProperties"] = false + return schema + } +} diff --git a/runtime/swift/prompty/Sources/Prompty/JSONSupport.swift b/runtime/swift/prompty/Sources/Prompty/JSONSupport.swift new file mode 100644 index 000000000..0ab2e2ece --- /dev/null +++ b/runtime/swift/prompty/Sources/Prompty/JSONSupport.swift @@ -0,0 +1,161 @@ +/// Helpers for working with the loosely-typed `Any` values that the +/// Typra-generated model uses for free-form data (`metadata`, `inputs`, +/// raw provider payloads, and template values). +/// +/// The generated model is the canonical type layer, so the runtime deliberately +/// does not introduce a parallel `JSONValue` domain type. These functions supply +/// the few operations — truthiness, rendering, deep equality, path lookup — +/// that `Any` cannot provide on its own. +import Foundation + +public enum JSONSupport { + + /// Normalize a decoded value so that `NSNumber`/`NSNull` bridges behave + /// predictably regardless of whether the value came from JSON or YAML. + public static func normalize(_ value: Any?) -> Any? { + guard let value else { return nil } + if value is NSNull { return nil } + if let number = value as? NSNumber { + if isBool(number) { return number.boolValue } + let double = number.doubleValue + if double.rounded() == double && abs(double) < 9.007_199_254_740_992e15 { + return Int(double) + } + return double + } + if let dict = value as? [String: Any] { + var result: [String: Any] = [:] + for (key, element) in dict { + // A null-valued key is data, not absence: dropping it makes `raw_json` + // lossy and breaks equality against runtimes that preserve it. Arrays + // already keep their nulls, so keeping them here makes the two paths + // agree. + result[key] = normalize(element) ?? NSNull() + } + return result + } + if let array = value as? [Any] { + return array.map { normalize($0) ?? NSNull() } + } + return value + } + + private static func isBool(_ number: NSNumber) -> Bool { + // Foundation stores booleans as NSNumber; the encoded Objective-C type is + // the portable way to tell them apart from integers on every platform. + let encoding = String(cString: number.objCType) + return encoding == "c" || encoding == "B" + } + + /// Jinja/Mustache truthiness: `nil`, `false`, `0`, `""`, and empty + /// collections are falsy; everything else is truthy. + public static func isTruthy(_ value: Any?) -> Bool { + guard let value = normalize(value) else { return false } + switch value { + case let bool as Bool: return bool + case let int as Int: return int != 0 + case let double as Double: return double != 0 + case let string as String: return !string.isEmpty + case let array as [Any]: return !array.isEmpty + case let dict as [String: Any]: return !dict.isEmpty + case is NSNull: return false + default: return true + } + } + + /// Render a value the way a template engine would interpolate it. + /// + /// Missing values render as the empty string, integral doubles lose their + /// trailing `.0`, and containers fall back to compact JSON. + public static func stringify(_ value: Any?) -> String { + guard let value = normalize(value) else { return "" } + switch value { + case let string as String: return string + case let bool as Bool: return bool ? "true" : "false" + case let int as Int: return String(int) + case let double as Double: + if double.rounded() == double && abs(double) < 1e15 { + return String(Int(double)) + } + return String(double) + case is NSNull: return "" + default: + if let data = try? JSONSerialization.data( + withJSONObject: value, options: [.sortedKeys, .fragmentsAllowed]), + let text = String(data: data, encoding: .utf8) + { + return text + } + return String(describing: value) + } + } + + /// Look up a dotted path such as `user.name` or `items.0` in a value tree. + public static func lookup(_ path: String, in root: Any?) -> Any? { + var current = normalize(root) + for segment in path.split(separator: ".") { + guard let value = current else { return nil } + if let dict = value as? [String: Any] { + current = normalize(dict[String(segment)]) + } else if let array = value as? [Any], let index = Int(segment), + index >= 0, index < array.count + { + current = normalize(array[index]) + } else { + return nil + } + } + return current + } + + /// Structural equality across the `Any` values the model uses. + public static func equals(_ lhs: Any?, _ rhs: Any?) -> Bool { + let left = normalize(lhs) + let right = normalize(rhs) + switch (left, right) { + case (nil, nil): return true + case (nil, _), (_, nil): return false + default: break + } + if let l = left as? Bool, let r = right as? Bool { return l == r } + if let l = left as? String, let r = right as? String { return l == r } + if let l = numeric(left), let r = numeric(right) { return l == r } + if let l = left as? [Any], let r = right as? [Any] { + guard l.count == r.count else { return false } + return zip(l, r).allSatisfy { equals($0, $1) } + } + if let l = left as? [String: Any], let r = right as? [String: Any] { + guard l.count == r.count else { return false } + return l.allSatisfy { key, value in r[key] != nil && equals(value, r[key]) } + } + return false + } + + private static func numeric(_ value: Any?) -> Double? { + switch value { + case let int as Int: return Double(int) + case let double as Double: return double + default: return nil + } + } + + /// Parse a JSON string into normalized `Any`. + public static func parse(json: String) -> Any? { + guard let data = json.data(using: .utf8), + let value = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) + else { return nil } + return normalize(value) + } + + /// Serialize a value to compact JSON, or `"null"` when it cannot be encoded. + public static func toJSON(_ value: Any?) -> String { + guard let value = normalize(value) else { return "null" } + if let data = try? JSONSerialization.data( + withJSONObject: value, options: [.sortedKeys, .fragmentsAllowed]), + let text = String(data: data, encoding: .utf8) + { + return text + } + return "null" + } +} diff --git a/runtime/swift/prompty/Sources/Prompty/Jinja2Renderer.swift b/runtime/swift/prompty/Sources/Prompty/Jinja2Renderer.swift new file mode 100644 index 000000000..1f792d113 --- /dev/null +++ b/runtime/swift/prompty/Sources/Prompty/Jinja2Renderer.swift @@ -0,0 +1,326 @@ +import Foundation + +/// Renders the Jinja2 subset that Agent templates use. +/// +/// Supported: `{{ expr }}` with dotted paths and filters, `{% if %}` / +/// `{% else %}` / `{% endif %}`, `{% for x in xs %}` / `{% endfor %}`, and +/// `{# comments #}`. +/// +/// Prompt templates are not HTML, so output is never escaped and whitespace is +/// preserved exactly. An undefined variable renders as an empty string. +import PromptyModel + +// MARK: - Template syntax tree + +/// A parsed template node. + +/// Recursive-descent parser for the supported Jinja2 subset. +public struct Jinja2Renderer: Renderer { + + public init() {} + + /// Render the agent's template. + /// + /// Inputs arrive already prepared: the pipeline has replaced rich-kind values + /// with nonce placeholders. Preparing again here would mint a *second* nonce + /// and leave the pipeline expanding a placeholder that no longer appears in + /// the output, so this stage is deliberately a plain render. + public func render(agent: Agent, template: String, inputs: [String: Any]) async throws + -> String + { + try render(template: template, inputs: inputs) + } + + /// Render a raw template string. + public func render(template: String, inputs: [String: Any]) throws -> String { + var parser = TemplateParser(template) + let nodes = try parser.parseNodes(until: nil).nodes + var output = "" + try emit(nodes, scope: inputs, into: &output) + return output + } + + // MARK: - Evaluation + + private func emit(_ nodes: [Node], scope: [String: Any], into output: inout String) throws { + for node in nodes { + switch node { + case .text(let text): + output += text + + case .expression(let source): + output += JSONSupport.stringify(try evaluate(source, scope: scope)) + + case .conditional(let condition, let body, let alternate): + let value = try evaluate(condition, scope: scope) + try emit(JSONSupport.isTruthy(value) ? body : alternate, scope: scope, into: &output) + + case .loop(let variable, let source, let body): + let sequence = try evaluate(source, scope: scope) + for element in iterate(sequence) { + var inner = scope + inner[variable] = element + try emit(body, scope: inner, into: &output) + } + } + } + } + + private func iterate(_ value: Any?) -> [Any] { + switch value { + case let array as [Any]: return array + case let dict as [String: Any]: return dict.keys.sorted().map { $0 } + case let string as String: return string.map { String($0) } + default: return [] + } + } + + /// Evaluate a template expression. + /// + /// Delegates to ``ExpressionParser``, which understands comparisons, logical + /// operators, arithmetic, membership, indexing and filters. Anything outside + /// that grammar raises rather than silently resolving to `nil`. + private func evaluate(_ source: String, scope: [String: Any]) throws -> Any? { + try ExpressionParser.evaluate(source, scope: scope) { name, arguments, value in + try applyFilter(name: name, arguments: arguments, to: value) + } + } + + private func applyFilter(name: String, arguments: [Any?], to value: Any?) throws -> Any? { + switch name { + case "upper": + return JSONSupport.stringify(value).uppercased() + case "lower": + return JSONSupport.stringify(value).lowercased() + case "trim": + return JSONSupport.stringify(value).trimmingCharacters(in: .whitespacesAndNewlines) + case "length", "count": + switch value { + case let array as [Any]: return array.count + case let dict as [String: Any]: return dict.count + case let string as String: return string.count + case nil: return 0 + default: return JSONSupport.stringify(value).count + } + case "join": + let separator = arguments.first.flatMap { $0 as? String } ?? "" + let elements = iterate(value).map { JSONSupport.stringify($0) } + return elements.joined(separator: separator) + case "default", "d": + if value == nil || value is NSNull || (value as? String)?.isEmpty == true { + return arguments.first.flatMap { $0 } ?? "" + } + return value + case "first": + return iterate(value).first + case "last": + return iterate(value).last + case "reverse": + return iterate(value).reversed().map { $0 } + case "capitalize": + let string = JSONSupport.stringify(value) + guard let head = string.first else { return string } + return String(head).uppercased() + string.dropFirst().lowercased() + case "string": + return JSONSupport.stringify(value) + case "int": + if let int = value as? Int { return int } + if let double = value as? Double { return Int(double) } + return Int(JSONSupport.stringify(value)) ?? 0 + case "tojson", "to_json": + return JSONSupport.toJSON(value) + default: + throw InvokerError.parse("unsupported template filter '\(name)'") + } + } + + private func parseFilter(_ spec: String) -> (name: String, arguments: [String]) { + guard let open = spec.firstIndex(of: "("), spec.hasSuffix(")") else { + return (spec.trimmingCharacters(in: .whitespaces), []) + } + let name = String(spec[spec.startIndex.. [String] { + var parts: [String] = [] + var current = "" + var depth = 0 + var quote: Character? + + for character in text { + if let active = quote { + current.append(character) + if character == active { quote = nil } + continue + } + switch character { + case "\"", "'": + quote = character + current.append(character) + case "(", "[": + depth += 1 + current.append(character) + case ")", "]": + depth -= 1 + current.append(character) + case separator where depth == 0: + parts.append(current) + current = "" + default: + current.append(character) + } + } + parts.append(current) + return parts + } +} +enum Node { + case text(String) + case expression(String) + case conditional(condition: String, body: [Node], alternate: [Node]) + case loop(variable: String, source: String, body: [Node]) +} +struct TemplateParser { + private let characters: [Character] + private var index: Int = 0 + + init(_ template: String) { + self.characters = Array(template) + } + + /// Parse until one of `terminators` is reached. + /// + /// Returns the parsed nodes and the terminating tag, which is `nil` at end of + /// input. + mutating func parseNodes(until terminators: Set?) throws -> ( + nodes: [Node], terminator: String? + ) { + var nodes: [Node] = [] + var text = "" + + func flushText() { + if !text.isEmpty { + nodes.append(.text(text)) + text = "" + } + } + + while index < characters.count { + guard characters[index] == "{", index + 1 < characters.count else { + text.append(characters[index]) + index += 1 + continue + } + + switch characters[index + 1] { + case "{": + flushText() + nodes.append(.expression(try readDelimited(open: 2, close: "}}"))) + + case "#": + // Comments produce no output. + _ = try readDelimited(open: 2, close: "#}") + + case "%": + let tag = try readDelimited(open: 2, close: "%}") + let keyword = tag.split(separator: " ", maxSplits: 1).first.map(String.init) ?? tag + + if let terminators, terminators.contains(keyword) { + flushText() + return (nodes, tag) + } + + flushText() + switch keyword { + case "if": + nodes.append(try parseConditional(tag)) + case "for": + nodes.append(try parseLoop(tag)) + default: + throw InvokerError.parse("unsupported template tag '{% \(tag) %}'") + } + + default: + text.append(characters[index]) + index += 1 + } + } + + flushText() + if let terminators, !terminators.isEmpty { + throw InvokerError.parse( + "unclosed template block — expected {% \(terminators.sorted().joined(separator: " / ")) %}") + } + return (nodes, nil) + } + + private mutating func parseConditional(_ tag: String) throws -> Node { + let condition = String(tag.dropFirst("if".count)).trimmingCharacters(in: .whitespaces) + + let branch = try parseNodes(until: ["else", "elif", "endif"]) + guard let terminator = branch.terminator else { + throw InvokerError.parse("unclosed {% if %} block") + } + + let keyword = + terminator.split(separator: " ", maxSplits: 1).first.map(String.init) ?? terminator + switch keyword { + case "endif": + return .conditional(condition: condition, body: branch.nodes, alternate: []) + case "else": + let alternate = try parseNodes(until: ["endif"]) + guard alternate.terminator != nil else { + throw InvokerError.parse("unclosed {% if %} block") + } + return .conditional(condition: condition, body: branch.nodes, alternate: alternate.nodes) + default: + // `elif` is sugar for a nested conditional in the else branch. + let nested = try parseConditional("if" + terminator.dropFirst("elif".count)) + return .conditional(condition: condition, body: branch.nodes, alternate: [nested]) + } + } + + private mutating func parseLoop(_ tag: String) throws -> Node { + let expression = String(tag.dropFirst("for".count)).trimmingCharacters(in: .whitespaces) + let parts = expression.components(separatedBy: " in ") + guard parts.count == 2 else { + throw InvokerError.parse("malformed loop tag '{% \(tag) %}'") + } + + let body = try parseNodes(until: ["endfor"]) + guard body.terminator != nil else { + throw InvokerError.parse("unclosed {% for %} block") + } + + return .loop( + variable: parts[0].trimmingCharacters(in: .whitespaces), + source: parts[1].trimmingCharacters(in: .whitespaces), + body: body.nodes + ) + } + + /// Consume `open` characters, then everything up to `close`. + private mutating func readDelimited(open: Int, close: String) throws -> String { + index += open + let closing = Array(close) + let start = index + + while index < characters.count { + if characters[index] == closing[0], index + closing.count <= characters.count, + Array(characters[index.. String { + // Scalar-level check: a CRLF pair is one Character, so `contains("\r")` + // reports false for exactly the text this needs to catch. + guard text.unicodeScalars.contains("\r") else { return text } + + var out = String.UnicodeScalarView() + out.reserveCapacity(text.unicodeScalars.count) + + var scalars = text.unicodeScalars.makeIterator() + var pending = scalars.next() + while let scalar = pending { + guard scalar == "\r" else { + out.append(scalar) + pending = scalars.next() + continue + } + let next = scalars.next() + if next == "\n" { + out.append("\n") + pending = scalars.next() + } else { + out.append("\r") + pending = next + } + } + return String(out) + } + + /// Split on LF or CRLF, keeping empty lines. A lone CR stays as content. + /// + /// This is the template and transcript spelling: it mirrors the reference + /// runtime, which normalizes `\r\n` and then splits on `\n` alone. Blank lines + /// separate turns and paragraphs, so they must survive. + public static func splitLineFeeds(_ text: String) -> [String] { + scan(text, loneCarriageReturnTerminates: false, omittingEmpty: false) + } + + /// Split on CR, LF, or CRLF, and on nothing else. + /// + /// Empty lines are dropped. Used for line-delimited formats — JSONL journals + /// and server-sent events — where blank lines carry no record. Both formats + /// allow a bare CR to end a line, and neither can carry one as data: an + /// unescaped CR inside a JSON string is invalid JSON. + public static func split(_ text: String) -> [String] { + scan(text, loneCarriageReturnTerminates: true, omittingEmpty: true) + } + + private static func scan( + _ text: String, + loneCarriageReturnTerminates: Bool, + omittingEmpty: Bool + ) -> [String] { + var lines: [String] = [] + var current = String.UnicodeScalarView() + + func flush() { + let line = String(current) + current = String.UnicodeScalarView() + if !omittingEmpty || !line.isEmpty { lines.append(line) } + } + + var scalars = text.unicodeScalars.makeIterator() + var pending = scalars.next() + while let scalar = pending { + switch scalar { + case "\n": + flush() + pending = scalars.next() + case "\r": + // Look ahead one scalar: CRLF is a single terminator, never two. + let next = scalars.next() + if next == "\n" { + flush() + pending = scalars.next() + } else if loneCarriageReturnTerminates { + flush() + pending = next + } else { + current.append("\r") + pending = next + } + default: + current.append(scalar) + pending = scalars.next() + } + } + + // The trailing segment, which has no terminator after it. When empty lines + // are kept this reproduces `components(separatedBy:)`, which yields a final + // "" for text that ends in a terminator. + flush() + return lines + } +} diff --git a/runtime/swift/prompty/Sources/Prompty/Loader.swift b/runtime/swift/prompty/Sources/Prompty/Loader.swift new file mode 100644 index 000000000..36d801746 --- /dev/null +++ b/runtime/swift/prompty/Sources/Prompty/Loader.swift @@ -0,0 +1,231 @@ +import Foundation +/// Options controlling how a `.prompty` file is loaded. +import PromptyModel + +/// Loads `.prompty` files into the Typra-generated `Agent` model. + +/// Load a `.prompty` file. Shorthand for ``Loader/load(path:options:)``. + +/// Load a `.prompty` file asynchronously. +public struct LoadOptions { + /// Extra directories that `${file:...}` references may read from. The prompt + /// file's own directory is always permitted. + public var allowedFileRoots: [URL] + + public init(allowedFileRoots: [URL] = []) { + self.allowedFileRoots = allowedFileRoots + } + + public static let `default` = LoadOptions() +} +public enum Loader { + + /// Load a `.prompty` file from disk. + /// + /// Per the spec the runtime never auto-loads `.env` files — populating the + /// environment is the host application's responsibility. + public static func load( + path: String, + options: LoadOptions = .default + ) throws -> Agent { + let url = URL(fileURLWithPath: path) + guard FileManager.default.fileExists(atPath: url.path) else { + throw LoadError.fileNotFound(path: path, detail: "No such file") + } + let raw: String + do { + raw = try String(contentsOf: url, encoding: .utf8) + } catch { + throw LoadError.fileNotFound(path: path, detail: String(describing: error)) + } + return try build(raw: raw, filePath: url, options: options) + } + + /// Load a `.prompty` file without blocking the calling thread. + public static func loadAsync( + path: String, + options: LoadOptions = .default + ) async throws -> Agent { + try await Task.detached(priority: .userInitiated) { + try load(path: path, options: options) + }.value + } + + /// Load `.prompty` content that did not come from a file. + /// + /// `basePath` anchors `${file:...}` resolution. + public static func load( + contents: String, + basePath: String, + options: LoadOptions = .default + ) throws -> Agent { + try build(raw: contents, filePath: URL(fileURLWithPath: basePath), options: options) + } + + // MARK: - Pipeline + + private static func build( + raw: String, + filePath: URL, + options: LoadOptions + ) throws -> Agent { + // 1. Split frontmatter from the markdown body. `Frontmatter.split` owns line + // ending normalization and returns an LF body; normalizing here as well + // would run a second pass and swallow a lone CR in `\r\r\n`. + var (data, body) = try Frontmatter.split(raw) + + // 2. The body becomes `instructions`. Editors append trailing newlines, so + // trim the end only — leading and internal whitespace is significant. + let trimmedBody = trimTrailingNewlines(body) + if !trimmedBody.isEmpty { + data["instructions"] = trimmedBody + } + + // 3. A `.prompty` file always describes a prompt. + data["kind"] = Defaults.kind + + // 4. Resolve ${env:} / ${file:} before handing the tree to the model. + let agentDirectory = filePath.deletingLastPathComponent() + var tree: Any = data + try References.resolve( + &tree, agentDirectory: agentDirectory, allowedRoots: options.allowedFileRoots) + + guard var resolved = tree as? [String: Any] else { + throw LoadError.invalidFrontmatter("Frontmatter must be a YAML mapping") + } + + // 5. Normalize the shapes the schema accepts as shorthand. + normalizeModel(&resolved) + try normalizeProperties(&resolved, key: "inputs", path: "inputs") + try normalizeProperties(&resolved, key: "outputs", path: "outputs") + + // 6. Hand off to the generated model — the canonical type layer. + var agent: Agent + do { + agent = try Agent.load(resolved) + } catch { + throw LoadError.invalidModel(String(describing: error)) + } + + // 7. Record the source path so relative tool references can resolve later. + var metadata = agent.metadata ?? [:] + metadata[Defaults.sourcePathKey] = filePath.path + agent.metadata = metadata + + return agent + } + + private static func trimTrailingNewlines(_ text: String) -> String { + var result = Substring(text) + while let last = result.last, last == "\n" || last == "\r" { + result = result.dropLast() + } + return String(result) + } + + /// `model: gpt-4` is shorthand for `model: { id: gpt-4 }`. + private static func normalizeModel(_ data: inout [String: Any]) { + if let shorthand = data["model"] as? String { + data["model"] = ["id": shorthand] + } + } + + /// `inputs`/`outputs` accept three shapes. Normalize all of them to the + /// canonical list of named properties the model expects: + /// + /// - a list of properties (already canonical), + /// - a mapping of name to property object, + /// - a mapping of name to a scalar. + /// + /// The two scalar contracts are deliberately distinct: + /// + /// - **Named-collection shorthand** (`inputs: { city: Seattle }`): the key + /// supplies `name`, the scalar infers `kind`, and the scalar is stored as + /// `default`. `example` stays absent. + /// - **Direct property coercion** (a bare scalar as a *list* element) mirrors + /// the `@coerce` decorators on the TypeSpec `Property` model + /// (`#{ kind: "string", example: "{value}" }`) and stores `example`. + /// + /// An immediate array value is never a property in a name-keyed collection, + /// so it is rejected with its full dotted path. Arrays nested inside declared + /// property fields (`default`, `items`, `enumValues`) stay valid. + /// + /// The Swift emitter does not yet emit `@coerce` constructions for + /// polymorphic enums, so the loader performs the widening. + private static func normalizeProperties( + _ data: inout [String: Any], key: String, path: String + ) throws { + guard let value = data[key] else { return } + + if let list = value as? [Any] { + data[key] = try list.enumerated().map { index, element in + try normalizeProperty(element, path: "\(path)[\(index)]") + } + return + } + + guard let mapping = value as? [String: Any] else { return } + + data[key] = try mapping.keys.sorted().map { name -> Any in + let entryPath = "\(path).\(name)" + let raw = mapping[name] as Any + let normalized = JSONSupport.normalize(raw) + + // An immediate array can never be a named property entry. + if normalized is [Any] { + throw LoadError.invalidNamedCollectionEntry(path: entryPath, valueCategory: "array") + } + + // Named-collection scalar shorthand: key names it, scalar becomes `default`. + if !(normalized is [String: Any]) && !(raw is NSNull) { + return ["name": name, "kind": inferKind(raw), "default": raw] + } + + guard var property = try normalizeProperty(raw, path: entryPath) as? [String: Any] else { + return raw + } + property["name"] = name + return property + } + } + + /// Widen one property entry: dictionaries gain an inferred `kind`; bare + /// scalars become `{ kind: , example: }` per the direct + /// `@coerce` contract. + static func normalizeProperty(_ element: Any, path: String = "") throws -> Any { + if let dict = element as? [String: Any] { + var property = dict + if property["kind"] == nil { + property["kind"] = inferKind(property["default"] ?? property["example"]) + } + if let nested = property["properties"] { + var wrapper: [String: Any] = ["properties": nested] + try normalizeProperties(&wrapper, key: "properties", path: "\(path).properties") + property["properties"] = wrapper["properties"] + } + if let items = property["items"] { + property["items"] = try normalizeProperty(items, path: "\(path).items") + } + return property + } + if element is NSNull { return element } + return ["kind": inferKind(element), "example": element] + } + + private static func inferKind(_ value: Any?) -> String { + switch JSONSupport.normalize(value) { + case is Bool: return "boolean" + case is Int: return "integer" + case is Double: return "float" + case is [Any]: return "array" + case is [String: Any]: return "object" + default: return "string" + } + } +} +public func load(_ path: String, options: LoadOptions = .default) throws -> Agent { + try Loader.load(path: path, options: options) +} +public func loadAsync(_ path: String, options: LoadOptions = .default) async throws -> Agent { + try await Loader.loadAsync(path: path, options: options) +} diff --git a/runtime/swift/prompty/Sources/Prompty/ModelExtensions.swift b/runtime/swift/prompty/Sources/Prompty/ModelExtensions.swift new file mode 100644 index 000000000..c9132fe72 --- /dev/null +++ b/runtime/swift/prompty/Sources/Prompty/ModelExtensions.swift @@ -0,0 +1,242 @@ +import Foundation + +// Convenience accessors over the Typra-generated model. +// +// These are extensions only — the generated types remain the single canonical +// domain layer. Nothing here redefines or shadows a generated type. + +import PromptyModel + +extension Property { + /// The property's raw frontmatter dictionary. + /// + /// Scalar kinds are preserved verbatim by the generated `Property.unknown` + /// case; the three declared subtypes round-trip through `save()`. + public var raw: [String: Any] { + switch self { + case .unknown(let dict): return dict + default: return (try? save()) ?? [:] + } + } + + /// The declared property name, or `""` when unnamed. + public var name: String { raw["name"] as? String ?? "" } + + /// The property's `kind` discriminator (`string`, `integer`, `thread`, ...). + public var kindName: String { raw["kind"] as? String ?? "" } + + /// A human-readable description, when declared. + public var propertyDescription: String? { raw["description"] as? String } + + /// Whether the property must be supplied by the caller. + public var isRequired: Bool { JSONSupport.isTruthy(raw["required"]) } + + /// The declared default, used to fill omitted inputs. + public var defaultValue: Any? { JSONSupport.normalize(raw["default"]) } + + /// The declared example. Examples are documentation only and are never used + /// to fill an omitted input. + public var exampleValue: Any? { JSONSupport.normalize(raw["example"]) } + + /// Allowed enumeration values, when declared. + public var enumValues: [Any]? { + guard let values = raw["enumValues"] as? [Any], !values.isEmpty else { return nil } + return values.map { JSONSupport.normalize($0) ?? NSNull() } + } + + /// Whether the property also accepts a JSON `null`. + public var isNullable: Bool { JSONSupport.isTruthy(raw["nullable"]) } + + /// The element schema of an `array` property, when declared. + public var arrayItems: Property? { + if case .arrayProperty(let array) = self { return array.items } + guard let items = raw["items"] else { return nil } + return try? Property.load(items) + } + + /// The child properties of an `object` property. + public var objectProperties: [Property] { + if case .objectProperty(let object) = self { return object.properties } + guard let nested = raw["properties"] as? [Any] else { return [] } + // `try?` deliberately drops a structurally invalid child: `build()` already + // rejects those upstream, so this only fires for hand-built `Property` + // values that never went through the loader. + return nested.compactMap { try? Property.load(Loader.normalizeProperty($0)) } + } + + /// The `oneOf` branches of a `union` property, when declared. + public var unionOneOf: [Property] { + if case .unionProperty(let union) = self { return union.oneOf ?? [] } + guard let branches = raw["oneOf"] as? [Any] else { return [] } + return branches.compactMap { try? Property.load($0) } + } + + /// The `anyOf` branches of a `union` property, when declared. + public var unionAnyOf: [Property] { + if case .unionProperty(let union) = self { return union.anyOf ?? [] } + guard let branches = raw["anyOf"] as? [Any] else { return [] } + return branches.compactMap { try? Property.load($0) } + } +} +extension Tool { + /// The tool's raw dictionary form. + public var raw: [String: Any] { (try? save()) ?? [:] } + + /// The declared tool name. + public var name: String { raw["name"] as? String ?? "" } + + /// The tool's `kind` discriminator (`function`, `mcp`, `openapi`, ...). + public var kindName: String { raw["kind"] as? String ?? "" } + + /// A human-readable description, when declared. + public var toolDescription: String? { + guard let value = raw["description"] as? String, !value.isEmpty else { return nil } + return value + } + + /// The tool's declared bindings, whichever concrete kind it is. + /// + /// `bindings` is inherited by every tool kind, but the generated `Tool` enum + /// reaches it only through its payload, so the switch lives here once. + public var bindings: [Binding] { + switch self { + case .functionTool(let tool): return tool.bindings ?? [] + case .mcpTool(let tool): return tool.bindings ?? [] + case .openApiTool(let tool): return tool.bindings ?? [] + case .promptyTool(let tool): return tool.bindings ?? [] + case .customTool(let tool, _): return tool.bindings ?? [] + } + } + + /// Parameter names bound to inputs. Bound parameters are stripped from the + /// schema sent to the provider — the runtime supplies them instead. + /// + /// An unnamed binding targets no parameter, so it strips nothing; that is the + /// same binding ``Pipeline/applyBindings(_:toolName:arguments:inputs:)`` + /// declines to inject, and the two must agree or a parameter would be removed + /// from the schema and never restored. + public var boundParameterNames: Set { + Set(bindings.map(\.name).filter { !$0.isEmpty }) + } + + /// The declared function parameters, for `function` tools. + public var functionParameters: [Property] { + if case .functionTool(let tool) = self { return tool.parameters } + return [] + } + + /// Whether the tool declares strict schema adherence. + public var isStrict: Bool { + if case .functionTool(let tool) = self { return tool.strict ?? false } + return false + } +} +extension Agent { + /// The renderer key: `template.format.kind`, defaulting to `jinja2`. + /// + /// The generated model defaults `kind` to the wildcard sentinel `"*"`, which + /// means "unconstrained" in the schema and is never a registry key. A prompt + /// that sets only one side of `template` therefore leaves the other holding + /// `"*"`, which must resolve to the runtime default rather than fail lookup. + public var formatKind: String { + Self.resolveKind(template?.format.kind, default: Defaults.templateFormat) + } + + /// The parser key: `template.parser.kind`, defaulting to `prompty`. + public var parserKind: String { + Self.resolveKind(template?.parser.kind, default: Defaults.parser) + } + + private static func resolveKind(_ kind: String?, default fallback: String) -> String { + guard let kind, !kind.isEmpty, kind != "*" else { return fallback } + return kind + } + + /// The executor/processor key: `model.provider`, defaulting to `openai`. + public var providerKind: String { + guard let provider = model?.provider, !provider.isEmpty else { return Defaults.provider } + return provider + } + + /// The API surface to call: `model.apiType`, defaulting to `chat`. + public var apiTypeName: String { + let name = model?.apiType?.rawValue ?? "" + return name.isEmpty ? "chat" : name + } + + /// The configured model id, or an empty string when no `model` block is present. + public var modelId: String { model?.id ?? "" } + + /// The configured model options, when a `model` block is present. + public var modelOptions: ModelOptions? { model?.options } + + /// Whether the prompt asks the provider to stream its response. + public var isStreaming: Bool { + JSONSupport.isTruthy(model?.options?.additionalProperties?["stream"]) + } + + /// Declared inputs, or an empty list. + public var inputProperties: [Property] { inputs ?? [] } + + /// Declared outputs, or an empty list. + public var outputProperties: [Property] { outputs ?? [] } + + /// Whether this prompt declares a structured output schema. + public var hasStructuredOutputs: Bool { !(outputs ?? []).isEmpty } + + /// The absolute path this prompt was loaded from, when known. + public var sourcePath: String? { metadata?[Defaults.sourcePathKey] as? String } +} +extension Message { + /// Build a single-text-part message. + public static func withText(_ role: Role, _ text: String) -> Message { + Message(role: role, parts: [.textPart(TextPart(kind: "text", value: text))]) + } + + /// Build a `tool` role message carrying a tool call result. + public static func toolResult(toolCallId: String, result: String) -> Message { + Message( + role: .tool, + parts: [.textPart(TextPart(kind: "text", value: result))], + metadata: ["tool_call_id": toolCallId] + ) + } + + /// All text parts concatenated. Non-text parts are ignored. + public var textContent: String { + parts.compactMap { part -> String? in + if case .textPart(let text) = part { return text.value } + return nil + }.joined() + } + + /// Wire-shaped content: a plain string when every part is text (joined by + /// newline), otherwise `nil` so the caller emits typed content blocks. + public var plainTextWireContent: String? { + guard !hasRichContent else { return nil } + return parts.compactMap { part -> String? in + if case .textPart(let text) = part { return text.value } + return nil + }.joined(separator: "\n") + } + + /// Whether the message carries any non-text content part. + public var hasRichContent: Bool { + parts.contains { part in + if case .textPart = part { return false } + return true + } + } +} +extension ContentPart { + /// Build a text content part. + public static func text(_ value: String) -> ContentPart { + .textPart(TextPart(kind: "text", value: value)) + } +} +extension Role { + /// Parse a role string, returning `nil` rather than throwing. + public static func parseOptional(_ value: String) -> Role? { + try? Role.parse(value.lowercased()) + } +} diff --git a/runtime/swift/prompty/Sources/Prompty/MustacheRenderer.swift b/runtime/swift/prompty/Sources/Prompty/MustacheRenderer.swift new file mode 100644 index 000000000..4a2dfe85c --- /dev/null +++ b/runtime/swift/prompty/Sources/Prompty/MustacheRenderer.swift @@ -0,0 +1,184 @@ +import Foundation + +/// Renders Mustache templates. +/// +/// Supported: `{{name}}` interpolation with dotted paths, `{{#section}}` +/// sections (truthy value, or iteration when the value is a list), `{{^section}}` +/// inverted sections, `{{.}}` implicit iteration context, and `{{! comment }}`. +/// +/// As with Jinja2, output is never HTML-escaped — `{{{triple}}}` and `{{&raw}}` +/// are accepted but behave identically to `{{double}}`. +import PromptyModel + +// MARK: - Template syntax tree + +public struct MustacheRenderer: Renderer { + + public init() {} + + /// Render the agent's template. + /// + /// Inputs arrive already prepared by the pipeline; see the note on + /// ``Jinja2Renderer/render(agent:template:inputs:)``. + public func render(agent: Agent, template: String, inputs: [String: Any]) async throws + -> String + { + try render(template: template, inputs: inputs) + } + + /// Render a raw template string. + public func render(template: String, inputs: [String: Any]) throws -> String { + var scanner = Scanner(template) + let nodes = try scanner.parse(closing: nil).nodes + var output = "" + emit(nodes, stack: [inputs], into: &output) + return output + } + + // MARK: - Evaluation + + private func emit(_ nodes: [MustacheNode], stack: [Any], into output: inout String) { + for node in nodes { + switch node { + case .text(let text): + output += text + + case .variable(let name): + output += JSONSupport.stringify(resolve(name, stack: stack)) + + case .section(let name, let body): + let value = resolve(name, stack: stack) + if let array = value as? [Any] { + for element in array { + emit(body, stack: stack + [element], into: &output) + } + } else if JSONSupport.isTruthy(value) { + emit(body, stack: stack + [value as Any], into: &output) + } + + case .inverted(let name, let body): + let value = resolve(name, stack: stack) + let isEmpty = (value as? [Any])?.isEmpty ?? !JSONSupport.isTruthy(value) + if isEmpty { + emit(body, stack: stack, into: &output) + } + } + } + } + + /// Look up a name against the context stack, innermost frame first. + private func resolve(_ name: String, stack: [Any]) -> Any? { + // `.` refers to the current context itself. + if name == "." { return stack.last } + + for frame in stack.reversed() { + guard let dictionary = frame as? [String: Any] else { continue } + if let value = JSONSupport.lookup(name, in: dictionary) { return value } + } + return nil + } +} +enum MustacheNode { + case text(String) + case variable(String) + case section(name: String, body: [MustacheNode]) + case inverted(name: String, body: [MustacheNode]) +} +private struct Scanner { + private let characters: [Character] + private var index = 0 + + init(_ template: String) { + self.characters = Array(template) + } + + /// Parse until `{{/closing}}` is reached, or end of input when `closing` is nil. + mutating func parse(closing: String?) throws -> (nodes: [MustacheNode], closed: Bool) { + var nodes: [MustacheNode] = [] + var text = "" + + func flushText() { + if !text.isEmpty { + nodes.append(.text(text)) + text = "" + } + } + + while index < characters.count { + guard characters[index] == "{", index + 1 < characters.count, characters[index + 1] == "{" + else { + text.append(characters[index]) + index += 1 + continue + } + + // `{{{raw}}}` uses a three-character close. + let isTriple = index + 2 < characters.count && characters[index + 2] == "{" + let tag = try readTag(triple: isTriple) + + guard let sigil = tag.first else { + flushText() + nodes.append(.variable("")) + continue + } + + let name = String(tag.dropFirst()).trimmingCharacters(in: .whitespaces) + + switch sigil { + case "!": + // Comment — no output. + continue + + case "#", "^": + flushText() + let inner = try parse(closing: name) + guard inner.closed else { + throw InvokerError.parse("unclosed mustache section '{{#\(name)}}'") + } + nodes.append( + sigil == "#" + ? .section(name: name, body: inner.nodes) + : .inverted(name: name, body: inner.nodes)) + + case "/": + flushText() + guard let closing, closing == name else { + throw InvokerError.parse("unexpected mustache close tag '{{/\(name)}}'") + } + return (nodes, true) + + case "&": + flushText() + nodes.append(.variable(name)) + + default: + flushText() + nodes.append(.variable(tag.trimmingCharacters(in: .whitespaces))) + } + } + + flushText() + if closing != nil { + throw InvokerError.parse("unclosed mustache section '{{#\(closing!)}}'") + } + return (nodes, false) + } + + private mutating func readTag(triple: Bool) throws -> String { + let close = Array(triple ? "}}}" : "}}") + index += triple ? 3 : 2 + let start = index + + while index < characters.count { + if characters[index] == "}", index + close.count <= characters.count, + Array(characters[index.. [String: Any] { + var result = inputs + + for property in agent.inputProperties { + let name = property.name + guard !name.isEmpty, result[name] == nil else { continue } + + if let fallback = property.defaultValue { + result[name] = fallback + } else if property.isRequired { + throw InvokerError.validation("Missing required input: \"\(name)\"") + } + } + + return result + } + + // MARK: - Stages + + /// Render the prompt's instructions with the supplied inputs. + public static func render( + _ agent: Agent, + inputs: [String: Any] = [:], + registry: Registry = .shared + ) async throws -> String { + try await renderWithNonces(agent, inputs: inputs, registry: registry).rendered + } + + /// Parse rendered text into messages. + /// + /// `context` carries the strict-mode nonce produced by the parser's + /// `preRender`. Passing `nil` parses without injection validation. + public static func parse( + _ agent: Agent, + rendered: String, + context: [String: Any]? = nil, + registry: Registry = .shared + ) async throws -> [Message] { + registry.registerDefaults() + return try await registry.parser(for: agent.parserKind) + .parse(agent: agent, rendered: rendered, context: context) + } + + /// Render, parse, and expand rich-kind inputs into real messages. + /// + /// With strict mode on (the default) the parser stamps every role marker in + /// the template with a nonce before rendering, then requires that nonce when + /// parsing. Role markers that appear only after interpolation therefore fail + /// validation instead of silently becoming new turns. + public static func prepare( + _ agent: Agent, + inputs: [String: Any] = [:], + registry: Registry = .shared + ) async throws -> [Message] { + registry.registerDefaults() + + let validated = try validateInputs(agent, inputs: inputs) + let parser = try registry.parser(for: agent.parserKind) + + var target = agent + var context: [String: Any]? + + if isStrict(agent) { + let result = try parser.preRender(template: agent.instructions ?? "") + if let preRendered = result as? PreRenderResult { + target.instructions = preRendered.text + context = preRendered.context + } else if result != nil { + // Silently discarding an unrecognized result would disable strict + // nonce validation — exactly the protection strict mode exists for. + throw InvokerError.parse( + "parser '\(agent.parserKind)' returned an unsupported pre-render result;" + + " strict mode requires a PreRenderResult") + } + } + + let render = try await renderWithNonces(target, inputs: validated, registry: registry) + let messages = try await parser.parse( + agent: agent, rendered: render.rendered, context: context) + + return expandThreads(messages, nonces: render.nonces, inputs: validated) + } + + /// Normalize a provider response. + public static func process( + _ agent: Agent, + response: Any, + registry: Registry = .shared + ) async throws -> Any? { + let processed = try await registry.processor(for: agent.providerKind) + .process(agent: agent, response: response) + return Structured.wrapIfNeeded(agent, result: processed) + } + + /// Execute prepared messages and process the response. + /// + /// When the prompt asks for streaming, the stream is consumed here and its + /// text accumulated, so `run` always answers with a complete result. Routing + /// a streaming response through the non-streaming decoder would hand SSE + /// frames to a JSON parser. + public static func run( + _ agent: Agent, + messages: [Message], + registry: Registry = .shared + ) async throws -> Any? { + if isStreaming(agent) { + do { + return try await accumulate( + try await stream(agent, messages: messages, registry: registry)) + } catch { + // A provider that cannot stream this request still has to answer, so + // fall through to the buffered path rather than failing the call. + } + } + + let executor = try registry.executor(for: agent.providerKind) + let response = try await executor.execute(agent: agent, messages: messages) + return Structured.unwrap(try await process(agent, response: response, registry: registry)) + } + + /// Whether the prompt asked for a streamed response. + static func isStreaming(_ agent: Agent) -> Bool { + guard let value = agent.model?.options?.additionalProperties?["stream"] else { return false } + return (JSONSupport.normalize(value) as? Bool) ?? false + } + + /// Drain a chunk stream into the text it represents. + static func accumulate(_ stream: ChunkStream) async throws -> String { + var text = "" + for try await chunk in stream { + if case .textChunk(let part) = chunk { + text += part.value + } + } + return text + } + + /// Run the full pipeline for a loaded prompt. + public static func invoke( + _ agent: Agent, + inputs: [String: Any] = [:], + registry: Registry = .shared + ) async throws -> Any? { + let messages = try await prepare(agent, inputs: inputs, registry: registry) + return try await run(agent, messages: messages, registry: registry) + } + + /// Run the full pipeline for a prompt on disk. + public static func invoke( + path: String, + inputs: [String: Any] = [:], + options: LoadOptions = .default, + registry: Registry = .shared + ) async throws -> Any? { + let agent = try Loader.load(path: path, options: options) + return try await invoke(agent, inputs: inputs, registry: registry) + } + + /// Stream a prepared conversation, yielding decoded chunks as the provider + /// emits them. + public static func stream( + _ agent: Agent, + messages: [Message], + registry: Registry = .shared + ) async throws -> ChunkStream { + let raw = try await registry.executor(for: agent.providerKind) + .executeStream(agent: agent, messages: messages) + let decoded = try await registry.processor(for: agent.providerKind).processStream(stream: raw) + + guard let stream = decoded as? ChunkStream else { + throw InvokerError.execution("processor did not return a decoded chunk stream") + } + return stream + } + + // MARK: - Tool turns + + /// One assistant turn that requested tools, plus the results being reported + /// back to the model. + /// + /// Appending these to the conversation and calling ``run(_:messages:registry:)`` + /// again continues the exchange. + public static func toolMessages( + _ agent: Agent, + rawResponse: Any = [:] as [String: Any], + toolCalls: [ToolCall], + toolResults: [String], + textContent: String? = nil, + registry: Registry = .shared + ) throws -> [Message] { + try registry.executor(for: agent.providerKind) + .formatToolMessages( + rawResponse: rawResponse, + toolCalls: toolCalls, + toolResults: toolResults, + textContent: textContent + ) + } + + /// Read the tool calls out of a processed provider result. + /// + /// Processors project tool calls as a list of `{ id, name, arguments }` + /// dictionaries; anything else means the model answered with content. + public static func toolCalls(in result: Any?) -> [ToolCall] { + guard let entries = result as? [Any], !entries.isEmpty else { return [] } + + let calls = entries.compactMap { entry -> ToolCall? in + guard let dict = entry as? [String: Any], + let name = dict["name"] as? String, !name.isEmpty + else { return nil } + return ToolCall( + id: dict["id"] as? String ?? "", + name: name, + arguments: dict["arguments"] as? String ?? "" + ) + } + return calls.count == entries.count ? calls : [] + } + + // MARK: - Tool bindings + + /// Inject bound parameters into a tool call's arguments (spec §9.6). + /// + /// Bindings are the second half of a two-part contract. Wire conversion + /// strips every bound parameter from the schema sent to the model (§2.9.1.1), + /// so the model never sees — and never supplies — them. This restores those + /// parameters from the prompt's own inputs before the tool runs. Without it a + /// bound parameter is stripped and never replaced, and the tool is invoked + /// with an argument missing. + /// + /// Each binding names a parameter (`binding.name`) and the input to read it + /// from (`binding.input`). A binding whose input is absent is skipped rather + /// than injected as null, so a partially supplied set of inputs degrades to + /// the arguments the model provided instead of failing the call. + /// + /// Bindings take precedence over anything the model produced for the same + /// parameter (§2.9.1.3) — that is the point of binding it. + /// + /// Unknown tool names and tools without bindings pass through untouched, so + /// this is safe to call unconditionally on every tool call. + public static func applyBindings( + _ agent: Agent, + toolName: String, + arguments: [String: Any], + inputs: [String: Any] + ) -> [String: Any] { + guard let tool = agent.tools?.first(where: { $0.name == toolName }) else { return arguments } + + let bindings = tool.bindings + guard !bindings.isEmpty else { return arguments } + + var merged = arguments + for binding in bindings where !binding.name.isEmpty { + guard let value = inputs[binding.input] else { continue } + merged[binding.name] = value + } + return merged + } + + /// ``applyBindings(_:toolName:arguments:inputs:)`` for a decoded tool call. + /// + /// Call this at dispatch time, on the arguments handed to the tool — not on + /// the recorded call. `ToolCall` is deliberately left untouched: a bound value + /// is hidden from the model on purpose (it may be a user id, a tenant, or a + /// credential), and rewriting the call would feed that value straight back + /// into the assistant tool-call history that ``toolMessages(_:results:)`` + /// sends on the next round. + /// + /// Bindings apply only when the payload is a JSON object, matching the + /// reference implementation: an array, a scalar, or malformed JSON is not an + /// argument object, so it is passed through rather than replaced by one that + /// contains only the bound values. + public static func boundArguments( + _ agent: Agent, + call: ToolCall, + inputs: [String: Any] + ) -> [String: Any] { + let decoded = call.argumentValues + guard isArgumentObject(call.arguments) else { return decoded } + return applyBindings(agent, toolName: call.name, arguments: decoded, inputs: inputs) + } + + /// Whether a raw arguments payload is a JSON object bindings may be added to. + /// + /// Providers send an empty payload for a call with no arguments, which is an + /// empty object in every meaningful sense — and is exactly the case where a + /// tool's only parameters are bound ones. + private static func isArgumentObject(_ raw: String) -> Bool { + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.isEmpty { return true } + return JSONSupport.parse(json: trimmed) is [String: Any] + } + + // MARK: - Internals + + /// Strict mode defaults to on — role-marker injection is the risk being + /// mitigated, so it must be opted out of explicitly. + static func isStrict(_ agent: Agent) -> Bool { + agent.template?.format.strict ?? true + } + + static func renderWithNonces( + _ agent: Agent, + inputs: [String: Any], + registry: Registry + ) async throws -> (rendered: String, nonces: [String: String]) { + registry.registerDefaults() + + let validated = try validateInputs(agent, inputs: inputs) + let (prepared, nonces) = RenderCommon.prepareRenderInputs(agent, inputs: validated) + + // Nonce substitution happens here and only here. Renderers receive the + // already-substituted inputs so the nonces recorded above are exactly the + // ones that survive into the rendered text for `expandThreads` to find. + let rendered = try await registry.renderer(for: agent.formatKind) + .render(agent: agent, template: agent.instructions ?? "", inputs: prepared) + + return (rendered, nonces) + } + + /// Replace nonce placeholders with the messages they stand for. + /// + /// A message whose text contains a placeholder is split into the text before + /// it, the expanded thread messages, and the text after it. + static func expandThreads( + _ messages: [Message], + nonces: [String: String], + inputs: [String: Any] + ) -> [Message] { + guard !nonces.isEmpty else { return messages } + + var nonceToName: [String: String] = [:] + for (name, nonce) in nonces { nonceToName[nonce] = name } + + var result: [Message] = [] + + for message in messages { + var expanded = false + + for part in message.parts { + guard case .textPart(let textPart) = part else { continue } + + for (nonce, name) in nonceToName { + guard let range = textPart.value.range(of: nonce) else { continue } + + let before = String(textPart.value[.. [Message] { + let entries: [Any] + switch value { + case let array as [Any]: + entries = array + case let dict as [String: Any]: + entries = dict["messages"] as? [Any] ?? [] + default: + return [] + } + + return entries.compactMap { entry in + guard + let dict = entry as? [String: Any], + let roleName = dict["role"] as? String, + let role = Role.parseOptional(roleName) + else { return nil } + return .withText(role, threadText(dict["content"])) + } + } + + /// Flatten a thread entry's `content` into plain text. + static func threadText(_ content: Any?) -> String { + switch content { + case let text as String: + return text + case let parts as [Any]: + return + parts + .compactMap { part -> String? in + guard let part = part as? [String: Any] else { return part as? String } + // `kind` is absent on some transcript shapes; treat those as text. + let kind = part["kind"] as? String ?? "text" + guard kind == "text" else { return nil } + return part["value"] as? String ?? part["text"] as? String + } + .joined() + default: + return JSONSupport.stringify(content) + } + } +} +public func render(_ agent: Agent, inputs: [String: Any] = [:]) async throws -> String { + try await Pipeline.render(agent, inputs: inputs) +} +public func prepare(_ agent: Agent, inputs: [String: Any] = [:]) async throws -> [Message] { + try await Pipeline.prepare(agent, inputs: inputs) +} +public func run(_ agent: Agent, messages: [Message]) async throws -> Any? { + try await Pipeline.run(agent, messages: messages) +} +public func invoke(_ agent: Agent, inputs: [String: Any] = [:]) async throws -> Any? { + try await Pipeline.invoke(agent, inputs: inputs) +} +public func invoke(path: String, inputs: [String: Any] = [:]) async throws -> Any? { + try await Pipeline.invoke(path: path, inputs: inputs) +} +public func boundArguments( + _ agent: Agent, call: ToolCall, inputs: [String: Any] = [:] +) -> [String: Any] { + Pipeline.boundArguments(agent, call: call, inputs: inputs) +} +public func registerDefaults() { + Registry.shared.registerDefaults() +} diff --git a/runtime/swift/prompty/Sources/Prompty/PromptyChatParser.swift b/runtime/swift/prompty/Sources/Prompty/PromptyChatParser.swift new file mode 100644 index 000000000..7a2df713b --- /dev/null +++ b/runtime/swift/prompty/Sources/Prompty/PromptyChatParser.swift @@ -0,0 +1,243 @@ +import Foundation + +/// Splits rendered text at role markers into messages. +/// +/// A role marker is a line that consists solely of `system:`, `user:`, or +/// `assistant:` — optionally preceded by whitespace and a `#`, and optionally +/// carrying an attribute block such as `system[nonce="abc"]:`. Case is ignored. +/// `developer:` is deliberately not a role marker. +/// +/// # Strict mode +/// +/// ``preRender(_:text:)`` stamps every marker with a random per-render nonce +/// before the template engine runs. ``parse(_:text:context:)`` then requires +/// that nonce on every marker it finds. A role marker that appears in rendered +/// output but not in the original template can only have come from interpolated +/// input, so it will lack the nonce and be rejected. That closes the prompt +/// injection path where a template variable smuggles in `system:`. +/// +/// Registered under the key `prompty`. +import PromptyModel + +public struct PromptyChatParser: Parser { + + public init() {} + + // A role marker occupying an entire line, with an optional attribute block. + private static let boundary = try! NSRegularExpression( + pattern: #"^\s*#?\s*(system|user|assistant)(\[(\w+\s*=\s*"?[^"]*"?\s*,?\s*)+\])?\s*:\s*$"#, + options: [.caseInsensitive] + ) + + // A single `key=value` pair inside an attribute block. + private static let attribute = try! NSRegularExpression( + pattern: #"(\w+)\s*=\s*"?([^",\]]*)"?"# + ) + + // MARK: - Parser + + /// Split a template or transcript into lines, tolerating Windows endings. + /// + /// The loader normalizes what it reads from disk, but a prompt built in + /// memory — `Agent.load` from a dictionary, or a template assembled by a + /// host — never passes through it. Swift treats a CRLF pair as a single + /// grapheme, so splitting such text on "\n" yields no split at all and every + /// role marker is missed. `Lines` scans scalars, which never cluster. + static func splitLines(_ text: String) -> [String] { + Lines.splitLineFeeds(text) + } + + public func preRender(template: String) throws -> Any? { + let nonce = Self.generateNonce() + let sanitized = + Self.splitLines(template) + .map { line -> String in + let trimmed = line.trimmingCharacters(in: .whitespaces) + guard let match = Self.matchBoundary(trimmed) else { return line } + return "\(match.role)[\(Self.nonceAttribute)=\"\(nonce)\"]:\n" + } + .joined(separator: "\n") + + return PreRenderResult(text: sanitized, context: [Self.nonceAttribute: nonce]) + } + + public func parse(agent: Agent, rendered: String, context: [String: Any]?) async throws + -> [Message] + { + try Self.parseChat(rendered, expectedNonce: context?[Self.nonceAttribute] as? String) + } + + /// Reserved attribute key carrying the strict-mode nonce. + /// + /// Reserved in two senses: its value is never type-coerced (see + /// ``parseAttributes(_:)``), and it is stripped from message metadata rather + /// than surfaced to callers. + static let nonceAttribute = "nonce" + + // MARK: - Parsing + + /// Parse without nonce validation. + public static func parseChat(_ text: String) -> [Message] { + (try? parseChat(text, expectedNonce: nil)) ?? [] + } + + /// Parse, validating nonces when `expectedNonce` is supplied. + public static func parseChat(_ text: String, expectedNonce: String?) throws -> [Message] { + var messages: [Message] = [] + var currentRole: Role = .system + var contentLines: [String] = [] + var currentAttributes: [String: Any] = [:] + var hasRoleMarker = false + + for line in Self.splitLines(text) { + let trimmed = line.trimmingCharacters(in: .whitespaces) + + guard let match = matchBoundary(trimmed) else { + // Accumulate the original line — indentation inside a turn matters. + contentLines.append(line) + continue + } + + // A marker closes the turn before it. The `hasRoleMarker` guard keeps an + // empty turn (`system:` immediately followed by `user:`) while suppressing + // a phantom leading message when the text opens with a marker. + if !contentLines.isEmpty || hasRoleMarker { + messages.append( + try buildMessage( + role: currentRole, + content: joinAndTrim(contentLines), + attributes: currentAttributes, + expectedNonce: hasRoleMarker ? expectedNonce : nil + )) + contentLines.removeAll() + currentAttributes = [:] + } + + currentRole = Role.parseOptional(match.role) ?? .system + if let block = match.attributes { + currentAttributes = parseAttributes(block) + } + hasRoleMarker = true + } + + if !contentLines.isEmpty || hasRoleMarker { + messages.append( + try buildMessage( + role: currentRole, + content: joinAndTrim(contentLines), + attributes: currentAttributes, + expectedNonce: hasRoleMarker ? expectedNonce : nil + )) + } + + return messages + } + + private static func buildMessage( + role: Role, + content: String, + attributes: [String: Any], + expectedNonce: String? + ) throws -> Message { + if let expected = expectedNonce { + let actual = JSONSupport.stringify(attributes[Self.nonceAttribute]) + guard actual == expected else { + throw InvokerError.parse( + """ + Nonce mismatch — possible prompt injection detected (strict mode is \ + enabled). A template variable may be injecting role markers. + """ + ) + } + } + + // The nonce is a transport detail; everything else becomes metadata. + var metadata = attributes + metadata.removeValue(forKey: Self.nonceAttribute) + + return Message(role: role, parts: [.text(content)], metadata: metadata) + } + + // MARK: - Helpers + + /// Match a role marker, returning the role and raw attribute block. + static func matchBoundary(_ line: String) -> (role: String, attributes: String?)? { + let range = NSRange(line.startIndex.. 2, let attrRange = Range(match.range(at: 2), in: line) { + attributes = String(line[attrRange]) + } + return (role, attributes) + } + + /// Extract `key=value` pairs from an attribute block, coercing scalars. + /// + /// `nonce` is exempt from coercion. It is a reserved transport key holding an + /// opaque 16-character hex token, and coercing it corrupts the value: hex is a + /// superset of decimal, so coercion corrupts roughly 1 in 1,200 generated + /// nonces (`0663512342083e99` parses as `6.63512342083e+110`, + /// `9677e80871924237` overflows to `inf`, and `0419856025378190` loses its + /// leading zero). Any of those then fails to equal the nonce that produced it, + /// so strict mode rejects its own untampered output as prompt injection. + /// + /// The root cause is fixed here rather than by stringifying at the comparison + /// site, which is what the Rust runtime does (`parsers/prompty.rs:174-182`). + /// That approach still mismatches on values whose numeric round-trip is not + /// identity, such as a leading zero. Never coercing a value that is + /// specification-defined to be an opaque string removes the class entirely. + static func parseAttributes(_ raw: String) -> [String: Any] { + var result: [String: Any] = [:] + let range = NSRange(raw.startIndex.. String { + var joined = Substring(lines.joined(separator: "\n")) + while joined.first == "\n" { joined = joined.dropFirst() } + while joined.last == "\n" { joined = joined.dropLast() } + return String(joined) + } + + /// 8 random bytes rendered as 16 hex characters. + static func generateNonce() -> String { + var hex = "" + for _ in 0..<8 { + hex += String(format: "%02x", UInt8.random(in: 0...255)) + } + return hex + } +} diff --git a/runtime/swift/prompty/Sources/Prompty/Protocols.swift b/runtime/swift/prompty/Sources/Prompty/Protocols.swift new file mode 100644 index 000000000..ea9cc9ee5 --- /dev/null +++ b/runtime/swift/prompty/Sources/Prompty/Protocols.swift @@ -0,0 +1,96 @@ +import Foundation + +/// Pipeline stage conformances. +/// +/// The four stage protocols — `Renderer`, `Parser`, `Executor`, and `Processor` +/// — are emitted from the shared TypeSpec model, so this runtime adopts them +/// directly rather than declaring a competing set. This file only supplies the +/// defaults and the small amount of Swift-side typing the generated `Any` +/// signatures leave open. +/// +/// Conventions the generated signatures leave to each runtime, matched to the +/// Rust reference implementation: +/// +/// - `Executor.executeStream` returns a ``RawChunkStream`` of raw provider +/// chunks — exactly the SSE payloads, undecoded. +/// - `Processor.processStream` turns that raw stream into a ``ChunkStream`` of +/// generated `StreamChunk` models. +/// - `Parser.preRender` returns the rewritten template plus any context the +/// matching `parse` call needs, as `PreRenderResult`. + +/// The raw provider chunks an `Executor` streams. +import PromptyModel + +/// The decoded chunks a `Processor` streams. + +/// What a strict-mode parser hands back from `preRender`. + +// MARK: - Generated model ergonomics + +public typealias RawChunkStream = AsyncThrowingStream<[String: Any], Error> +public typealias ChunkStream = AsyncThrowingStream +public struct PreRenderResult { + /// The rewritten template to render. + public var text: String + /// Context the matching `parse` call needs to validate the render. + public var context: [String: Any] + + public init(text: String, context: [String: Any]) { + self.text = text + self.context = context + } +} +extension Parser { + /// Leave the template untouched. + /// + /// Only strict-mode parsers need to rewrite instructions before rendering. + public func preRender(template: String) throws -> Any? { nil } +} +extension Executor { + /// Report that this provider cannot stream. + public func executeStream(agent: Agent, messages: [Message]) async throws -> Any { + throw InvokerError.execution("streaming is not supported by this executor") + } + + /// Report that this provider has no tool-turn representation. + public func formatToolMessages( + rawResponse: Any, toolCalls: [ToolCall], toolResults: [String], textContent: String? + ) throws -> [Message] { + throw InvokerError.execution("tool calling is not supported by this executor") + } +} +extension Processor { + /// Report that this provider cannot stream. + public func processStream(stream: Any) async throws -> Any { + throw InvokerError.execution("streaming is not supported by this processor") + } +} +extension StreamChunk { + /// Incremental assistant text. + public static func text(_ value: String) -> StreamChunk { + .textChunk(TextChunk(value: value)) + } + + /// A completed tool call. + public static func tool(_ call: ToolCall) -> StreamChunk { + .toolChunk(ToolChunk(toolCall: call)) + } + + /// The text carried by this chunk, when it carries any. + public var textValue: String? { + if case .textChunk(let chunk) = self { return chunk.value } + return nil + } + + /// The tool call carried by this chunk, when it carries one. + public var toolCallValue: ToolCall? { + if case .toolChunk(let chunk) = self { return chunk.toolCall } + return nil + } +} +extension ToolCall { + /// Decoded arguments, or an empty dictionary when the payload is unusable. + public var argumentValues: [String: Any] { + JSONSupport.parse(json: arguments) as? [String: Any] ?? [:] + } +} diff --git a/runtime/swift/prompty/Sources/Prompty/References.swift b/runtime/swift/prompty/Sources/Prompty/References.swift new file mode 100644 index 000000000..1274dac9a --- /dev/null +++ b/runtime/swift/prompty/Sources/Prompty/References.swift @@ -0,0 +1,158 @@ +import Foundation + +/// Resolves `${protocol:value}` references in loaded frontmatter. +/// +/// Two protocols are supported: +/// - `${env:VAR}` and `${env:VAR:default}` read process environment variables. +/// - `${file:relative/path}` inlines a sibling file. `.json`, `.yaml`, and +/// `.yml` are parsed into structured values; anything else is inlined as text. +/// +/// Only whole-value references are resolved — a reference must be the entire +/// string. Unknown protocols are left untouched. +import Yams + +public enum References { + + /// Recursively resolve every reference in a value tree, in place. + public static func resolve( + _ value: inout Any, + agentDirectory: URL, + allowedRoots: [URL] + ) throws { + if var dict = value as? [String: Any] { + for key in dict.keys { + if let string = dict[key] as? String { + if let resolved = try resolveString( + string, key: key, agentDirectory: agentDirectory, allowedRoots: allowedRoots) + { + dict[key] = resolved + } + } else if var nested = dict[key] { + try resolve(&nested, agentDirectory: agentDirectory, allowedRoots: allowedRoots) + dict[key] = nested + } + } + value = dict + return + } + + if var array = value as? [Any] { + for index in array.indices { + if let string = array[index] as? String { + if let resolved = try resolveString( + string, key: "[\(index)]", agentDirectory: agentDirectory, allowedRoots: allowedRoots) + { + array[index] = resolved + } + } else { + var element = array[index] + try resolve(&element, agentDirectory: agentDirectory, allowedRoots: allowedRoots) + array[index] = element + } + } + value = array + } + } + + /// Resolve a single string. Returns `nil` when the string is not a reference. + public static func resolveString( + _ string: String, + key: String, + agentDirectory: URL, + allowedRoots: [URL] + ) throws -> Any? { + guard string.hasPrefix("${"), string.hasSuffix("}") else { return nil } + + let inner = String(string.dropFirst(2).dropLast()) + guard let colon = inner.firstIndex(of: ":") else { return nil } + + let proto = inner[inner.startIndex.. Any { + let separator = argument.firstIndex(of: ":") + let name = separator.map { String(argument[argument.startIndex..<$0]) } ?? argument + let fallback = separator.map { String(argument[argument.index(after: $0)...]) } + + // An explicitly empty variable is a value, not an absence — deliberately + // clearing a key must not silently fall back to a default. + if let value = ProcessInfo.processInfo.environment[name] { + return value + } + if let fallback { + return fallback + } + throw LoadError.envVarNotSet(varName: name, key: key) + } + + private static func resolveFile( + _ relativePath: String, + key: String, + agentDirectory: URL, + allowedRoots: [URL] + ) throws -> Any { + let requested = URL(fileURLWithPath: relativePath, relativeTo: agentDirectory) + let full = requested.standardizedFileURL.resolvingSymlinksInPath() + + guard FileManager.default.fileExists(atPath: full.path) else { + throw LoadError.fileReference(path: full.path, detail: "No such file") + } + + // File references are a host-controlled capability: by default they may not + // escape the prompt's own directory tree. + var roots = [agentDirectory.standardizedFileURL.resolvingSymlinksInPath()] + roots.append(contentsOf: allowedRoots.map { $0.standardizedFileURL.resolvingSymlinksInPath() }) + + let isPermitted = roots.contains { root in + full.path == root.path || full.path.hasPrefix(root.path + "/") + || full.path.hasPrefix(root.path + "\\") + } + guard isPermitted else { + throw LoadError.fileReference( + path: full.path, + detail: "File reference '\(relativePath)' for key '\(key)' resolves outside allowed roots" + ) + } + + let contents: String + do { + contents = try String(contentsOf: full, encoding: .utf8) + } catch { + throw LoadError.fileReference(path: full.path, detail: String(describing: error)) + } + + switch full.pathExtension.lowercased() { + case "json": + guard let parsed = JSONSupport.parse(json: contents) else { + throw LoadError.fileReference(path: full.path, detail: "Invalid JSON") + } + return parsed + case "yaml", "yml": + do { + guard let parsed = JSONSupport.normalize(try Yams.load(yaml: contents)) else { + throw LoadError.fileReference(path: full.path, detail: "Invalid YAML") + } + return parsed + } catch let error as LoadError { + throw error + } catch { + throw LoadError.fileReference( + path: full.path, detail: "Invalid YAML: \(String(describing: error))") + } + default: + return contents + } + } +} diff --git a/runtime/swift/prompty/Sources/Prompty/Registry.swift b/runtime/swift/prompty/Sources/Prompty/Registry.swift new file mode 100644 index 000000000..b4f12106c --- /dev/null +++ b/runtime/swift/prompty/Sources/Prompty/Registry.swift @@ -0,0 +1,110 @@ +import Foundation +import PromptyModel + +/// Thread-safe lookup tables mapping spec keys to implementations. +/// +/// Renderers and parsers are keyed by `template.format.kind` and +/// `template.parser.kind`; executors and processors by `model.provider`. +public final class Registry: @unchecked Sendable { + + /// The registry used by the top-level pipeline functions. + public static let shared = Registry() + + private let lock = NSLock() + private var renderers: [String: Renderer] = [:] + private var parsers: [String: Parser] = [:] + private var executors: [String: Executor] = [:] + private var processors: [String: Processor] = [:] + + public init() {} + + // MARK: - Registration + + public func register(renderer: Renderer, for key: String) { + lock.lock() + defer { lock.unlock() } + renderers[key] = renderer + } + + public func register(parser: Parser, for key: String) { + lock.lock() + defer { lock.unlock() } + parsers[key] = parser + } + + public func register(executor: Executor, for key: String) { + lock.lock() + defer { lock.unlock() } + executors[key] = executor + } + + public func register(processor: Processor, for key: String) { + lock.lock() + defer { lock.unlock() } + processors[key] = processor + } + + // MARK: - Lookup + + public func renderer(for key: String) throws -> Renderer { + lock.lock() + defer { lock.unlock() } + guard let value = renderers[key] else { + throw InvokerError.notFound(group: "renderer", key: key) + } + return value + } + + public func parser(for key: String) throws -> Parser { + lock.lock() + defer { lock.unlock() } + guard let value = parsers[key] else { + throw InvokerError.notFound(group: "parser", key: key) + } + return value + } + + public func executor(for key: String) throws -> Executor { + lock.lock() + defer { lock.unlock() } + guard let value = executors[key] else { + throw InvokerError.notFound(group: "executor", key: key) + } + return value + } + + public func processor(for key: String) throws -> Processor { + lock.lock() + defer { lock.unlock() } + guard let value = processors[key] else { + throw InvokerError.notFound(group: "processor", key: key) + } + return value + } + + // MARK: - Defaults + + private var defaultsRegistered = false + + /// Register the renderers and parsers built into the core runtime. + /// + /// Providers register themselves — importing `PromptyOpenAI` and calling its + /// `registerOpenAI()` adds the OpenAI executor and processor. + /// + /// The lock is held across the whole installation. Publishing the flag first + /// and registering afterwards would let a concurrent caller observe + /// "registered" and then look up an empty table. + public func registerDefaults() { + lock.lock() + defer { lock.unlock() } + guard !defaultsRegistered else { return } + defaultsRegistered = true + + let jinja = Jinja2Renderer() + renderers["jinja2"] = jinja + // `nunjucks` is the JavaScript port of the same template language. + renderers["nunjucks"] = jinja + renderers["mustache"] = MustacheRenderer() + parsers["prompty"] = PromptyChatParser() + } +} diff --git a/runtime/swift/prompty/Sources/Prompty/RenderCommon.swift b/runtime/swift/prompty/Sources/Prompty/RenderCommon.swift new file mode 100644 index 000000000..2afc12148 --- /dev/null +++ b/runtime/swift/prompty/Sources/Prompty/RenderCommon.swift @@ -0,0 +1,42 @@ +import Foundation + +/// Shared renderer preparation. +import PromptyModel + +public enum RenderCommon { + + /// Replace rich-kind inputs with nonce placeholders. + /// + /// Thread, image, file, and audio inputs are structured values that cannot be + /// stringified into a template. Instead each is swapped for a unique marker + /// that the preparation stage expands back into real content parts after + /// parsing. Doing it this way keeps injected content out of the template + /// engine entirely. + public static func prepareRenderInputs( + _ agent: Agent, + inputs: [String: Any] + ) -> (inputs: [String: Any], nonces: [String: String]) { + var prepared = inputs + var nonces: [String: String] = [:] + + for property in agent.inputProperties + where Defaults.richKinds.contains(property.kindName) { + let name = property.name + guard !name.isEmpty, prepared[name] != nil else { continue } + let nonce = makeNonce(for: name) + nonces[name] = nonce + prepared[name] = nonce + } + + return (prepared, nonces) + } + + /// Build a `__PROMPTY_THREAD_<8 hex>___` placeholder. + public static func makeNonce(for name: String) -> String { + var hex = "" + for _ in 0..<4 { + hex += String(format: "%02x", UInt8.random(in: 0...255)) + } + return "\(Defaults.threadNoncePrefix)\(hex)_\(name)__" + } +} diff --git a/runtime/swift/prompty/Sources/Prompty/Structured.swift b/runtime/swift/prompty/Sources/Prompty/Structured.swift new file mode 100644 index 000000000..19bbb1d38 --- /dev/null +++ b/runtime/swift/prompty/Sources/Prompty/Structured.swift @@ -0,0 +1,79 @@ +import Foundation + +/// Transport for structured (schema-shaped) results. +/// +/// When a prompt declares `outputs`, a processed object or array is wrapped so +/// that the exact provider JSON survives alongside the parsed value. Callers +/// that just want the data see it through ``unwrap(_:)``; callers that need a +/// lossless round-trip decode ``cast(_:as:)`` from the preserved raw JSON. +import PromptyModel + +public enum Structured { + + /// Wrap a processed result when the prompt declares outputs. + public static func wrapIfNeeded(_ agent: Agent, result: Any?) -> Any? { + guard agent.hasStructuredOutputs else { return result } + guard result is [String: Any] || result is [Any] else { return result } + + return [ + Defaults.structuredMarker: true, + "data": result as Any, + "raw_json": JSONSupport.toJSON(result), + ] + } + + /// Whether a value is a structured transport envelope. + public static func isStructured(_ value: Any?) -> Bool { + guard let dict = value as? [String: Any] else { return false } + return JSONSupport.isTruthy(dict[Defaults.structuredMarker]) + } + + /// Unwrap the envelope, returning the payload. Non-envelopes pass through. + public static func unwrap(_ value: Any?) -> Any? { + guard isStructured(value), let dict = value as? [String: Any] else { return value } + return dict["data"] + } + + /// The preserved raw provider JSON, when present. + public static func rawJSON(_ value: Any?) -> String? { + guard isStructured(value), let dict = value as? [String: Any] else { return nil } + return dict["raw_json"] as? String + } + + /// Decode a structured result into a `Decodable` type. + /// + /// The preserved raw JSON is preferred so nothing is lost re-encoding the + /// intermediate representation. + public static func cast(_ value: Any?, as type: T.Type) throws -> T { + let json: String + if let raw = rawJSON(value) { + json = raw + } else if let text = unwrap(value) as? String { + // A bare string result is already JSON text. Re-encoding it would + // produce a JSON *string literal* and decoding into a structure would + // then fail, so it is used verbatim. + json = text + } else { + json = JSONSupport.toJSON(unwrap(value)) + } + + guard let data = json.data(using: .utf8) else { + throw InvokerError.processing("result is not valid UTF-8") + } + do { + return try JSONDecoder().decode(T.self, from: data) + } catch { + throw InvokerError.processing("failed to decode structured result: \(error)") + } + } + + /// Build a JSON Schema from a prompt's declared outputs. + /// + /// Providers use this to request schema-constrained responses. Structured + /// output is always strict. + public static func outputSchema(_ agent: Agent) throws -> [String: Any]? { + let outputs = agent.outputProperties + guard !outputs.isEmpty else { return nil } + return try JSONSchema.outputs(outputs) + } +} diff --git a/runtime/swift/prompty/Sources/Prompty/TurnRunner.swift b/runtime/swift/prompty/Sources/Prompty/TurnRunner.swift new file mode 100644 index 000000000..c0de9d024 --- /dev/null +++ b/runtime/swift/prompty/Sources/Prompty/TurnRunner.swift @@ -0,0 +1,243 @@ +import Foundation + +/// A durable, replayable single-turn runner. +/// +/// This is the Swift counterpart of `ReferenceTurnRunner` in +/// `runtime/rust/prompty/src/harness.rs`. It drives one turn to completion — +/// model call, permission gate, host tool execution, checkpointing — while +/// emitting the exact event sequence pinned by +/// `spec/vectors/harness/replay_vectors.json`, so a journal produced here +/// replays identically against any other runtime. +/// +/// The clock and id factory are injected so a run is byte-for-byte +/// reproducible; durability adapters are injected so the same loop works +/// against in-memory, on-disk, or remote storage. +import PromptyModel + +public struct ReferenceTurnRunner { + public typealias ModelCallback = (TurnModelRequest) async throws -> TurnModelResponse + public typealias Clock = () -> String + public typealias IdFactory = (String) -> String + + private let eventSink: any EventSink + private let journal: any EventJournalWriter + private let checkpointStore: any CheckpointStore + private let permissionResolver: any PermissionResolver + private let hostToolExecutor: any HostToolExecutor + private let invokeModel: ModelCallback + private let now: Clock + private let nextId: IdFactory + + public init( + eventSink: any EventSink, + journal: any EventJournalWriter, + checkpointStore: any CheckpointStore, + permissionResolver: any PermissionResolver, + hostToolExecutor: any HostToolExecutor, + invokeModel: @escaping ModelCallback, + now: @escaping Clock = { ISO8601DateFormatter().string(from: Date()) }, + nextId: @escaping IdFactory = { "\($0)-\(UUID().uuidString)" } + ) { + self.eventSink = eventSink + self.journal = journal + self.checkpointStore = checkpointStore + self.permissionResolver = permissionResolver + self.hostToolExecutor = hostToolExecutor + self.invokeModel = invokeModel + self.now = now + self.nextId = nextId + } + + // MARK: - Turn loop + + public func run(_ request: RunTurnRequest) async throws -> RunTurnResult { + // A negative budget is clamped rather than rejected so a caller cannot + // accidentally invert the loop guard. + let maxIterations = Int(max(request.options?.maxIterations ?? 10, 0)) + let inputs = request.inputs ?? [:] + + var checkpoints: [Checkpoint] = [] + var toolResults: [HostToolResult] = [] + var pendingResults: [HostToolResult] = [] + var iteration = 0 + var output: Any? + var status: RunTurnStatus = .success + + try emitSession(.sessionStart, request: request) + try emitTurn(.turnStart, request: request, iteration: 0) + + // `maxIterations == 0` means "never call the model", so the guard is + // evaluated before the first invocation as well as between rounds. + while iteration < maxIterations { + var modelRequest = TurnModelRequest( + sessionId: request.sessionId, turnId: request.turnId, iteration: Int32(iteration)) + modelRequest.inputs = inputs + modelRequest.options = request.options + modelRequest.toolResults = pendingResults + + try emitTurn(.llmStart, request: request, iteration: iteration) + let response = try await invokeModel(modelRequest) + try emitTurn(.llmComplete, request: request, iteration: iteration) + + let checkpoint = try await recordCheckpoint( + request: request, iteration: iteration, state: response.checkpointState) + checkpoints.append(checkpoint) + + let requests = response.toolRequests ?? [] + guard !requests.isEmpty else { + output = response.output + iteration += 1 + break + } + + pendingResults = [] + for toolRequest in requests { + let result = try await runTool(toolRequest, request: request, iteration: iteration) + pendingResults.append(result) + toolResults.append(result) + } + try emitTurn(.messagesUpdated, request: request, iteration: iteration) + + iteration += 1 + + // The budget is consumed by the round that just ran, so exhaustion is + // reported against the iteration that would have run next. + if iteration >= maxIterations { + status = .error + output = ["message": "Maximum turn iterations reached"] + try emitTurn( + .error, request: request, iteration: iteration, payload: ["errorKind": "max_iterations"]) + break + } + } + + try emitTurn( + .turnEnd, request: request, iteration: iteration, payload: ["status": status.rawValue]) + try emitSession( + .sessionEnd, request: request, payload: ["status": status.rawValue]) + + var summary = SessionSummary(sessionId: request.sessionId) + summary.status = status == .success ? .success : .error + summary.turns = 1 + summary.checkpoints = Int32(checkpoints.count) + _ = try journal.close(summary: summary) + + var result = RunTurnResult( + sessionId: request.sessionId, turnId: request.turnId, status: status, + iterations: Int32(iteration)) + result.output = output + result.toolResults = toolResults + result.checkpoints = checkpoints + return result + } + + // MARK: - Tool round + + /// Gate a single tool call on permission, then execute it. + /// + /// A denial is turned into an unsuccessful result rather than an error so the + /// model sees — and can respond to — the refusal on the next iteration. + private func runTool( + _ toolRequest: HostToolRequest, request: RunTurnRequest, iteration: Int + ) async throws -> HostToolResult { + let requestId = toolRequest.requestId ?? nextId("exec") + let permissionId = "\(requestId)-permission" + + var permissionRequest = PermissionRequest(permission: "tool:\(toolRequest.toolName)") + permissionRequest.requestId = permissionId + permissionRequest.toolCallId = toolRequest.toolCallId + permissionRequest.target = toolRequest.toolName + + try emitTurn( + .permissionRequested, request: request, iteration: iteration, + payload: ["requestId": permissionId, "toolName": toolRequest.toolName]) + + let decision = try await permissionResolver.request(request: permissionRequest) + + try emitTurn( + .permissionCompleted, request: request, iteration: iteration, + payload: ["requestId": permissionId, "approved": decision.approved]) + + let result: HostToolResult + if decision.approved { + try emitTurn( + .toolExecutionStart, request: request, iteration: iteration, + payload: ["toolName": toolRequest.toolName]) + + result = try await hostToolExecutor.execute(request: toolRequest) + + try emitTurn( + .toolExecutionComplete, request: request, iteration: iteration, + payload: resultPayload(result)) + } else { + var denied = HostToolResult( + requestId: toolRequest.requestId, + toolCallId: toolRequest.toolCallId, + toolName: toolRequest.toolName, + success: false + ) + denied.errorKind = "permission_denied" + denied.result = ["message": decision.reason ?? "Permission denied"] + result = denied + } + + try emitTurn( + .toolResult, request: request, iteration: iteration, payload: resultPayload(result)) + return result + } + + private func resultPayload(_ result: HostToolResult) -> [String: Any] { + var payload: [String: Any] = [ + "toolName": result.toolName, + "success": result.success, + ] + if let errorKind = result.errorKind { payload["errorKind"] = errorKind } + return payload + } + + // MARK: - Durability + + private func recordCheckpoint( + request: RunTurnRequest, iteration: Int, state: [String: Any]? + ) async throws -> Checkpoint { + var checkpoint = Checkpoint(title: "Iteration \(iteration)") + checkpoint.id = nextId("checkpoint") + checkpoint.sessionId = request.sessionId + checkpoint.turnId = request.turnId + checkpoint.checkpointNumber = Int32(iteration) + checkpoint.state = state + checkpoint.createdAt = now() + + let saved = try await checkpointStore.save(checkpoint: checkpoint) + try emitSession( + .checkpointCreated, request: request, payload: ["checkpointId": saved.id ?? ""]) + return saved + } + + /// Every event goes to the sink first and the journal second, so an + /// observer never sees an event that was not durably recorded behind it. + private func emitTurn( + _ type: TurnEventType, request: RunTurnRequest, iteration: Int, + payload: [String: Any] = [:] + ) throws { + var event = TurnEvent(id: nextId("event"), type: type, timestamp: now()) + event.turnId = request.turnId + event.iteration = Int32(iteration) + event.payload = payload + + _ = try eventSink.emitTurn(turnEvent: event) + _ = try journal.appendTurn(turnEvent: event) + } + + private func emitSession( + _ type: SessionEventType, request: RunTurnRequest, payload: [String: Any] = [:] + ) throws { + var event = SessionEvent(id: nextId("event"), type: type, timestamp: now()) + event.sessionId = request.sessionId + event.turnId = request.turnId + event.payload = payload + + _ = try eventSink.emitSession(sessionEvent: event) + _ = try journal.appendSession(sessionEvent: event) + } +} diff --git a/runtime/swift/prompty/Sources/PromptyOpenAI/OpenAIConfig.swift b/runtime/swift/prompty/Sources/PromptyOpenAI/OpenAIConfig.swift new file mode 100644 index 000000000..691075061 --- /dev/null +++ b/runtime/swift/prompty/Sources/PromptyOpenAI/OpenAIConfig.swift @@ -0,0 +1,80 @@ +import Foundation + +import Prompty + +import PromptyModel + +/// Shared configuration for talking to an OpenAI-compatible endpoint. +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif +struct OpenAIConfig { + var baseURL: URL + var apiKey: String + var model: String + var extraHeaders: [String: String] + + /// Derive request configuration from a prompt's model and connection. + /// + /// The API key is read from the connection when present, otherwise from + /// `OPENAI_API_KEY`. Populating the environment is the host's job — the + /// runtime never reads `.env` files itself. + static func resolve(_ agent: Agent) throws -> OpenAIConfig { + let connection = agent.model?.connection + var endpoint = "https://api.openai.com/v1" + var apiKey = "" + var headers: [String: String] = [:] + + if let connection { + let fields = connectionFields(connection) + if let value = fields["endpoint"] as? String, !value.isEmpty { endpoint = value } + if let value = fields["apiKey"] as? String, !value.isEmpty { apiKey = value } + if let value = fields["headers"] as? [String: Any] { + for (key, header) in value { headers[key] = JSONSupport.stringify(header) } + } + } + + if apiKey.isEmpty { + apiKey = ProcessInfo.processInfo.environment["OPENAI_API_KEY"] ?? "" + } + guard !apiKey.isEmpty else { + throw InvokerError.execution( + "no OpenAI API key — set model.connection.apiKey or the OPENAI_API_KEY environment variable" + ) + } + + let model = agent.modelId + guard !model.isEmpty else { + throw InvokerError.execution("no model id — set model.id in the prompt frontmatter") + } + + guard let url = URL(string: endpoint.hasSuffix("/") ? String(endpoint.dropLast()) : endpoint) + else { + throw InvokerError.execution("invalid endpoint '\(endpoint)'") + } + + return OpenAIConfig(baseURL: url, apiKey: apiKey, model: model, extraHeaders: headers) + } + + /// Flatten a connection into its raw fields. + static func connectionFields(_ connection: Connection) -> [String: Any] { + (try? connection.save()) ?? [:] + } + + /// Build a request against a path under the configured base URL. + func request(path: String, body: [String: Any]) throws -> URLRequest { + var request = URLRequest(url: baseURL.appendingPathComponent(path)) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + for (key, value) in extraHeaders { + request.setValue(value, forHTTPHeaderField: key) + } + + guard let data = JSONSupport.toJSON(body).data(using: .utf8) else { + throw InvokerError.execution("failed to encode request body") + } + request.httpBody = data + return request + } +} diff --git a/runtime/swift/prompty/Sources/PromptyOpenAI/OpenAIExecutor.swift b/runtime/swift/prompty/Sources/PromptyOpenAI/OpenAIExecutor.swift new file mode 100644 index 000000000..bb8dc1800 --- /dev/null +++ b/runtime/swift/prompty/Sources/PromptyOpenAI/OpenAIExecutor.swift @@ -0,0 +1,159 @@ +import Foundation +import Prompty +import PromptyModel + +/// Calls the OpenAI API over HTTP. +/// +/// The executor is deliberately thin: request bodies come from ``OpenAIWire`` +/// so they stay identical to every other Agent runtime, and responses are +/// handed to ``OpenAIProcessor`` unmodified. +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +/// Minimal server-sent-events reader. +public struct OpenAIExecutor: Executor { + private let session: URLSession + + public init(session: URLSession = .shared) { + self.session = session + } + + // MARK: - Execute + + public func execute(agent: Agent, messages: [Message]) async throws -> Any { + let config = try OpenAIConfig.resolve(agent) + let (path, body) = try OpenAIExecutor.request(agent, messages: messages) + return try await send(config.request(path: path, body: body)) + } + + /// Build the endpoint path and request body for a prompt's API type. + public static func request(_ agent: Agent, messages: [Message]) throws -> ( + path: String, body: [String: Any] + ) { + switch agent.apiTypeName { + case "chat", "agent": + return ("chat/completions", try OpenAIWire.chatArgs(agent, messages: messages)) + case "responses": + return ("responses", try OpenAIWire.responsesArgs(agent, messages: messages)) + case "embedding": + return ("embeddings", OpenAIWire.embeddingArgs(agent, messages: messages)) + case "image": + return ("images/generations", OpenAIWire.imageArgs(agent, messages: messages)) + case let other: + throw InvokerError.execution("unsupported apiType '\(other)' for the openai provider") + } + } + + // MARK: - Stream + + /// Stream raw provider chunks. + /// + /// Returns a ``RawChunkStream``; decoding into `StreamChunk` values is the + /// processor's job, matching every other Agent runtime. + public func executeStream(agent: Agent, messages: [Message]) async throws -> Any { + let config = try OpenAIConfig.resolve(agent) + var (path, body) = try OpenAIExecutor.request(agent, messages: messages) + OpenAIWire.enableStreaming(&body, apiType: agent.apiTypeName) + let request = try config.request(path: path, body: body) + let session = self.session + + let stream: RawChunkStream = AsyncThrowingStream { continuation in + Task { + do { + for try await line in try await SSE.lines(for: request, session: session) { + guard let event = SSE.payload(of: line) else { continue } + if event == "[DONE]" { break } + guard let json = JSONSupport.parse(json: event) as? [String: Any] else { continue } + continuation.yield(json) + } + continuation.finish() + } catch { + continuation.finish(throwing: error) + } + } + } + return stream + } + + // MARK: - Tool turns + + public func formatToolMessages( + rawResponse: Any, toolCalls: [ToolCall], toolResults: [String], textContent: String? + ) throws -> [Message] { + var messages = OpenAIWire.toolMessages(toolCalls, results: toolResults) + if let text = textContent, !text.isEmpty, !messages.isEmpty { + messages[0].parts = [.textPart(TextPart(value: text))] + } + return messages + } + + // MARK: - Transport + + private func send(_ request: URLRequest) async throws -> [String: Any] { + let (data, response) = try await session.data(for: request) + + if let http = response as? HTTPURLResponse, !(200..<300).contains(http.statusCode) { + let body = String(data: data, encoding: .utf8) ?? "" + throw InvokerError.execution("OpenAI request failed (\(http.statusCode)): \(body)") + } + + guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { + throw InvokerError.execution("OpenAI response was not a JSON object") + } + return json + } +} +enum SSE { + /// Stream response lines, using the platform's native byte stream when it is + /// available and falling back to a buffered read otherwise. + static func lines(for request: URLRequest, session: URLSession) async throws + -> AsyncThrowingStream + { + #if canImport(FoundationNetworking) + let (data, response) = try await session.data(for: request) + try validate(response, data: data) + let text = String(data: data, encoding: .utf8) ?? "" + return AsyncThrowingStream { continuation in + for line in Lines.split(text) { + continuation.yield(String(line)) + } + continuation.finish() + } + #else + let (bytes, response) = try await session.bytes(for: request) + try validate(response, data: Data()) + return AsyncThrowingStream { continuation in + Task { + do { + for try await line in bytes.lines { + continuation.yield(line) + } + continuation.finish() + } catch { + continuation.finish(throwing: error) + } + } + } + #endif + } + + /// The JSON payload of an SSE `data:` line, or `nil` for other lines. + /// + /// Trimming includes newlines so a line handed over with its CR still attached + /// — `CharacterSet.whitespaces` is space and tab only — does not defeat the + /// `[DONE]` sentinel comparison. + static func payload(of line: String) -> String? { + guard line.hasPrefix("data:") else { return nil } + let value = line.dropFirst("data:".count).trimmingCharacters(in: .whitespacesAndNewlines) + return value.isEmpty ? nil : value + } + + private static func validate(_ response: URLResponse, data: Data) throws { + guard let http = response as? HTTPURLResponse, !(200..<300).contains(http.statusCode) else { + return + } + let body = String(data: data, encoding: .utf8) ?? "" + throw InvokerError.execution("OpenAI stream failed (\(http.statusCode)): \(body)") + } +} diff --git a/runtime/swift/prompty/Sources/PromptyOpenAI/OpenAIProcessor.swift b/runtime/swift/prompty/Sources/PromptyOpenAI/OpenAIProcessor.swift new file mode 100644 index 000000000..e9809b3cf --- /dev/null +++ b/runtime/swift/prompty/Sources/PromptyOpenAI/OpenAIProcessor.swift @@ -0,0 +1,172 @@ +import Foundation + +/// Normalizes OpenAI responses into runtime values. +/// +/// The response shape alone determines the projection, so one processor serves +/// the Chat Completions, Responses, embedding, and image APIs. Behaviour is +/// pinned by `spec/vectors/process/process_vectors.json`. +import Prompty + +import PromptyModel + +public struct OpenAIProcessor: Processor { + public init() {} + + public func process(agent: Agent, response: Any) async throws -> Any { + try OpenAIProcessor.processResponse(agent, response: response) + } + + /// Decode a ``RawChunkStream`` of provider events into generated + /// `StreamChunk` values. + public func processStream(stream: Any) async throws -> Any { + guard let raw = stream as? RawChunkStream else { + throw InvokerError.execution("expected a raw provider chunk stream") + } + + let decoded: ChunkStream = AsyncThrowingStream { continuation in + Task { + var accumulator = StreamAccumulator() + do { + for try await event in raw { + for chunk in accumulator.consume(event) { + continuation.yield(chunk) + } + } + for chunk in accumulator.finish() { + continuation.yield(chunk) + } + continuation.finish() + } catch { + continuation.yield(.errorChunk(ErrorChunk(message: "\(error)"))) + continuation.finish(throwing: error) + } + } + } + return decoded + } + + /// Project a raw provider response onto a runtime value. + /// + /// - Text responses become a `String`. + /// - Tool calls become `[[String: Any]]` of `{ id, name, arguments }`. + /// - Embeddings become a vector, or a list of vectors for batches. + /// - Images become the URL or base64 payload. + /// - When the prompt declares outputs, text is JSON-parsed; unparseable text + /// falls back to the raw string rather than failing the call. + public static func processResponse(_ agent: Agent, response: Any) throws -> Any { + guard let body = response as? [String: Any] else { + return response + } + + if let choices = body["choices"] as? [Any] { + return try processChat(agent, choices: choices) + } + if body["output"] != nil || body["output_text"] != nil { + return try processResponses(agent, body: body) + } + if let data = body["data"] as? [Any] { + return try processData(data) + } + return body + } + + // MARK: - Chat Completions + + private static func processChat(_ agent: Agent, choices: [Any]) throws -> Any { + guard let first = choices.first as? [String: Any], + let message = first["message"] as? [String: Any] + else { + return "" + } + + if let calls = message["tool_calls"] as? [Any], !calls.isEmpty { + return calls.compactMap { entry -> [String: Any]? in + guard let call = entry as? [String: Any] else { return nil } + let function = call["function"] as? [String: Any] ?? [:] + return [ + "id": call["id"] as? String ?? "", + "name": function["name"] as? String ?? "", + "arguments": function["arguments"] as? String ?? "", + ] + } + } + + if let refusal = message["refusal"] as? String, !refusal.isEmpty { + return refusal + } + + let content = message["content"] as? String ?? "" + return finalize(agent, text: content) + } + + // MARK: - Responses API + + private static func processResponses(_ agent: Agent, body: [String: Any]) throws -> Any { + let output = body["output"] as? [Any] ?? [] + + let calls: [[String: Any]] = output.compactMap { entry in + guard let item = entry as? [String: Any], item["type"] as? String == "function_call" else { + return nil + } + return [ + "id": item["call_id"] as? String ?? item["id"] as? String ?? "", + "name": item["name"] as? String ?? "", + "arguments": item["arguments"] as? String ?? "", + ] + } + if !calls.isEmpty { return calls } + + if let text = body["output_text"] as? String, !text.isEmpty { + return finalize(agent, text: text) + } + + // Fall back to assembling `output_text` blocks when the convenience field + // is absent — some SDK shapes only carry the structured output items. + var assembled = "" + for entry in output { + guard let item = entry as? [String: Any], + let content = item["content"] as? [Any] + else { continue } + for block in content { + guard let part = block as? [String: Any], + part["type"] as? String == "output_text", + let text = part["text"] as? String + else { continue } + assembled += text + } + } + return finalize(agent, text: assembled) + } + + // MARK: - Embeddings and images + + private static func processData(_ data: [Any]) throws -> Any { + let entries = data.compactMap { $0 as? [String: Any] } + + if entries.allSatisfy({ $0["embedding"] != nil }) && !entries.isEmpty { + let vectors = entries.map { $0["embedding"] ?? [] } + return vectors.count == 1 ? vectors[0] : vectors + } + + let images: [Any] = entries.compactMap { entry in + if let url = entry["url"] as? String { return url } + if let b64 = entry["b64_json"] as? String { return b64 } + return nil + } + if images.isEmpty { return "" } + return images.count == 1 ? images[0] : images + } + + // MARK: - Structured output + + private static func finalize(_ agent: Agent, text: String) -> Any { + guard agent.hasStructuredOutputs, !text.isEmpty else { return text } + guard let parsed = JSONSupport.parse(json: text), + parsed is [String: Any] || parsed is [Any] + else { + // A model that ignored the schema still returned something useful. + return text + } + return parsed + } +} diff --git a/runtime/swift/prompty/Sources/PromptyOpenAI/Registration.swift b/runtime/swift/prompty/Sources/PromptyOpenAI/Registration.swift new file mode 100644 index 000000000..855d91189 --- /dev/null +++ b/runtime/swift/prompty/Sources/PromptyOpenAI/Registration.swift @@ -0,0 +1,20 @@ +import Foundation + +/// Registers the OpenAI executor and processor. +/// +/// Call once during host startup: +/// +/// ```swift +/// import Agent +/// import PromptyOpenAI +/// +/// registerOpenAI() +/// ``` +import Prompty + +import PromptyModel + +public func registerOpenAI(into registry: Registry = .shared) { + registry.register(executor: OpenAIExecutor(), for: "openai") + registry.register(processor: OpenAIProcessor(), for: "openai") +} diff --git a/runtime/swift/prompty/Sources/PromptyOpenAI/StreamAccumulator.swift b/runtime/swift/prompty/Sources/PromptyOpenAI/StreamAccumulator.swift new file mode 100644 index 000000000..df020aa69 --- /dev/null +++ b/runtime/swift/prompty/Sources/PromptyOpenAI/StreamAccumulator.swift @@ -0,0 +1,103 @@ +import Foundation + +/// Assembles streamed OpenAI events into generated `StreamChunk` values. +/// +/// Text deltas are emitted as they arrive; tool calls arrive in fragments and +/// are only emitted once the stream ends, because a call's arguments are split +/// across many events. Usage totals are emitted last when the provider reports +/// them. +import Prompty + +import PromptyModel + +struct StreamAccumulator { + private var calls: [Int: ToolCall] = [:] + private var usage: [String: Any]? + + /// Fold one streamed event into the accumulator, emitting any chunks it + /// completes. + mutating func consume(_ event: [String: Any]) -> [StreamChunk] { + // Responses API events carry their delta at the top level. + if let type = event["type"] as? String { + return consumeResponsesEvent(type: type, event: event) + } + + if let reported = event["usage"] as? [String: Any] { usage = reported } + + guard let choices = event["choices"] as? [Any], + let choice = choices.first as? [String: Any], + let delta = choice["delta"] as? [String: Any] + else { return [] } + + var chunks: [StreamChunk] = [] + if let piece = delta["content"] as? String, !piece.isEmpty { + chunks.append(.text(piece)) + } + if let thinking = delta["reasoning_content"] as? String, !thinking.isEmpty { + chunks.append(.thinkingChunk(ThinkingChunk(value: thinking))) + } + + for entry in delta["tool_calls"] as? [Any] ?? [] { + guard let call = entry as? [String: Any] else { continue } + let index = (call["index"] as? Int) ?? 0 + var current = calls[index] ?? ToolCall() + if let id = call["id"] as? String, !id.isEmpty { current.id = id } + if let function = call["function"] as? [String: Any] { + if let name = function["name"] as? String, !name.isEmpty { current.name = name } + if let arguments = function["arguments"] as? String { current.arguments += arguments } + } + calls[index] = current + } + return chunks + } + + private mutating func consumeResponsesEvent(type: String, event: [String: Any]) + -> [StreamChunk] + { + switch type { + case "response.output_text.delta": + guard let piece = event["delta"] as? String, !piece.isEmpty else { return [] } + return [.text(piece)] + + case "response.reasoning_summary_text.delta": + guard let piece = event["delta"] as? String, !piece.isEmpty else { return [] } + return [.thinkingChunk(ThinkingChunk(value: piece))] + + case "response.output_item.done": + guard let item = event["item"] as? [String: Any], + item["type"] as? String == "function_call" + else { return [] } + calls[calls.count] = ToolCall( + id: item["call_id"] as? String ?? "", + name: item["name"] as? String ?? "", + arguments: item["arguments"] as? String ?? "" + ) + return [] + + case "response.completed": + if let response = event["response"] as? [String: Any], + let reported = response["usage"] as? [String: Any] + { + usage = reported + } + return [] + + case "error": + let message = (event["message"] as? String) ?? "stream error" + return [.errorChunk(ErrorChunk(message: message))] + + default: + return [] + } + } + + /// The chunks that can only be emitted once the stream is exhausted. + func finish() -> [StreamChunk] { + var chunks = calls.keys.sorted().map { StreamChunk.tool(calls[$0]!) } + + if let usage, let parsed = try? InvocationUsage.load(usage) { + chunks.append(.usageChunk(UsageChunk(usage: parsed))) + } + return chunks + } +} diff --git a/runtime/swift/prompty/Sources/PromptyOpenAI/Wire.swift b/runtime/swift/prompty/Sources/PromptyOpenAI/Wire.swift new file mode 100644 index 000000000..4b044656e --- /dev/null +++ b/runtime/swift/prompty/Sources/PromptyOpenAI/Wire.swift @@ -0,0 +1,335 @@ +import Foundation + +/// Wire-format projection for the OpenAI Chat Completions and Responses APIs. +/// +/// Every function here mirrors the Rust reference implementation so all runtimes +/// produce identical request bodies for the shared `spec/vectors/wire` contract. +import Prompty + +import PromptyModel + +public enum OpenAIWire { + + // MARK: - Messages + + /// Project a message onto the Chat Completions wire shape. + public static func message(_ message: Message) -> [String: Any] { + var wire: [String: Any] = ["role": message.role.rawValue] + + for (key, value) in message.metadata where key != "role" && key != "content" { + wire[key] = value + } + + if let text = message.plainTextWireContent { + wire["content"] = text + } else { + wire["content"] = message.parts.map(part) + } + return wire + } + + /// Project a single content part onto its typed wire block. + public static func part(_ part: ContentPart) -> [String: Any] { + switch part { + case .textPart(let text): + return ["type": "text", "text": text.value] + + case .imagePart(let image): + var url: [String: Any] = ["url": image.source] + if let detail = image.detail { url["detail"] = detail } + return ["type": "image_url", "image_url": url] + + case .audioPart(let audio): + let format = audio.mediaType.map(audioFormat) ?? "wav" + return ["type": "input_audio", "input_audio": ["data": audio.source, "format": format]] + + case .filePart(let file): + return ["type": "file", "file": ["url": file.source]] + } + } + + /// Map an audio MIME type onto OpenAI's `format` token. + public static func audioFormat(_ mime: String) -> String { + switch mime { + case "audio/wav", "audio/x-wav": return "wav" + case "audio/mpeg", "audio/mp3": return "mp3" + case "audio/mp4": return "mp4" + case "audio/ogg": return "ogg" + case "audio/flac": return "flac" + case "audio/webm": return "webm" + case "audio/pcm": return "pcm" + default: + guard mime.hasPrefix("audio/") else { return "wav" } + return String(mime.dropFirst("audio/".count)) + } + } + + // MARK: - Request bodies + + /// Build the request body for a chat completions call. + public static func chatArgs(_ agent: Agent, messages: [Message]) throws -> [String: Any] { + var args: [String: Any] = [ + "model": agent.modelId, + "messages": messages.map(message), + ] + + applyOptions(&args, agent.modelOptions, provider: "openai") + + let tools = try self.tools(agent) + if !tools.isEmpty { args["tools"] = tools } + + if let format = try responseFormat(agent) { + args["response_format"] = format + } + return args + } + + /// Build the request body for the Responses API. + /// + /// System and developer messages collapse into `instructions`; everything + /// else becomes an `input` item. + public static func responsesArgs(_ agent: Agent, messages: [Message]) throws -> [String: Any] { + var systemParts: [String] = [] + var input: [Any] = [] + + for message in messages { + let role = message.role.rawValue + if role == "system" || role == "developer" { + systemParts.append(message.textContent) + } else { + input.append(responsesInput(message)) + } + } + + var args: [String: Any] = [ + "model": agent.modelId.isEmpty ? "gpt-4o" : agent.modelId, + "input": input, + ] + if !systemParts.isEmpty { + args["instructions"] = systemParts.joined(separator: "\n\n") + } + + applyOptions(&args, agent.modelOptions, provider: "responses") + + let tools = try responsesTools(agent) + if !tools.isEmpty { args["tools"] = tools } + + if let text = try responsesTextFormat(agent) { + args["text"] = text + } + return args + } + + /// Build the request body for an embedding call. + public static func embeddingArgs(_ agent: Agent, messages: [Message]) -> [String: Any] { + var args: [String: Any] = [ + "model": agent.modelId.isEmpty ? "text-embedding-ada-002" : agent.modelId, + "input": textInput(messages), + ] + mergeAdditionalProperties(&args, agent.modelOptions, overwrite: true) + return args + } + + /// Build the request body for an image generation call. + public static func imageArgs(_ agent: Agent, messages: [Message]) -> [String: Any] { + let prompt: String + switch textInput(messages) { + case let single as String: prompt = single + case let many as [String]: prompt = many.joined(separator: " ") + default: prompt = "" + } + + var args: [String: Any] = [ + "model": agent.modelId.isEmpty ? "dall-e-3" : agent.modelId, + "prompt": prompt, + ] + mergeAdditionalProperties(&args, agent.modelOptions, overwrite: true) + return args + } + + /// Turn on server-sent streaming for a request body. + /// + /// `chat` and `agent` calls also request usage on the terminal event so every + /// OpenAI-wire provider reports identical token counts. + public static func enableStreaming(_ body: inout [String: Any], apiType: String) { + body["stream"] = true + if apiType == "chat" || apiType == "agent" { + body["stream_options"] = ["include_usage": true] + } + } + + // MARK: - Options + + static func applyOptions( + _ args: inout [String: Any], _ options: ModelOptions?, provider: String + ) { + guard let options else { return } + + if let wire = try? options.toWire(provider) { + for (key, value) in wire where !(value is NSNull) { + args[key] = fixFloat(value) + } + } + mergeAdditionalProperties(&args, options, overwrite: false) + } + + static func mergeAdditionalProperties( + _ args: inout [String: Any], _ options: ModelOptions?, overwrite: Bool + ) { + guard let extras = options?.additionalProperties else { return } + for (key, value) in extras where overwrite || args[key] == nil { + args[key] = value + } + } + + /// `ModelOptions` stores fractional values as `Float`. Widening a `Float` to + /// `Double` for JSON serialization exposes binary artifacts (0.1 becomes + /// 0.10000000149011612), so round-trip through the shortest `Float` literal. + static func fixFloat(_ value: Any) -> Any { + guard let float = value as? Float else { return value } + return Double(String(float)) ?? Double(float) + } + + // MARK: - Tools + + /// Project every function tool onto the Chat Completions tool shape. + public static func tools(_ agent: Agent) throws -> [[String: Any]] { + try (agent.tools ?? []) + .filter { $0.kindName == "function" } + .map(functionTool) + } + + static func functionTool(_ tool: Tool) throws -> [String: Any] { + var definition: [String: Any] = ["name": tool.name] + if let description = tool.toolDescription { definition["description"] = description } + + let bound = tool.boundParameterNames + let visible = tool.functionParameters.filter { !bound.contains($0.name) } + let strict = tool.isStrict + + var parameters = try JSONSchema.parameters(visible, strict: strict) + if strict { + parameters["additionalProperties"] = false + definition["strict"] = true + } + definition["parameters"] = parameters + + return ["type": "function", "function": definition] + } + + /// Project every function tool onto the Responses API's flat tool shape. + public static func responsesTools(_ agent: Agent) throws -> [[String: Any]] { + try (agent.tools ?? []) + .filter { $0.kindName == "function" } + .map(responsesFunctionTool) + } + + static func responsesFunctionTool(_ tool: Tool) throws -> [String: Any] { + var wire: [String: Any] = ["type": "function", "name": tool.name] + if let description = tool.toolDescription { wire["description"] = description } + + let bound = tool.boundParameterNames + let visible = tool.functionParameters.filter { !bound.contains($0.name) } + let strict = tool.isStrict + + var parameters = try JSONSchema.parameters(visible, strict: strict) + if strict { + parameters["additionalProperties"] = false + wire["strict"] = true + } + wire["parameters"] = parameters + + return wire + } + + // MARK: - Structured output + + static func responseFormat(_ agent: Agent) throws -> [String: Any]? { + guard let schema = try Structured.outputSchema(agent) else { return nil } + return [ + "type": "json_schema", + "json_schema": [ + "name": "structured_output", + "strict": true, + "schema": schema, + ], + ] + } + + static func responsesTextFormat(_ agent: Agent) throws -> [String: Any]? { + guard let schema = try Structured.outputSchema(agent) else { return nil } + return [ + "format": [ + "type": "json_schema", + "name": "structured_output", + "schema": schema, + "strict": true, + ] + ] + } + + // MARK: - Agent loop + + /// Format tool results back into conversation messages. + /// + /// Produces one assistant message carrying `tool_calls` metadata, then one + /// `tool` message per result. + public static func toolMessages(_ calls: [ToolCall], results: [String]) -> [Message] { + var messages: [Message] = [] + + let wireCalls: [Any] = calls.map { call in + [ + "id": call.id, + "type": "function", + "function": ["name": call.name, "arguments": call.arguments], + ] + } + + var assistant = Message.withText(.assistant, "") + assistant.metadata = ["tool_calls": wireCalls] + messages.append(assistant) + + for (call, result) in zip(calls, results) { + var message = Message.toolResult(toolCallId: call.id, result: result) + var metadata = message.metadata + metadata["name"] = call.name + message.metadata = metadata + messages.append(message) + } + return messages + } + + /// Whether a durable message carries a provider-owned Responses function-call + /// item. Native `previous_response_id` continuation already owns that item. + public static func isResponsesFunctionCall(_ message: Message) -> Bool { + message.metadata["responses_function_call"] != nil + } + + static func responsesInput(_ message: Message) -> Any { + if let passthrough = message.metadata["responses_function_call"] { + return passthrough + } + + let content: Any = message.plainTextWireContent ?? message.parts.map(part) + + if let callId = message.metadata["tool_call_id"] { + let output: String + if let text = content as? String { + output = text + } else { + output = JSONSupport.toJSON(content) + } + return ["type": "function_call_output", "call_id": callId, "output": output] + } + + let role = message.role == .tool ? "user" : message.role.rawValue + return ["role": role, "content": content] + } + + // MARK: - Helpers + + static func textInput(_ messages: [Message]) -> Any { + let texts = messages.map(\.textContent).filter { !$0.isEmpty } + return texts.count == 1 ? texts[0] : texts + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/BindingExpectationPairingTests.swift b/runtime/swift/prompty/Tests/PromptyTests/BindingExpectationPairingTests.swift new file mode 100644 index 000000000..a094a9cd5 --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/BindingExpectationPairingTests.swift @@ -0,0 +1,200 @@ +import XCTest + +@testable import Prompty +@testable import PromptyModel + +/// How a vector's `bindings` expectation is paired with the loaded bindings. +/// +/// `validateBindings` addresses entries by name, which is sound only while +/// those names are unique. An object cannot carry the same key twice, so a +/// repeated name proves the source used the array fallback — the one +/// representation that carries order — and those entries are therefore +/// positional. Uniquely named entries stay order- and representation-agnostic, +/// because both forms are legal for them and object form's order is an +/// artefact of key iteration, not content. +/// +/// No spec vector declares a duplicate binding name today, so none of this is +/// reachable through `testLoadVectors`. These tests drive `validateBindings` +/// directly, to pin the rule before PR #447 lands vectors that rely on it — +/// otherwise the first duplicate-bearing vector would be compared by a rule +/// nothing had ever checked. +final class BindingExpectationPairingTests: XCTestCase { + + // MARK: - Fixture + + /// Load a function tool whose bindings are declared in array form, so the + /// declared order is the loaded order. + /// + /// The loaded shape is asserted here rather than assumed: if loading + /// collapsed the duplicates into a map or reordered the array, every + /// `XCTAssertThrowsError` below would still pass — for the wrong reason, on a + /// count mismatch that has nothing to do with pairing. + private func tool( + _ bindings: [(name: String, input: String)], + file: StaticString = #filePath, + line: UInt = #line + ) throws -> Tool { + let loaded = try Tool.load([ + "name": "lookup", + "kind": "function", + "bindings": bindings.map { ["name": $0.name, "input": $0.input] }, + ]) + XCTAssertEqual( + loaded.bindings.map(\.name), bindings.map(\.name), + "loader did not preserve the declared binding names in order", + file: file, line: line) + XCTAssertEqual( + loaded.bindings.map(\.input), bindings.map(\.input), + "loader did not preserve the declared binding inputs in order", + file: file, line: line) + return loaded + } + + private func validate(_ tool: Tool, _ bindings: Any) throws { + try LoadVectorTests.validateBindings(tool, expected: ["bindings": bindings], index: 0) + } + + // MARK: - Duplicate names are positional + + /// The case name addressing cannot see at all. + /// + /// Both expectations carry the same name *and* the same input, so a + /// `first(where:)` lookup satisfies both from the first entry and never + /// examines the second — reporting a pass while half the collection went + /// unverified. Positional comparison reaches it. + func testDuplicateNamesDoNotLeaveLaterEntriesUnverified() throws { + let loaded = try tool([("dup", "a"), ("dup", "wrong")]) + XCTAssertThrowsError( + try validate( + loaded, + [ + ["name": "dup", "input": "a"], + ["name": "dup", "input": "a"], + ]), + "the second duplicate entry was never compared") + } + + /// Duplicates come from the ordered representation, so exchanging the + /// payloads of two identically named entries is a real difference — not the + /// reordering of a keyed collection. A multiset comparison would call these + /// equal. + func testDuplicateNamesAreComparedPositionallyNotAsAMultiset() throws { + let loaded = try tool([("dup", "b"), ("dup", "a")]) + XCTAssertThrowsError( + try validate( + loaded, + [ + ["name": "dup", "input": "a"], + ["name": "dup", "input": "b"], + ]), + "exchanged payloads under a repeated name must not compare equal") + } + + /// Positive control for the two above: the same duplicated shape passes when + /// the positions do agree, so those failures are pairing-specific rather than + /// a blanket rejection of duplicate names. + func testDuplicateNamesPassWhenPositionsAgree() throws { + let loaded = try tool([("dup", "a"), ("dup", "b")]) + XCTAssertNoThrow( + try validate( + loaded, + [ + ["name": "dup", "input": "a"], + ["name": "dup", "input": "b"], + ])) + } + + // MARK: - Unique names stay agnostic + + /// Unique names must *not* be compared positionally. Object form is legal for + /// them and its order is an artefact, so asserting position here would reject + /// a conforming loader. + func testUniqueNamesStayOrderAgnosticInListForm() throws { + let loaded = try tool([("b", "2"), ("a", "1")]) + XCTAssertNoThrow( + try validate( + loaded, + [ + ["name": "a", "input": "1"], + ["name": "b", "input": "2"], + ])) + } + + /// Guards the test above from vacuity: order-agnostic must not mean + /// value-blind. + func testUniqueNamesStillCatchAWrongInput() throws { + let loaded = try tool([("b", "2"), ("a", "wrong")]) + XCTAssertThrowsError( + try validate( + loaded, + [ + ["name": "a", "input": "1"], + ["name": "b", "input": "2"], + ]), + "a wrong input must fail even though the names all match") + } + + /// Map expectations address by key, so they are order-agnostic for the same + /// reason — and cannot express duplicates at all. + func testMapFormStaysOrderAgnostic() throws { + let loaded = try tool([("b", "2"), ("a", "1")]) + XCTAssertNoThrow( + try validate(loaded, ["a": ["input": "1"], "b": ["input": "2"]])) + } + + /// The positional branch must compare names, not only inputs. Every entry + /// here carries the same input, so only the name comparison can see that two + /// entries changed places. + func testPositionalComparisonChecksNamesNotJustInputs() throws { + let loaded = try tool([("dup", "same"), ("dup", "same"), ("unique", "same")]) + XCTAssertThrowsError( + try validate( + loaded, + [ + ["name": "dup", "input": "same"], + ["name": "unique", "input": "same"], + ["name": "dup", "input": "same"], + ]), + "a name moved between positions must be caught even when inputs match") + } + + // MARK: - Empty names are positional too + + /// An empty name disqualifies object form exactly as a duplicate does, so it + /// equally proves the source was the ordered array fallback — even though + /// every name here is unique, and so would pass a uniqueness-only pre-scan + /// while the array had been reordered. + func testEmptyNameForcesPositionalComparison() throws { + let loaded = try tool([("named", "ok"), ("", "blank")]) + XCTAssertThrowsError( + try validate( + loaded, + [ + ["name": "", "input": "blank"], + ["name": "named", "input": "ok"], + ]), + "an empty name makes the collection ordered, so the reorder must be caught") + } + + /// Positive control for the above: an empty name is not rejected outright, + /// only held to its position. + func testEmptyNamePassesWhenPositionsAgree() throws { + let loaded = try tool([("", "blank"), ("named", "ok")]) + XCTAssertNoThrow( + try validate( + loaded, + [ + ["name": "", "input": "blank"], + ["name": "named", "input": "ok"], + ])) + } + + /// Object form cannot legally carry an empty key, so a map expectation that + /// does is malformed rather than something to address by name. + func testMapFormRejectsAnEmptyKey() throws { + let loaded = try tool([("", "blank")]) + XCTAssertThrowsError( + try validate(loaded, ["": ["input": "blank"]]), + "an empty key cannot be expressed in object form") + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/ContentPartDiscriminatorTests.swift b/runtime/swift/prompty/Tests/PromptyTests/ContentPartDiscriminatorTests.swift new file mode 100644 index 000000000..d46f8060e --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/ContentPartDiscriminatorTests.swift @@ -0,0 +1,403 @@ +import Foundation + +import PromptyModel + +import XCTest + +@testable import Prompty + +/// Strict-discriminator acceptance gate for `ContentPart`. +/// +/// This is the Swift half of the shared acceptance strengthened by Agent +/// PR #447 commit `0c64ef33`, whose canonical vector is +/// `spec/vectors/model/content_part_discriminator_vectors.json`. +/// +/// That vector is **not checked out on this branch** — `spec/vectors/model/` +/// is absent here — but it does exist in repository history, added by commit +/// `b820d785` ("Define strict content part discriminators"). The three cases +/// below are transcribed from that commit *verbatim*, including payload fields +/// the discriminator logic never reads, so this suite cannot pass on a +/// simplified input that the real vector would reject. +/// +/// ``testCanonicalVectorWhenPresent`` drives the same assertions directly off +/// the vector file and activates automatically once it lands on this branch, +/// at which point the inline mirror becomes redundant and can be deleted. +/// +/// The contract under test, from the vector's own description — "ContentPart +/// is closed and case-sensitive: unknown kinds are rejected rather than +/// preserved like unknown Connection values or dispatched like unknown Tool +/// values": +/// +/// 1. `text` loads unchanged; +/// 2. `video` is rejected — it is not a member of the closed union; +/// 3. `Text` is rejected — matching is exact and case-sensitive; +/// 4. rejection surfaces a *structured* diagnostic exposing the field `kind` +/// and the offending raw value verbatim. +/// +/// `ContentPart` is **closed**: unlike `Connection` (which passes unknown +/// kinds through) and `Tool` (which dispatches them to `CustomTool`), it must +/// never gain a Swift `.unknown` case. All three policies are pinned against +/// each other in ``testClosedContentPartIsIndependentOfOpenUnions``. +/// +/// Closedness itself is enforced at *compile time* — see ``caseName(_:)``. +final class ContentPartDiscriminatorTests: XCTestCase { + + // MARK: - Canonical inputs, transcribed from commit b820d785 + + /// `known_text_content_part_loads` + private static let textInput: [String: Any] = ["kind": "text", "value": "hello"] + + /// `unknown_content_part_kind_is_rejected`. `durationSeconds` is carried + /// deliberately: a loader that accepted `video` only for a particular + /// payload shape would pass a stripped-down input but fail the real vector. + private static let videoInput: [String: Any] = [ + "kind": "video", + "source": "https://example.test/video.mp4", + "durationSeconds": 3, + ] + + /// `content_part_case_collision_is_rejected` + private static let capitalTextInput: [String: Any] = [ + "kind": "Text", "value": "case-sensitive", + ] + + // MARK: - Helpers + + /// Exhaustive switch with no `default:`. + /// + /// This is the closedness gate. If a future emitter adds an `.unknown` case + /// to `ContentPart`, this function stops compiling and the whole suite fails + /// to build — an earlier and louder signal than any runtime assertion. + /// + /// It is deliberately broader than "no `.unknown`": it freezes the case set, + /// so *any* added member breaks the build and forces a human decision. Two + /// other exhaustive switches (the generated `save()` and the runtime's + /// `OpenAIWire.part(_:)`) happen to break first today, but both are code an + /// emitter change could update automatically. This one cannot be updated + /// without editing a test, which is the review signal worth keeping. + /// + /// Do not add a `default:` branch here. + private func caseName(_ part: ContentPart) -> String { + switch part { + case .textPart: return "textPart" + case .imagePart: return "imagePart" + case .filePart: return "filePart" + case .audioPart: return "audioPart" + } + } + + /// Asserts that `raw` is rejected by `ContentPart.load` with the specific + /// structured diagnostic the acceptance requires. + /// + /// Deliberately stricter than `XCTAssertThrowsError`: a bare "it threw" + /// assertion would also pass for `invalidObject` or `invalidField`, so this + /// pattern-matches the exact error case and checks that the reported field is + /// `kind` and the reported value is the offending discriminator verbatim. + private func assertRejects( + _ raw: [String: Any], + expectedValue: String, + file: StaticString = #filePath, + line: UInt = #line + ) { + do { + let part = try ContentPart.load(raw) + XCTFail( + "expected \(expectedValue) to be rejected, got \(caseName(part))", + file: file, line: line) + } catch { + assertDiagnostic(error, expectedValue: expectedValue, file: file, line: line) + } + } + + /// Shared diagnostic assertion, so nested-path tests hold the error to the + /// same standard as direct loads instead of merely checking that something + /// was thrown. + private func assertDiagnostic( + _ error: Error, + expectedValue: String, + file: StaticString = #filePath, + line: UInt = #line + ) { + guard let typed = error as? TypraRuntimeError else { + XCTFail("expected TypraRuntimeError, got \(error)", file: file, line: line) + return + } + guard case .unknownDiscriminator(let type, let field, let value) = typed else { + XCTFail("expected .unknownDiscriminator, got \(typed)", file: file, line: line) + return + } + XCTAssertEqual( + type, "ContentPart", "diagnostic must name the type", file: file, line: line) + XCTAssertEqual(field, "kind", "diagnostic must expose the field", file: file, line: line) + XCTAssertEqual( + value, expectedValue, "diagnostic must expose the raw value verbatim", + file: file, line: line) + + // Foundation's `StringProtocol.contains` overload bridges to + // `NSString.range(of:)`, which reports `false` for an empty needle — the + // opposite of the stdlib overload's `true`. Both were verified on the + // toolchain in use. The check is therefore only meaningful, and only + // applied, for a non-empty discriminator. + if !expectedValue.isEmpty { + XCTAssertTrue( + typed.description.contains(expectedValue), + "rendered message must carry the raw value, got: \(typed.description)", + file: file, line: line) + } + } + + private func canonical(_ value: Any) throws -> String { + let data = try JSONSerialization.data(withJSONObject: value, options: [.sortedKeys]) + return String(decoding: data, as: UTF8.self) + } + + private var canonicalVectorURL: URL { + Spec.root.appendingPathComponent("vectors/model/content_part_discriminator_vectors.json") + } + + // MARK: - 1. text loads unchanged + + func testTextLoadsUnchanged() throws { + let raw = Self.textInput + let part = try ContentPart.load(raw) + + guard case .textPart(let text) = part else { + return XCTFail("expected .textPart, got \(caseName(part))") + } + XCTAssertEqual(text.kind, "text") + XCTAssertEqual(text.value, "hello") + + // "Unchanged" is a round-trip claim, not just a field claim. `TextPart` + // has exactly `kind` and `value`, so strict equality is safe — and a + // newly emitted or defaulted field *should* fail an "unchanged" gate. + XCTAssertEqual(try canonical(try part.save()), try canonical(raw)) + } + + func testTextRoundTripIsIdempotent() throws { + var current = try canonical(Self.textInput) + for pass in 0..<3 { + let object = try JSONSerialization.jsonObject(with: Data(current.utf8)) + let saved = try ContentPart.load(object).save() + XCTAssertEqual(try canonical(saved), current, "drift on pass \(pass)") + current = try canonical(saved) + } + } + + func testAllKnownKindsResolve() throws { + let cases: [(String, String)] = [ + ("text", "textPart"), + ("image", "imagePart"), + ("file", "filePart"), + ("audio", "audioPart"), + ] + for (kind, expected) in cases { + var raw: [String: Any] = ["kind": kind] + if kind == "text" { + raw["value"] = "v" + } else { + raw["source"] = "s" + } + let part = try ContentPart.load(raw) + XCTAssertEqual(caseName(part), expected, "kind \(kind)") + } + } + + // MARK: - 2. video rejects + + func testVideoIsRejected() { + assertRejects(Self.videoInput, expectedValue: "video") + } + + /// `video` must be rejected on the discriminator alone, regardless of how + /// plausible or sparse the rest of the payload is. Guards against a future + /// "recover by shape" heuristic quietly reopening the union. + func testVideoIsRejectedRegardlessOfPayloadShape() { + assertRejects(["kind": "video", "value": "looks like a text part"], expectedValue: "video") + assertRejects(["kind": "video"], expectedValue: "video") + } + + // MARK: - 3. Text rejects (case-sensitive) + + func testCapitalTextIsRejected() { + assertRejects(Self.capitalTextInput, expectedValue: "Text") + } + + /// The two payloads below differ *only* in the casing of the discriminator, + /// so exact, case-sensitive matching is the sole thing that can separate an + /// accepted load from a rejected one. + func testCapitalTextDiffersFromTextOnlyByCasing() throws { + let accepted: [String: Any] = ["kind": "text", "value": "case-sensitive"] + let rejected: [String: Any] = ["kind": "Text", "value": "case-sensitive"] + + XCTAssertNoThrow(try ContentPart.load(accepted)) + assertRejects(rejected, expectedValue: "Text") + } + + func testOtherCasingsAreAlsoRejected() { + for kind in ["TEXT", "Image", "IMAGE", "File", "Audio", "AUDIO", "tExT"] { + assertRejects(["kind": kind, "value": "v", "source": "s"], expectedValue: kind) + } + } + + func testAdjacentUnknownKindsAreRejected() { + for kind in [" text", "text ", "video/mp4", "textPart", "unknown"] { + assertRejects(["kind": kind, "value": "v"], expectedValue: kind) + } + } + + /// An explicitly empty discriminator is rejected. The generated loader gates + /// on `discriminator.isEmpty` before dispatch, so an empty `kind` surfaces as + /// `.invalidField(field: "kind", expected: "non-blank string")` rather than an + /// unknown-discriminator (there is no value to echo). + func testEmptyKindIsRejected() { + XCTAssertThrowsError(try ContentPart.load(["kind": "", "value": "empty kind"])) { error in + guard case .invalidField(let field, let expected)? = error as? TypraRuntimeError else { + return XCTFail("expected .invalidField, got \(error)") + } + XCTAssertEqual(field, "kind") + XCTAssertEqual(expected, "non-blank string") + } + } + + /// A *missing* `kind` is rejected too. The loader coalesces an absent value to + /// `NSNull`, which fails the `TypraRuntime.string` gate, so it surfaces as + /// `.invalidField(field: "kind", expected: "string")`. Only the field is + /// pinned; the exact `expected` text is a diagnostic detail owned by the + /// runtime. + func testMissingKindIsRejected() { + XCTAssertThrowsError(try ContentPart.load(["value": "no kind at all"])) { error in + guard case .invalidField(let field, _)? = error as? TypraRuntimeError else { + return XCTFail("expected .invalidField, got \(error)") + } + XCTAssertEqual(field, "kind") + } + } + + // MARK: - 4. Rejection is not swallowed by nesting + + /// `ContentPart` is reached in practice through `Message.parts`. A union that + /// rejects in isolation but is skipped, defaulted or dropped when nested + /// would satisfy a naive gate while still losing data. + func testRejectionPropagatesThroughMessage() { + let raw: [String: Any] = [ + "role": "user", + "parts": [["kind": "text", "value": "fine"], Self.videoInput], + ] + XCTAssertThrowsError(try Message.load(raw)) { error in + self.assertDiagnostic(error, expectedValue: "video") + } + } + + /// The second nested path. + func testRejectionPropagatesThroughToolResult() { + let raw: [String: Any] = ["parts": [Self.capitalTextInput]] + XCTAssertThrowsError(try ToolResult.load(raw)) { error in + self.assertDiagnostic(error, expectedValue: "Text") + } + } + + /// Rejection has to win even when the invalid part is not the first element, + /// so a valid sibling cannot mask it. + func testRejectionIsNotMaskedByValidSiblings() { + let raw: [String: Any] = [ + "role": "assistant", + "parts": [ + ["kind": "text", "value": "one"], + ["kind": "image", "source": "two"], + Self.videoInput, + ], + ] + XCTAssertThrowsError(try Message.load(raw)) { error in + self.assertDiagnostic(error, expectedValue: "video") + } + } + + // MARK: - Closed ContentPart vs the open Tool union + + /// Pins the two opposing unknown-kind policies against each other using the + /// *identical* payload, so no shape-based heuristic can satisfy this by + /// accident: `Tool` is an open union that dispatches an unknown kind to + /// `CustomTool`, while `ContentPart` is closed and refuses it outright. + /// + /// This is not the only thing keeping the policies apart — the rejection + /// tests above pin the closed side — but it is the only place the contrast is + /// asserted against one shared input. + func testClosedContentPartIsIndependentOfOpenUnions() throws { + let shared: [String: Any] = [ + "kind": "future-auth", + "name": "shared", + "connection": [ + "kind": "key", + "endpoint": "https://example.test", + "apiKey": "x", + ], + ] + + let tool = try Tool.load(shared) + guard case .customTool = tool else { + return XCTFail("Tool must dispatch unknown kinds to CustomTool") + } + + // Same input, opposite policy. + assertRejects(shared, expectedValue: "future-auth") + } + + /// `ContentPart` must not acquire a passthrough representation. The + /// compile-time gate is ``caseName(_:)``; this is the behavioural half. + func testUnknownContentPartNeverSurvivesLoad() { + for kind in ["video", "Text", "future-auth", "custom"] { + XCTAssertThrowsError( + try ContentPart.load(["kind": kind, "value": "v"]), + "\(kind) must not be representable" + ) { error in + self.assertDiagnostic(error, expectedValue: kind) + } + } + } + + // MARK: - Vector-driven run, active once the vector lands + + /// Drives the same contract straight off the canonical vector. + /// + /// Skips while `spec/vectors/model/` is absent from this branch; the inline + /// transcription above covers the identical cases in the meantime. When the + /// vector arrives this activates with no code change, and the inline mirror + /// can then be deleted. + func testCanonicalVectorWhenPresent() throws { + let url = canonicalVectorURL + guard FileManager.default.fileExists(atPath: url.path) else { + throw XCTSkip("canonical vector not on this branch; inline mirror covers the same cases") + } + + let data = try Data(contentsOf: url) + let root = try JSONSerialization.jsonObject(with: data) as? [String: Any] + let vectors = root?["vectors"] as? [[String: Any]] ?? [] + XCTAssertFalse(vectors.isEmpty, "vector file declares no cases") + + for vector in vectors { + let name = vector["name"] as? String ?? "" + guard let input = vector["input"] as? [String: Any] else { + XCTFail("\(name): missing input") + continue + } + let expected = vector["expected"] as? [String: Any] ?? [:] + + switch vector["operation"] as? String { + case "load": + let part = try ContentPart.load(input) + XCTAssertEqual( + try canonical(try part.save()), try canonical(expected), + "\(name): loaded value must match expected") + + case "load-error": + XCTAssertEqual( + expected["discriminator"] as? String, "kind", + "\(name): vector expects a different field") + assertRejects(input, expectedValue: expected["value"] as? String ?? "") + + case let other: + XCTFail("\(name): unsupported operation \(other ?? "nil")") + } + } + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/GeneratedModelRoundTripTests.swift b/runtime/swift/prompty/Tests/PromptyTests/GeneratedModelRoundTripTests.swift new file mode 100644 index 000000000..32b412968 --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/GeneratedModelRoundTripTests.swift @@ -0,0 +1,275 @@ +import Foundation + +import PromptyModel + +import XCTest + +/// Guards the fields `schema/scripts/patch-swift-emitter-defects.mjs` restores. +/// +/// @typra/emitter@0.4.2 drops `extends` base-model fields from derived Swift +/// structs, so `ArrayProperty` / `ObjectProperty` / `UnionProperty` and every +/// `Tool` subtype silently lose data on load and save. The shim injects them +/// back. These tests fail loudly if that regresses — including if a future +/// emitter release fixes the defect differently. +@testable import Prompty + +final class GeneratedModelRoundTripTests: XCTestCase { + + // MARK: - Property subtypes + + /// Every base `Property` field must survive `load` → `save` on each subtype. + func testPropertyBaseFieldsRoundTripOnEverySubtype() throws { + let subtypes: [String: [String: Any]] = [ + "array": ["items": ["name": "item", "kind": "string"]], + "object": ["properties": [["name": "child", "kind": "string"]]], + "union": ["anyOf": [["name": "a", "kind": "string"]]], + ] + + for (kind, extra) in subtypes { + var source: [String: Any] = [ + "name": "field_\(kind)", + "kind": kind, + "description": "a \(kind) property", + "required": true, + "nullable": true, + "default": ["seeded"], + "example": ["sampled"], + "enumValues": ["one", "two"], + ] + source.merge(extra) { current, _ in current } + + let property = try Property.load(source) + let saved = try property.save() + + for key in [ + "name", "description", "required", "nullable", "default", "example", "enumValues", + ] { + XCTAssertTrue( + Spec.equal(saved[key], source[key]), + "\(kind) property lost '\(key)': got \(Spec.describe(saved[key])), expected \(Spec.describe(source[key]))" + ) + } + } + } + + /// The runtime reads base fields through `Property.raw`, so accessors must + /// see them too — not just the saved dictionary. + func testPropertyAccessorsSeeBaseFields() throws { + let property = try Property.load([ + "name": "choices", + "kind": "array", + "description": "pick one", + "required": true, + "nullable": true, + "items": ["name": "item", "kind": "string"], + ]) + + XCTAssertEqual(property.name, "choices") + XCTAssertEqual(property.kindName, "array") + XCTAssertEqual(property.propertyDescription, "pick one") + XCTAssertTrue(property.isRequired) + XCTAssertTrue(property.isNullable) + XCTAssertEqual(property.arrayItems?.kindName, "string") + } + + /// Nested subtypes must round-trip too — this is the case that motivated + /// marking the generated `Property` enum `indirect`. + func testNestedPropertySubtypesRoundTrip() throws { + let source: [String: Any] = [ + "name": "matrix", + "kind": "array", + "items": [ + "name": "row", + "kind": "array", + "items": ["name": "cell", "kind": "integer", "description": "a cell"], + ], + ] + + let property = try Property.load(source) + XCTAssertEqual(property.arrayItems?.name, "row") + XCTAssertEqual(property.arrayItems?.arrayItems?.name, "cell") + XCTAssertEqual(property.arrayItems?.arrayItems?.propertyDescription, "a cell") + XCTAssertTrue(Spec.equal(try property.save(), source)) + } + + // MARK: - Tool subtypes + + /// Every `Tool` subtype must retain the base `name` / `description`. + func testToolBaseFieldsRoundTripOnEverySubtype() throws { + // mcp/openapi and the wildcard CustomTool all carry a required `connection`. + let connection: [String: Any] = [ + "kind": "key", + "endpoint": "https://example.test", + "apiKey": "x", + ] + let subtypes: [[String: Any]] = [ + ["kind": "function", "parameters": [["name": "city", "kind": "string"]]], + ["kind": "mcp", "serverName": "files", "connection": connection], + ["kind": "openapi", "specification": "./api.json", "connection": connection], + ["kind": "prompty", "path": "./child.prompty"], + // Unknown kinds fall through to the wildcard CustomTool case. + ["kind": "vendor_specific", "connection": connection], + ] + + for extra in subtypes { + var source: [String: Any] = [ + "name": "tool_\(extra["kind"] as? String ?? "?")", + "description": "a tool", + ] + source.merge(extra) { current, _ in current } + + let tool = try Tool.load(source) + XCTAssertEqual(tool.name, source["name"] as? String, "tool lost 'name'") + XCTAssertEqual(tool.toolDescription, "a tool", "tool lost 'description'") + + let saved = try tool.save() + XCTAssertTrue( + Spec.equal(saved["name"], source["name"]), + "\(source["kind"] ?? "?") tool did not save 'name'") + XCTAssertTrue( + Spec.equal(saved["description"], source["description"]), + "\(source["kind"] ?? "?") tool did not save 'description'") + } + } + + /// Bindings arrive either as a `Record` map — where the key supplies + /// the binding name — or as an already-named list. Both must load. + func testToolBindingsLoadFromMapForm() throws { + let tool = try Tool.load([ + "name": "lookup", + "kind": "function", + "parameters": [ + ["name": "query", "kind": "string"], + ["name": "tenant", "kind": "string"], + ], + "bindings": [ + "tenant": ["value": "contoso"], + "apiKey": ["value": "secret"], + ], + ]) + + // Map keys are sorted so generation stays deterministic. + XCTAssertEqual(Self.bindingNames(tool), ["apiKey", "tenant"]) + XCTAssertEqual(tool.boundParameterNames, ["apiKey", "tenant"]) + } + + func testToolBindingsLoadFromListForm() throws { + let tool = try Tool.load([ + "name": "lookup", + "kind": "function", + "bindings": [ + ["name": "tenant", "value": "contoso"] + ], + ]) + + XCTAssertEqual(Self.bindingNames(tool), ["tenant"]) + XCTAssertEqual(tool.boundParameterNames, ["tenant"]) + } + + // MARK: - Wildcard / closed-union cases + + /// `Tool` is an open union: an unknown kind survives a round trip through the + /// wildcard `CustomTool` case instead of throwing. `CustomTool` still carries + /// a required `connection`, so the fixture supplies one. + func testUnknownToolKindRoundTrips() throws { + let source: [String: Any] = [ + "name": "vendor_tool", + "kind": "vendor.custom", + "connection": ["kind": "key", "endpoint": "https://example.test", "apiKey": "x"], + "options": ["setting": "value"], + ] + let saved = try Tool.load(source).save() + XCTAssertTrue( + Spec.equal(saved["kind"], "vendor.custom"), "unknown tool kind was not preserved") + XCTAssertTrue(Spec.equal(saved["name"], "vendor_tool")) + } + + /// `Connection` is an *open* union: an unknown discriminator survives a round + /// trip through the wildcard `unknown` case instead of throwing. This is the + /// cross-runtime contract (Rust preserves the same input as + /// `ConnectionKind::Unknown` rather than rejecting it). + func testUnknownConnectionKindRoundTrips() throws { + let source: [String: Any] = ["kind": "vendor.auth", "endpoint": "https://example.test"] + let saved = try Connection.load(source).save() + XCTAssertTrue( + Spec.equal(saved["kind"], "vendor.auth"), "unknown connection kind was not preserved") + XCTAssertTrue(Spec.equal(saved["endpoint"], "https://example.test")) + } + + /// The base `Connection` fields survive a load/save round trip on every + /// subtype. + /// + /// `model Connection` declares `authenticationMode` and `usageDescription` + /// (`schema/model/connection/connection.tsp`), and every subtype `extends` + /// it. The emitter now declares, loads, and saves these inherited fields on + /// each subtype, so both must round-trip. Covering all six subtypes keeps the + /// guarantee measured rather than assumed. + func testConnectionBaseFieldsRoundTripOnEverySubtype() throws { + let subtypes: [(kind: String, declared: [String: String])] = [ + ("reference", ["name": "my-connection", "target": "some-target"]), + ("remote", ["name": "my-connection", "endpoint": "https://example.test"]), + ("key", ["endpoint": "https://example.test", "apiKey": "secret"]), + ("anonymous", ["endpoint": "https://example.test"]), + ("oauth", ["endpoint": "https://example.test", "clientId": "client-id"]), + ("foundry", ["endpoint": "https://example.test", "name": "my-connection"]), + ] + + for (kind, declared) in subtypes { + var source: [String: Any] = ["kind": kind] + for (key, value) in declared { source[key] = value } + source["authenticationMode"] = "system" + source["usageDescription"] = "respond to email on your behalf" + + let loaded = try Connection.load(source) + + let saved = try loaded.save() + XCTAssertEqual(saved["kind"] as? String, kind, "\(kind): discriminator lost") + for (key, value) in declared { + XCTAssertEqual(saved[key] as? String, value, "\(kind): declared field \(key) lost") + } + + XCTAssertEqual( + saved["authenticationMode"] as? String, "system", + "\(kind): authenticationMode lost on round trip") + XCTAssertEqual( + saved["usageDescription"] as? String, "respond to email on your behalf", + "\(kind): usageDescription lost on round trip") + } + } + + // MARK: - Helpers + + /// Read binding names off the loaded tool, which the generated `Tool` enum + /// exposes only through its raw payload. + /// Named collections serialize either as a name-keyed map — the default — or + /// as an already-named list when `collectionFormat` is `array`. Accept both, + /// so this pins binding *identity* rather than the emitter's chosen shape. + private static func bindingNames(_ tool: Tool) -> [String]? { + switch tool.raw["bindings"] { + case let list as [Any]: + return list.compactMap { ($0 as? [String: Any])?["name"] as? String }.sorted() + case let map as [String: Any]: + return map.keys.sorted() + default: + return nil + } + } + + // MARK: - Convenience factories + + /// The emitter's factories built messages from raw literals; the shim makes + /// them construct real enum values. + func testMessageFactoriesProduceTypedValues() { + let assistant = Message.assistant(text: "hi") + XCTAssertEqual(assistant.role.rawValue, "assistant") + XCTAssertEqual(assistant.textContent, "hi") + + let user = Message.user(text: "hello") + XCTAssertEqual(user.role.rawValue, "user") + XCTAssertEqual(user.textContent, "hello") + + let system = Message.system(text: "be helpful") + XCTAssertEqual(system.role.rawValue, "system") + XCTAssertEqual(system.textContent, "be helpful") + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/HarnessAdapterTests.swift b/runtime/swift/prompty/Tests/PromptyTests/HarnessAdapterTests.swift new file mode 100644 index 000000000..a254d9a3f --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/HarnessAdapterTests.swift @@ -0,0 +1,204 @@ +import Foundation + +import PromptyModel + +import XCTest + +/// Focused tests for the durability adapters. +/// +/// The replay vectors prove the *sequence* is right; these prove the adapters +/// behave correctly at their edges — where crash-durability, ordering, and +/// error containment actually live. +@testable import Prompty + +final class HarnessAdapterTests: XCTestCase { + + // MARK: - Journal + + /// Each record must be on disk before the call returns, so a crash mid-turn + /// still leaves a replayable prefix rather than an empty file. + func testJournalFlushesEachRecordImmediately() throws { + let path = Self.tempPath() + defer { try? FileManager.default.removeItem(atPath: path) } + let journal = JsonlEventJournalWriter(path: path) + + var first = TurnEvent(id: "e1", type: .turnStart, timestamp: "t") + first.iteration = 0 + _ = try journal.appendTurn(turnEvent: first) + + // Read back without closing — this is the crash case. + let midFlight = try JsonlEventJournalWriter.readRecords(path: path) + XCTAssertEqual(midFlight.count, 1) + XCTAssertEqual(midFlight[0]["kind"] as? String, "turn") + + var session = SessionEvent(id: "e2", type: .sessionEnd, timestamp: "t") + session.sessionId = "s1" + _ = try journal.appendSession(sessionEvent: session) + + var summary = SessionSummary(sessionId: "s1") + summary.turns = 1 + _ = try journal.close(summary: summary) + + let records = try JsonlEventJournalWriter.readRecords(path: path) + XCTAssertEqual(records.map { $0["kind"] as? String }, ["turn", "session", "summary"]) + } + + /// Closing twice must not append a second summary, or replay would see a + /// journal that ends after it already ended. + func testJournalCloseIsIdempotent() throws { + let path = Self.tempPath() + defer { try? FileManager.default.removeItem(atPath: path) } + let journal = JsonlEventJournalWriter(path: path) + + let summary = SessionSummary(sessionId: "s1") + _ = try journal.close(summary: summary) + _ = try journal.close(summary: summary) + + let records = try JsonlEventJournalWriter.readRecords(path: path) + XCTAssertEqual(records.filter { $0["kind"] as? String == "summary" }.count, 1) + } + + /// A journal must never interleave a record across lines, even when the + /// payload itself contains newlines. + func testJournalEscapesEmbeddedNewlines() throws { + let path = Self.tempPath() + defer { try? FileManager.default.removeItem(atPath: path) } + let journal = JsonlEventJournalWriter(path: path) + + var event = TurnEvent(id: "e1", type: .error, timestamp: "t") + event.payload = ["message": "line one\nline two"] + _ = try journal.appendTurn(turnEvent: event) + + let raw = try String(contentsOfFile: path, encoding: .utf8) + XCTAssertEqual(raw.split(separator: "\n", omittingEmptySubsequences: true).count, 1) + + let records = try JsonlEventJournalWriter.readRecords(path: path) + let payload = (records[0]["event"] as? [String: Any])?["payload"] as? [String: Any] + XCTAssertEqual(payload?["message"] as? String, "line one\nline two") + } + + // MARK: - Checkpoints + + /// Checkpoints must come back in creation order regardless of insertion + /// order, because replay walks them forward. + func testCheckpointsListInDeterministicOrder() async throws { + let store = InMemoryCheckpointStore() + + for index in [2, 0, 1] { + var checkpoint = Checkpoint(title: "cp\(index)") + checkpoint.id = "cp-\(index)" + checkpoint.sessionId = "s1" + checkpoint.checkpointNumber = Int32(index) + _ = try await store.save(checkpoint: checkpoint) + } + + let listed = try await store.listCheckpoints(sessionId: "s1") + XCTAssertEqual(listed.map { $0.id }, ["cp-0", "cp-1", "cp-2"]) + } + + /// Sessions must not read each other's checkpoints. + func testCheckpointsAreScopedPerSession() async throws { + let store = InMemoryCheckpointStore() + + for session in ["s1", "s2"] { + var checkpoint = Checkpoint(title: "cp") + checkpoint.id = "cp-\(session)" + checkpoint.sessionId = session + _ = try await store.save(checkpoint: checkpoint) + } + + let scoped = try await store.listCheckpoints(sessionId: "s1") + XCTAssertEqual(scoped.map { $0.id }, ["cp-s1"]) + let loaded = try await store.load(sessionId: "s2", checkpointId: "cp-s2") + XCTAssertEqual(loaded?.id, "cp-s2") + let crossSession = try await store.load(sessionId: "s1", checkpointId: "cp-s2") + XCTAssertNil(crossSession, "a checkpoint must not be readable from another session") + } + + // MARK: - Permissions + + func testPermissionResolversReportTheirDecision() async throws { + let request = PermissionRequest(permission: "tool:add") + + let allowed = try await AllowAllPermissionResolver().request(request: request) + XCTAssertTrue(allowed.approved) + XCTAssertEqual(allowed.permission, "tool:add") + + let denied = try await DenyAllPermissionResolver().request(request: request) + XCTAssertFalse(denied.approved) + XCTAssertEqual(denied.permission, "tool:add") + } + + // MARK: - Tool execution + + /// A tool executor must convert every failure into a result. If it threw, + /// one bad tool would abort the turn instead of letting the model recover. + func testToolExecutorContainsFailures() async throws { + let executor = FunctionHostToolExecutor(handlers: [ + "ok": { arguments in ["echo": arguments["value"] as Any] }, + "boom": { _ in throw InvokerError.execution("kaboom") }, + ]) + + var okRequest = HostToolRequest(toolName: "ok") + okRequest.arguments = ["value": 7] + let ok = try await executor.execute(request: okRequest) + XCTAssertTrue(ok.success) + XCTAssertNil(ok.errorKind) + XCTAssertEqual((ok.result as? [String: Any])?["echo"] as? Int, 7) + + let boom = try await executor.execute(request: HostToolRequest(toolName: "boom")) + XCTAssertFalse(boom.success) + XCTAssertEqual(boom.errorKind, "exception") + + let missing = try await executor.execute(request: HostToolRequest(toolName: "nope")) + XCTAssertFalse(missing.success) + XCTAssertEqual(missing.errorKind, "not_found") + } + + // MARK: - Replay verification + + func testVerifierDetectsLengthAndContentDrift() throws { + let verifier = ReferenceReplayVerifier() + + var a = ReplayJournalRecord() + a.kind = .turn + a.type = "turn:turn_start:0" + var b = ReplayJournalRecord() + b.kind = .turn + b.type = "turn:turn_end:1" + + let identical = try verifier.verify( + ReplayVerificationRequest(expected: [a, b], actual: [a, b])) + XCTAssertEqual(identical.status, .passed) + XCTAssertEqual(identical.mismatches?.isEmpty ?? true, true) + + let truncated = try verifier.verify( + ReplayVerificationRequest(expected: [a, b], actual: [a])) + XCTAssertEqual(truncated.status, .failed) + XCTAssertEqual(Int(truncated.expectedCount), 2) + XCTAssertEqual(Int(truncated.actualCount), 1) + + let divergent = try verifier.verify( + ReplayVerificationRequest(expected: [a, b], actual: [a, a])) + XCTAssertEqual(divergent.status, .failed) + XCTAssertEqual(divergent.mismatches?.first?.index, 1) + } + + // MARK: - Event sink + + func testCollectingSinkPreservesInterleavedOrder() throws { + let sink = CollectingEventSink() + + _ = try sink.emitSession( + sessionEvent: SessionEvent(id: "s1", type: .sessionStart, timestamp: "t")) + _ = try sink.emitTurn(turnEvent: TurnEvent(id: "t1", type: .turnStart, timestamp: "t")) + _ = try sink.emitTurn(turnEvent: TurnEvent(id: "t2", type: .turnEnd, timestamp: "t")) + + XCTAssertEqual(sink.turnEvents.map(\.id), ["t1", "t2"]) + XCTAssertEqual(sink.sessionEvents.map(\.id), ["s1"]) + } + + private static func tempPath() -> String { + NSTemporaryDirectory() + "prompty-journal-\(UUID().uuidString).jsonl" + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/InheritedPropertyFieldTests.swift b/runtime/swift/prompty/Tests/PromptyTests/InheritedPropertyFieldTests.swift new file mode 100644 index 000000000..47fadb2b6 --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/InheritedPropertyFieldTests.swift @@ -0,0 +1,469 @@ +import Foundation + +import PromptyModel + +import XCTest + +/// Acceptance gate for inherited-field data loss on composite `Property` subtypes. +/// +/// `ArrayProperty`, `ObjectProperty` and `UnionProperty` all `extends Property` +/// in `schema/model/core/properties.tsp`, so each inherits six fields: +/// `description`, `required`, `nullable`, `default`, `example` and `enumValues`. +/// A seventh, `name`, arrives through the `Named` alias rather than +/// through `extends`, but the emitter drops it the same way, so it is guarded +/// alongside them. @typra/emitter drops these fields from derived Swift structs, +/// which loses the values *silently* — the code compiles and the data just +/// disappears. `schema/scripts/patch-swift-emitter-defects.mjs` injects them +/// back into the stored properties, `load` and `save`. +/// +/// `GeneratedModelRoundTripTests` already covers the in-memory `load` → `save` +/// pair for these subtypes. This file extends that guarantee to the axes the +/// shared spec vectors never reach: JSON and YAML text round trips, values +/// constructed in Swift rather than parsed, non-scalar values in `default` / +/// `example` / `enumValues`, and inherited fields on nested branches. +/// +/// These tests are meant to survive the emitter fix: they assert the required +/// *behaviour*, not the shim. They deliberately avoid whole-dictionary equality, +/// so that unrelated additions to a saved payload do not fail a gate about +/// inherited fields. They do *not* tolerate a materialized schema default +/// standing in for an absent value: the canonical rule is that absent optional +/// collections stay omitted and save never synthesizes an empty one, so +/// `enumValues: []` appearing where the source wrote nothing is a violation +/// rather than an equivalent. `OptionalCollectionPresenceTests` pins that rule. +/// +/// Known residual gap, deliberately not asserted here: the shim restores base +/// fields to the stored properties but not to the generated memberwise `init`, +/// which only the emitter can do. Callers must therefore construct a composite +/// subtype and then assign inherited fields, as +/// `testInheritedFieldsSurviveProgrammaticConstruction` does. The shim's +/// `assertPinnedEmitterVersion()` already fails the build on any emitter version +/// change, so that gap gets re-audited without a canary test here. +@testable import Prompty + +final class InheritedPropertyFieldTests: XCTestCase { + + /// Every field `Property` passes down to its composite subtypes. + private static let inheritedFields = [ + "name", "description", "required", "nullable", "default", "example", "enumValues", + ] + + /// The discriminator-specific payload each composite subtype requires, + /// keyed by `kind`. + private static let compositePayloads: [String: [String: Any]] = [ + "array": ["items": ["name": "item", "kind": "string"]], + "object": ["properties": [["name": "child", "kind": "string"]]], + "union": ["oneOf": [["name": "branch", "kind": "string"]]], + ] + + /// A composite property carrying a value for every inherited field. + private static func source(kind: String, extra: [String: Any] = [:]) -> [String: Any] { + var source: [String: Any] = [ + "name": "field_\(kind)", + "kind": kind, + "description": "an inherited description on a \(kind) property", + "required": true, + "nullable": true, + "default": "default_\(kind)", + "example": "example_\(kind)", + "enumValues": ["one", "two"], + ] + source.merge(compositePayloads[kind] ?? [:]) { current, _ in current } + source.merge(extra) { _, replacement in replacement } + return source + } + + /// Assert every inherited field in `expected` survived into `actual`. + private func assertInheritedFieldsPreserved( + _ actual: [String: Any], + _ expected: [String: Any], + _ label: String, + file: StaticString = #filePath, + line: UInt = #line + ) { + for key in Self.inheritedFields { + // An earlier revision exempted an empty `enumValues` standing in for an + // absent one, assuming a corrected emitter would materialize the schema + // default. The canonical rule inverts that: optional collection presence + // is semantic, so an absent collection must stay omitted and save must + // never synthesize an empty one from absent input. Materializing the + // default *is* the defect, so the exemption would have let a rule + // violation pass silently here. `OptionalCollectionPresenceTests` pins + // the rule directly; this assertion is now unconditional. + XCTAssertTrue( + Spec.equal(actual[key], expected[key]), + """ + \(label) lost inherited field '\(key)': \ + got \(Spec.describe(actual[key])), expected \(Spec.describe(expected[key])) + """, + file: file, + line: line + ) + } + } + + /// Named collections save either as a name-keyed map — the default, where the + /// entry name is the key and no longer repeated in the payload — or as an + /// already-named list. Read the sole entry from either shape, re-injecting the + /// key as `name`, so these assertions pin inherited *fields* rather than the + /// emitter's chosen collection form. Returns nil unless the collection holds + /// exactly one entry, so a multi-entry source fails loudly instead of + /// silently asserting against a positionally- vs lexicographically-chosen + /// element. + private static func soleNamedEntry(_ value: Any?) -> [String: Any]? { + switch value { + case let list as [Any]: + guard list.count == 1 else { return nil } + return list.first as? [String: Any] + case let map as [String: Any]: + guard map.count == 1, let key = map.keys.first, + var entry = map[key] as? [String: Any] + else { return nil } + if entry["name"] == nil { entry["name"] = key } + return entry + default: + return nil + } + } + + // MARK: - Serialized round trips + + /// JSON text is how a `.prompty` frontmatter property reaches other runtimes, + /// so inherited fields must survive the encode/decode pair — not just the + /// in-memory `load`/`save` pair the sibling suite covers. + func testInheritedFieldsSurviveJSONTextRoundTrip() throws { + for kind in Self.compositePayloads.keys.sorted() { + let source = Self.source(kind: kind) + let json = try Property.load(source).toJSON() + let saved = try Property.fromJSON(json).save() + assertInheritedFieldsPreserved(saved, source, "\(kind) property (JSON)") + } + } + + /// YAML is the on-disk frontmatter format, so it needs the same guarantee. + func testInheritedFieldsSurviveYAMLTextRoundTrip() throws { + for kind in Self.compositePayloads.keys.sorted() { + let source = Self.source(kind: kind) + let yaml = try Property.load(source).toYAML() + let saved = try Property.fromYAML(yaml).save() + assertInheritedFieldsPreserved(saved, source, "\(kind) property (YAML)") + } + } + + /// Repeated round trips must reach a fixed point. A subtype that drops an + /// inherited field only on the second pass would still look correct to a + /// single-pass assertion. Each pass is compared against the previous pass in + /// full, so drift in *any* field is caught, not just the inherited ones. + func testInheritedFieldsAreStableAcrossRepeatedRoundTrips() throws { + for kind in Self.compositePayloads.keys.sorted() { + let source = Self.source(kind: kind) + var previous = try Property.load(source).save() + assertInheritedFieldsPreserved(previous, source, "\(kind) property (pass 0)") + + for pass in 1...3 { + let saved = try Property.load(previous).save() + assertInheritedFieldsPreserved(saved, source, "\(kind) property (pass \(pass))") + XCTAssertTrue( + Spec.equal(saved, previous), + """ + \(kind) property drifted on pass \(pass): \ + got \(Spec.describe(saved)), previous pass was \(Spec.describe(previous)) + """ + ) + previous = saved + } + } + } + + // MARK: - Programmatic construction + + /// Values set on a Swift-constructed subtype must reach the wire. This is the + /// axis the memberwise-`init` gap sits on: if the emitter drops the stored + /// properties again, these assignments stop compiling instead of silently + /// vanishing, which is the failure mode we want. + func testInheritedFieldsSurviveProgrammaticConstruction() throws { + var array = ArrayProperty(items: .unknown(["name": "item", "kind": "string"])) + array.name = "built_array" + array.description = "built in Swift" + array.required = true + array.nullable = true + array.default = "fallback" + array.example = "sample" + array.enumValues = ["one", "two"] + + var object = ObjectProperty(properties: [.unknown(["name": "child", "kind": "string"])]) + object.name = "built_object" + object.description = "built in Swift" + object.required = true + object.nullable = true + object.default = "fallback" + object.example = "sample" + object.enumValues = ["one", "two"] + + var union = UnionProperty(oneOf: [.unknown(["name": "branch", "kind": "string"])]) + union.name = "built_union" + union.description = "built in Swift" + union.required = true + union.nullable = true + union.default = "fallback" + union.example = "sample" + union.enumValues = ["one", "two"] + + let cases: [(String, [String: Any])] = [ + ("array", try array.save()), + ("object", try object.save()), + ("union", try union.save()), + ] + + for (kind, saved) in cases { + let expected = Self.source( + kind: kind, + extra: [ + "name": "built_\(kind)", + "description": "built in Swift", + "default": "fallback", + "example": "sample", + ] + ) + assertInheritedFieldsPreserved(saved, expected, "constructed \(kind) property") + + // And the constructed value must survive a reload unchanged. + let reloaded = try Property.load(saved).save() + assertInheritedFieldsPreserved(reloaded, expected, "reloaded \(kind) property") + } + } + + // MARK: - Non-scalar inherited values + + /// The shared spec vectors only ever put scalars in `default` / `example` / + /// `enumValues`. Composite subtypes are exactly where structured values show + /// up in practice, so exercise them explicitly. + func testNonScalarInheritedValuesSurviveRoundTrip() throws { + let structured: [String: Any] = [ + "default": ["nested": ["deep": [1, 2, 3]], "flag": true], + "example": [["id": 1, "label": "first"], ["id": 2, "label": "second"]], + "enumValues": [["tier": "gold"], ["tier": "silver"]], + ] + + for kind in Self.compositePayloads.keys.sorted() { + let source = Self.source(kind: kind, extra: structured) + + let saved = try Property.load(source).save() + assertInheritedFieldsPreserved(saved, source, "\(kind) property (structured)") + + // Structured values must survive serialization too, not just the + // dictionary path — JSON is where type coercion tends to flatten them. + let json = try Property.load(source).toJSON() + let decoded = try Property.fromJSON(json).save() + assertInheritedFieldsPreserved(decoded, source, "\(kind) property (structured, JSON)") + } + } + + // MARK: - Nested composites + + /// Object children each carry their own inherited fields. A subtype that only + /// restored base fields at the top level would pass every other test here. + func testInheritedFieldsSurviveOnNestedObjectChildren() throws { + let source: [String: Any] = [ + "name": "envelope", + "kind": "object", + "description": "outer", + "required": true, + "properties": [ + [ + "name": "inner_array", + "kind": "array", + "description": "a nested array", + "required": true, + "nullable": true, + "default": ["seeded"], + "example": ["sampled"], + "enumValues": ["one", "two"], + "items": ["name": "cell", "kind": "string", "description": "a cell", "required": true], + ] + ], + ] + + let property = try Property.load(source) + let child = try XCTUnwrap(property.objectProperties.first) + + XCTAssertEqual(child.name, "inner_array") + XCTAssertEqual(child.propertyDescription, "a nested array") + XCTAssertTrue(child.isRequired) + XCTAssertTrue(child.isNullable) + XCTAssertEqual(child.arrayItems?.propertyDescription, "a cell") + + // Assert the child's inherited fields on the saved payload rather than + // comparing whole dictionaries, so newly-materialized schema defaults from + // a corrected emitter don't fail this for the wrong reason. + let saved = try property.save() + assertInheritedFieldsPreserved(saved, source, "outer object property") + + let savedChild = try XCTUnwrap( + Self.soleNamedEntry(saved["properties"]), "outer object lost its 'properties' entirely") + let expectedChild = try XCTUnwrap(Self.soleNamedEntry(source["properties"])) + assertInheritedFieldsPreserved(savedChild, expectedChild, "nested array child") + + let savedItems = try XCTUnwrap( + savedChild["items"] as? [String: Any], "nested array child lost its 'items'") + let expectedItems = try XCTUnwrap(expectedChild["items"] as? [String: Any]) + assertInheritedFieldsPreserved(savedItems, expectedItems, "nested array item") + } + + /// Union branches are properties in their own right, so each branch must keep + /// its inherited fields through a round trip. `oneOf` and `anyOf` are separate + /// generated fields and are checked separately. + func testInheritedFieldsSurviveOnUnionBranches() throws { + let branch: [String: Any] = [ + "name": "branch", + "kind": "array", + "description": "a union branch", + "required": true, + "nullable": true, + "default": ["seeded"], + "example": ["sampled"], + "enumValues": ["one", "two"], + "items": ["name": "item", "kind": "string", "description": "an item"], + ] + + for composition in ["oneOf", "anyOf"] { + let source: [String: Any] = [ + "name": "choice", + "kind": "union", + "description": "outer union", + "required": true, + composition: [branch], + ] + + let saved = try Property.load(source).save() + assertInheritedFieldsPreserved(saved, source, "outer union ('\(composition)')") + + let branches = try XCTUnwrap( + saved[composition] as? [Any], + "union lost its '\(composition)' branches entirely" + ) + let first = try XCTUnwrap(branches.first as? [String: Any]) + assertInheritedFieldsPreserved(first, branch, "union '\(composition)' branch") + + // The branch's own nested item must keep its inherited fields too. + let items = try XCTUnwrap( + first["items"] as? [String: Any], "union '\(composition)' branch lost its 'items'") + let expectedItems = try XCTUnwrap(branch["items"] as? [String: Any]) + assertInheritedFieldsPreserved(items, expectedItems, "union '\(composition)' branch item") + + // Only the populated composition field may be emitted. + let other = composition == "oneOf" ? "anyOf" : "oneOf" + XCTAssertNil(saved[other], "union emitted '\(other)' alongside '\(composition)'") + } + } + + /// A composite nested inside a composite — three levels of the recursive + /// `Property` shape. `Property` nests without limit, so this is a + /// representative depth, not an exhaustive one; it guards against a fix that + /// only walks one level down. + func testInheritedFieldsSurviveDeeplyNestedComposites() throws { + let source: [String: Any] = [ + "name": "rows", + "kind": "array", + "description": "outer array", + "required": true, + "items": [ + "name": "row", + "kind": "object", + "description": "a row", + "nullable": true, + "properties": [ + [ + "name": "values", + "kind": "union", + "description": "a cell value", + "required": true, + "enumValues": ["one", "two"], + "anyOf": [ + ["name": "as_text", "kind": "string", "description": "text form", "required": true] + ], + ] + ], + ], + ] + + let property = try Property.load(source) + let row = try XCTUnwrap(property.arrayItems) + XCTAssertEqual(row.propertyDescription, "a row") + XCTAssertTrue(row.isNullable) + + let cell = try XCTUnwrap(row.objectProperties.first) + XCTAssertEqual(cell.propertyDescription, "a cell value") + XCTAssertTrue(cell.isRequired) + XCTAssertTrue(Spec.equal(cell.enumValues, ["one", "two"])) + + // Walk the saved payload level by level, asserting inherited fields at each + // depth instead of comparing whole dictionaries. + let saved = try property.save() + assertInheritedFieldsPreserved(saved, source, "level 1 (array)") + + let savedRow = try XCTUnwrap(saved["items"] as? [String: Any], "level 1 lost 'items'") + let expectedRow = try XCTUnwrap(source["items"] as? [String: Any]) + assertInheritedFieldsPreserved(savedRow, expectedRow, "level 2 (object)") + + let savedCell = try XCTUnwrap( + Self.soleNamedEntry(savedRow["properties"]), "level 2 lost 'properties'") + let expectedCell = try XCTUnwrap(Self.soleNamedEntry(expectedRow["properties"])) + assertInheritedFieldsPreserved(savedCell, expectedCell, "level 3 (union)") + + let savedBranch = try XCTUnwrap( + (savedCell["anyOf"] as? [Any])?.first as? [String: Any], "level 3 lost 'anyOf'") + let expectedBranch = try XCTUnwrap((expectedCell["anyOf"] as? [Any])?.first as? [String: Any]) + assertInheritedFieldsPreserved(savedBranch, expectedBranch, "level 4 (union branch)") + } + + // MARK: - Falsy and explicitly-null inherited values + + /// Inherited fields set to their *default-looking* values must still survive. + /// `save()` guards each field differently — `name` is dropped when empty while + /// `required` is emitted whenever non-nil — so a field carrying `false`, `""` + /// or `[]` is the case most likely to be quietly discarded. + func testFalsyInheritedValuesSurviveRoundTrip() throws { + for kind in Self.compositePayloads.keys.sorted() { + let source = Self.source( + kind: kind, + extra: [ + "required": false, + "nullable": false, + "default": "", + "example": 0, + "enumValues": [], + ] + ) + + let saved = try Property.load(source).save() + for key in ["required", "nullable", "default", "example", "enumValues"] { + XCTAssertNotNil(saved[key], "\(kind) property dropped falsy inherited field '\(key)'") + XCTAssertTrue( + Spec.equal(saved[key], source[key]), + """ + \(kind) property corrupted falsy inherited field '\(key)': \ + got \(Spec.describe(saved[key])), expected \(Spec.describe(source[key])) + """ + ) + } + } + } + + /// Structured values must survive YAML as well as JSON — YAML is the on-disk + /// frontmatter format, and block/flow collection handling is a separate code + /// path from JSON encoding. + func testNonScalarInheritedValuesSurviveYAMLRoundTrip() throws { + let structured: [String: Any] = [ + "default": ["nested": ["deep": [1, 2, 3]], "flag": true], + "example": [["id": 1, "label": "first"], ["id": 2, "label": "second"]], + "enumValues": [["tier": "gold"], ["tier": "silver"]], + ] + + for kind in Self.compositePayloads.keys.sorted() { + let source = Self.source(kind: kind, extra: structured) + let yaml = try Property.load(source).toYAML() + let saved = try Property.fromYAML(yaml).save() + assertInheritedFieldsPreserved(saved, source, "\(kind) property (structured, YAML)") + } + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/LineEndingTests.swift b/runtime/swift/prompty/Tests/PromptyTests/LineEndingTests.swift new file mode 100644 index 000000000..a02266555 --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/LineEndingTests.swift @@ -0,0 +1,374 @@ +import Foundation +import PromptyModel +import XCTest + +/// Guards for Windows line endings. +/// +/// Swift treats a CRLF pair as a single `Character`, so splitting text on a +/// literal `"\n"` does not split a CRLF document at all — it returns the whole +/// thing as one line. Rust splits at byte level and degrades gracefully, so +/// this is a Swift-specific hazard with no counterpart in the reference +/// implementation, and nothing in the LF-only spec vectors can catch it. +@testable import Prompty +@testable import PromptyOpenAI + +final class LineEndingTests: XCTestCase { + + /// A prompt assembled in memory with Windows line endings must parse. + /// + /// The loader normalizes files it reads, but a prompt built from a dictionary + /// never passes through it, so every role marker was missed and parsing + /// produced one lumped message. + func testParserHandlesCarriageReturnLineEndings() async throws { + let agent = try Agent.load([ + "kind": "prompt", + "name": "crlf", + "instructions": "system:\r\nYou are helpful.\r\n\r\nuser:\r\nHello there.", + ]) + + let messages = try await Pipeline.prepare(agent) + XCTAssertEqual(messages.map(\.role.rawValue), ["system", "user"]) + XCTAssertEqual(Self.text(messages[0].parts), "You are helpful.") + XCTAssertEqual(Self.text(messages[1].parts), "Hello there.") + } + + /// CRLF and LF forms of the same prompt must produce identical messages. + func testCarriageReturnMatchesUnixParse() throws { + let unix = "system:\nOne.\n\nuser:\nTwo.\n\nassistant:\nThree." + let windows = unix.replacingOccurrences(of: "\n", with: "\r\n") + + let expected = PromptyChatParser.parseChat(unix) + let actual = PromptyChatParser.parseChat(windows) + + XCTAssertEqual(expected.count, 3) + XCTAssertEqual(actual.map(\.role.rawValue), expected.map(\.role.rawValue)) + XCTAssertEqual(actual.map { Self.text($0.parts) }, expected.map { Self.text($0.parts) }) + } + + /// Role markers must still be found after `preRender` rewrites the template. + func testPreRenderRewritesCarriageReturnMarkers() throws { + let parser = PromptyChatParser() + let result = try parser.preRender(template: "system:\r\nOne.\r\n\r\nuser:\r\nTwo.") + let prepared = try XCTUnwrap(result as? PreRenderResult) + + let nonce = try XCTUnwrap(prepared.context["nonce"] as? String) + XCTAssertTrue(prepared.text.contains("system[nonce=\"\(nonce)\"]:")) + XCTAssertTrue(prepared.text.contains("user[nonce=\"\(nonce)\"]:")) + } + + /// A journal written with CRLF endings must replay. + /// + /// `readRecords` split on a literal `"\n"`, so a CRLF journal parsed as a + /// single unparseable line and silently verified as empty. + func testJournalReadsCarriageReturnRecords() throws { + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent("prompty-crlf-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: directory) } + + let path = directory.appendingPathComponent("journal.jsonl") + let journal = #"{"type":"session_start"}"# + "\r\n" + #"{"type":"session_end"}"# + "\r\n" + try journal.write(to: path, atomically: true, encoding: .utf8) + + let records = try JsonlEventJournalWriter.readRecords(path: path.path) + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records.map { $0["type"] as? String }, ["session_start", "session_end"]) + } + + /// A journal record containing U+2028 must survive a round trip. + /// + /// `Character.isNewline` matches U+2028, U+2029 and U+0085, all of which are + /// legal *inside* a JSON string. Splitting on them tore a record in half and + /// left both pieces unparseable, so the record was silently discarded. + func testJournalKeepsUnicodeLineSeparatorsInsideRecords() throws { + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent("prompty-u2028-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: directory) } + + let path = directory.appendingPathComponent("journal.jsonl") + // U+2028 LINE SEPARATOR and U+0085 NEXT LINE, unescaped inside the value. + let payload = "before\u{2028}after\u{0085}end" + let journal = "{\"type\":\"note\",\"text\":\"\(payload)\"}\n{\"type\":\"session_end\"}\n" + try journal.write(to: path, atomically: true, encoding: .utf8) + + let records = try JsonlEventJournalWriter.readRecords(path: path.path) + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0]["text"] as? String, payload) + } + + /// A lone CR is content, not a line ending. + /// + /// The reference loader normalizes `\r\n` and nothing else, so rewriting a + /// bare CR would alter legitimate message text and diverge from every other + /// runtime. + func testLoneCarriageReturnIsPreservedAsContent() throws { + let messages = PromptyChatParser.parseChat("user:\nbefore\rafter") + + XCTAssertEqual(messages.count, 1) + XCTAssertEqual(Self.text(messages[0].parts), "before\rafter") + } + + /// Splitting recognises the three real terminators and no others. + func testLineSplitterRecognisesOnlyCarriageReturnAndLineFeed() { + XCTAssertEqual(Lines.split("a\nb\r\nc\rd"), ["a", "b", "c", "d"]) + XCTAssertEqual(Lines.split("a\u{2028}b"), ["a\u{2028}b"]) + XCTAssertEqual(Lines.split("a\u{0085}b"), ["a\u{0085}b"]) + XCTAssertEqual(Lines.split("a\n\n\nb"), ["a", "b"]) + } + + /// A CR immediately before a CRLF must not hide the terminator. + /// + /// Normalizing and then splitting on characters is not enough: rewriting + /// `"a\r\r\nb"` once yields `"a\r\nb"`, whose CR and LF are now adjacent and + /// collapse into a single grapheme, so a character-level split misses it + /// again. The scalar scanner has no such blind spot. + func testAdjacentCarriageReturnsStillSplit() { + // `split` treats a lone CR as a terminator, `splitLineFeeds` as content — + // but neither may miss the CRLF that follows it. Before the scalar scanner + // both returned the whole string as a single line. + XCTAssertEqual(Lines.split("a\r\r\nb"), ["a", "b"]) + XCTAssertEqual(Lines.splitLineFeeds("a\r\r\nb"), ["a\r", "b"]) + + let messages = PromptyChatParser.parseChat("system:\r\r\nOne.\r\r\n\r\r\nuser:\r\r\nTwo.") + XCTAssertEqual(messages.map(\.role.rawValue), ["system", "user"]) + } + + /// Empty, terminator-only, and trailing-terminator inputs. + /// + /// `splitLineFeeds` stands in for `components(separatedBy:)` in the parser, so + /// it has to agree with it on the trailing empty segment or turn content + /// silently changes shape. + func testLineSplitterEdgeCases() { + XCTAssertEqual(Lines.split(""), []) + XCTAssertEqual(Lines.split("\n\r\n\r"), []) + XCTAssertEqual(Lines.split("a\n"), ["a"]) + + XCTAssertEqual(Lines.splitLineFeeds(""), [""]) + XCTAssertEqual(Lines.splitLineFeeds("a\n"), ["a", ""]) + XCTAssertEqual(Lines.splitLineFeeds("a\r\n"), ["a", ""]) + // Agreement with the spelling it replaced, for LF-only text. + for sample in ["", "a", "a\nb", "a\n", "\na", "a\n\nb"] { + XCTAssertEqual( + Lines.splitLineFeeds(sample), sample.components(separatedBy: "\n"), + "diverged from components(separatedBy:) for \(sample.debugDescription)") + } + } + + /// A lone CR must not be promoted to a terminator by normalization. + func testNormalizeRewritesOnlyCarriageReturnLineFeed() { + XCTAssertEqual(Lines.normalizeCRLF("a\r\nb"), "a\nb") + XCTAssertEqual(Lines.normalizeCRLF("a\rb"), "a\rb") + XCTAssertEqual(Lines.normalizeCRLF("a\r\r\nb"), "a\r\nb") + XCTAssertEqual(Lines.normalizeCRLF("a\n\rb"), "a\n\rb") + XCTAssertEqual(Lines.normalizeCRLF("plain"), "plain") + } + + private static func text(_ parts: [ContentPart]) -> String { + parts.compactMap { part -> String? in + if case .textPart(let text) = part { return text.value } + return nil + }.joined() + } + + // MARK: - Frontmatter + + /// `Frontmatter.split` must not depend on its caller having normalized first. + /// + /// It scanned with `firstIndex(of: "\n")`, which never matches in a CRLF + /// document because the pair is one `Character`. The opening delimiter looked + /// unterminated, so both the frontmatter and the body came back empty and a + /// Windows-authored file was silently discarded with no error. + func testFrontmatterSplitHandlesCarriageReturnDelimiters() throws { + let raw = "---\r\nname: crlf\r\ndescription: windows\r\n---\r\nsystem:\r\nHello.\r\n" + let (frontmatter, body) = try Frontmatter.split(raw) + + XCTAssertEqual(frontmatter["name"] as? String, "crlf") + XCTAssertEqual(frontmatter["description"] as? String, "windows") + XCTAssertEqual(body, "system:\nHello.\n") + } + + /// The `+++` delimiter and an indented opener take the same scalar path. + func testFrontmatterSplitHandlesCarriageReturnAlternateDelimiters() throws { + let (frontmatter, body) = try Frontmatter.split("+++\r\nname: toml-style\r\n+++\r\nBody.") + XCTAssertEqual(frontmatter["name"] as? String, "toml-style") + XCTAssertEqual(body, "Body.") + + let indented = try Frontmatter.split("\r\n\r\n ---\r\nname: indented\r\n --- \r\nBody.") + XCTAssertEqual(indented.frontmatter["name"] as? String, "indented") + XCTAssertEqual(indented.body, "Body.") + } + + /// Blank-line skipping must use `isWhitespace`, not `CharacterSet.whitespaces`. + /// + /// `.whitespaces` is horizontal only: vertical tab, form feed, NEL, and the + /// Unicode separators are excluded. Treating such a line as non-blank would + /// make the opener unreachable and silently demote the file to body-only. + func testFrontmatterSplitSkipsVerticalWhitespaceBeforeDelimiter() throws { + for blank in ["\u{000B}", "\u{000C}", "\u{0085}", "\u{2028}", "\u{2029}"] { + let (frontmatter, body) = try Frontmatter.split( + "\(blank)\n---\nname: after-blank\n---\nBody.") + XCTAssertEqual( + frontmatter["name"] as? String, "after-blank", + "U+\(String(format: "%04X", blank.unicodeScalars.first!.value)) should count as blank") + XCTAssertEqual(body, "Body.") + } + } + + /// CRLF and LF forms of one document must split identically. + func testFrontmatterSplitCarriageReturnMatchesUnix() throws { + let unix = "---\nname: same\nmodel:\n id: gpt-4o\n---\nsystem:\nOne.\n\nuser:\nTwo.\n" + let windows = unix.replacingOccurrences(of: "\n", with: "\r\n") + + let expected = try Frontmatter.split(unix) + let actual = try Frontmatter.split(windows) + + XCTAssertEqual(actual.frontmatter["name"] as? String, expected.frontmatter["name"] as? String) + XCTAssertEqual(actual.body, expected.body) + } + + /// A document with no frontmatter is still line-ending normalized. + /// + /// `split` owns the single normalization pass in the load path, so every + /// return — including the body-only one — hands back LF. + func testFrontmatterSplitWithoutDelimiterNormalizesCarriageReturns() throws { + let (frontmatter, body) = try Frontmatter.split("system:\r\nNo frontmatter here.\r\n") + + XCTAssertTrue(frontmatter.isEmpty) + XCTAssertEqual(body, "system:\nNo frontmatter here.\n") + } + + /// `\r\r\n` must keep its lone CR: exactly one normalization pass runs. + /// + /// The scalar scanner reads the CR + LF pair as the terminator and leaves the + /// preceding CR as content. A second pass over the already-normalized text + /// would read the residual `\r\n` as another terminator and delete that CR — + /// which is why `Loader` no longer normalizes before calling in. + func testAdjacentCarriageReturnSurvivesSingleNormalizationPass() throws { + let (_, body) = try Frontmatter.split("---\nname: cr\n---\nbefore\r\r\nafter") + XCTAssertEqual(body, "before\r\nafter") + + let bodyOnly = try Frontmatter.split("before\r\r\nafter") + XCTAssertEqual(bodyOnly.body, "before\r\nafter") + } + + /// An unterminated CRLF document must still be reported, not silently emptied. + func testFrontmatterSplitReportsUnclosedCarriageReturnDelimiter() { + XCTAssertThrowsError(try Frontmatter.split("---\r\nname: unclosed\r\nstill: yaml\r\n")) { + error in + guard case LoadError.invalidFrontmatter = error else { + return XCTFail("expected invalidFrontmatter, got \(error)") + } + } + } + + // MARK: - Files on disk + + /// The end-to-end Windows case: a `.prompty` file whose bytes contain CRLF. + /// + /// Every other line-ending test builds its input in memory, so none of them + /// exercises reading a file off disk and normalizing what came back. + func testLoadsPromptyFileWrittenWithCarriageReturnBytes() async throws { + let unix = """ + --- + name: crlf-file + model: + id: gpt-4o + --- + system: + You are helpful. + + user: + Hello there. + + """ + let path = try Self.writeTemporaryPrompt( + unix.replacingOccurrences(of: "\n", with: "\r\n")) + defer { try? FileManager.default.removeItem(at: path.deletingLastPathComponent()) } + + let agent = try Loader.load(path: path.path) + XCTAssertEqual(agent.name, "crlf-file") + XCTAssertEqual(agent.instructions, "system:\nYou are helpful.\n\nuser:\nHello there.") + + // The normalized instructions must still parse into distinct turns. + let messages = try await Pipeline.prepare(agent) + XCTAssertEqual(messages.map(\.role.rawValue), ["system", "user"]) + XCTAssertEqual(Self.text(messages[1].parts), "Hello there.") + } + + /// End-to-end: a lone CR inside a `\r\r\n` sequence survives the load path. + /// + /// This is the composition regression — `Loader` must not normalize before + /// `Frontmatter.split`, or the two passes together delete the CR. + func testLoadedInstructionsKeepLoneCarriageReturn() throws { + let raw = "---\r\nname: adjacent-cr\r\n---\r\nsystem:\r\nbefore\r\r\nafter\r\n" + let path = try Self.writeTemporaryPrompt(raw) + defer { try? FileManager.default.removeItem(at: path.deletingLastPathComponent()) } + + let agent = try Loader.load(path: path.path) + XCTAssertEqual(agent.instructions, "system:\nbefore\r\nafter") + } + + /// A CRLF file and its LF twin must load to the same prompt. + func testCarriageReturnFileMatchesUnixFile() throws { + let unix = """ + --- + name: twin + description: line endings must not matter + model: + id: gpt-4o + --- + system: + Identical. + + user: + Content. + + """ + let unixPath = try Self.writeTemporaryPrompt(unix) + let windowsPath = try Self.writeTemporaryPrompt( + unix.replacingOccurrences(of: "\n", with: "\r\n")) + defer { + try? FileManager.default.removeItem(at: unixPath.deletingLastPathComponent()) + try? FileManager.default.removeItem(at: windowsPath.deletingLastPathComponent()) + } + + let expected = try Loader.load(path: unixPath.path) + let actual = try Loader.load(path: windowsPath.path) + + XCTAssertEqual(actual.name, expected.name) + XCTAssertEqual(actual.description, expected.description) + XCTAssertEqual(actual.instructions, expected.instructions) + } + + // MARK: - Server-sent events + + /// A `data:` line may still carry its CR when the transport splits on LF. + /// + /// `CharacterSet.whitespaces` is space and tab only, so a trailing CR survived + /// trimming and `[DONE]` never compared equal. + func testServerSentEventPayloadToleratesTrailingCarriageReturn() { + XCTAssertEqual(SSE.payload(of: "data: [DONE]\r"), "[DONE]") + XCTAssertEqual(SSE.payload(of: "data: {\"id\":\"a\"}\r"), "{\"id\":\"a\"}") + XCTAssertEqual(SSE.payload(of: "data: [DONE]"), "[DONE]") + XCTAssertNil(SSE.payload(of: "data:\r")) + XCTAssertNil(SSE.payload(of: ": keep-alive\r")) + } + + /// A CRLF-delimited SSE body must split into one record per event. + func testServerSentEventStreamSplitsCarriageReturnDelimitedBody() { + let body = "data: {\"n\":1}\r\n\r\ndata: {\"n\":2}\r\n\r\ndata: [DONE]\r\n" + let payloads = Lines.split(body).compactMap(SSE.payload(of:)) + XCTAssertEqual(payloads, ["{\"n\":1}", "{\"n\":2}", "[DONE]"]) + } + + private static func writeTemporaryPrompt(_ contents: String) throws -> URL { + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent("prompty-line-endings-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + let path = directory.appendingPathComponent("prompt.prompty") + // Write bytes directly: a String write would be re-encoded by the platform. + try Data(contents.utf8).write(to: path) + return path + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/LiveOpenAITests.swift b/runtime/swift/prompty/Tests/PromptyTests/LiveOpenAITests.swift new file mode 100644 index 000000000..c5cec409d --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/LiveOpenAITests.swift @@ -0,0 +1,376 @@ +import Foundation + +import PromptyModel + +import PromptyOpenAI + +import XCTest + +/// End-to-end tests against the real OpenAI API. +/// +/// These are the only tests that leave the machine. They skip themselves when +/// `OPENAI_API_KEY` is absent, so a checkout without credentials still runs a +/// full green suite. Credentials come from a `.env` beside the package, which +/// is ignored by git and never committed. +/// +/// The runtime itself never reads `.env` — populating the environment is the +/// host's job, so the loading below belongs to the test, not the library. +@testable import Prompty + +// MARK: - .env loading + +/// Load `KEY=VALUE` lines from a `.env` beside the package into the process +/// environment, without overwriting values that are already set. +final class LiveOpenAITests: XCTestCase { + + private static let loaded: Bool = { + loadDotEnv() + Registry.shared.registerDefaults() + registerOpenAI() + return true + }() + + override func setUp() { + super.setUp() + _ = Self.loaded + } + + /// Skip rather than fail when the environment has no credentials. + private func requireCredentials() throws -> String { + let key = ProcessInfo.processInfo.environment["OPENAI_API_KEY"] ?? "" + try XCTSkipIf(key.isEmpty, "OPENAI_API_KEY not set — skipping live OpenAI tests") + return key + } + + private var modelId: String { + let configured = ProcessInfo.processInfo.environment["OPENAI_MODEL"] ?? "" + return configured.isEmpty ? "gpt-4o-mini" : configured + } + + // MARK: - Chat + + /// The whole pipeline against a real endpoint: load, render, parse, execute, + /// process. + func testLiveChatCompletion() async throws { + _ = try requireCredentials() + + let agent = try Agent.load([ + "kind": "prompt", + "name": "live-chat", + "model": ["id": modelId, "provider": "openai", "apiType": "chat"], + "inputs": [["name": "topic", "kind": "string"]], + "instructions": + "system:\nAnswer with a single word and no punctuation.\n\nuser:\nWhat colour is a {{topic}}?", + ]) + + let result = try await Pipeline.invoke(agent, inputs: ["topic": "banana"]) + let text = try XCTUnwrap( + result as? String, "expected text content, got \(String(describing: result))") + + XCTAssertFalse(text.isEmpty) + XCTAssertTrue( + text.lowercased().contains("yellow"), + "expected the model to answer 'yellow', got \(text.debugDescription)") + } + + /// Model options must actually reach the provider. + /// + /// Asserted relatively: the same prompt is run twice, once capped and once + /// with room to answer. An absolute check on the capped reply alone would + /// also pass if the model simply happened to be terse. + func testLiveChatHonoursMaxOutputTokens() async throws { + _ = try requireCredentials() + + func answer(maxOutputTokens: Int) async throws -> String { + let agent = try Agent.load([ + "kind": "prompt", + "name": "live-capped", + "model": [ + "id": modelId, "provider": "openai", "apiType": "chat", + "options": ["temperature": 0, "maxOutputTokens": maxOutputTokens], + ], + "instructions": "user:\nCount slowly from one to one hundred in words.", + ]) + let result = try await Pipeline.invoke(agent) + return try XCTUnwrap(result as? String) + } + + let capped = try await answer(maxOutputTokens: 16) + let generous = try await answer(maxOutputTokens: 800) + + XCTAssertFalse(capped.isEmpty) + XCTAssertFalse(generous.isEmpty) + XCTAssertGreaterThan( + generous.count, capped.count * 3, + "maxOutputTokens did not reach the provider — a 16-token cap and an " + + "800-token cap produced comparable output: " + + "capped=\(capped.count) generous=\(generous.count)") + } + + // MARK: - Streaming + + /// Streaming has to deliver the answer as a sequence of events, not one blob. + /// + /// This asserts on decoded SSE events rather than on wire timing. On Windows + /// `URLSession.bytes(for:)` is unavailable, so the provider buffers the + /// response and then decodes it; requiring many events still proves the SSE + /// framing and chunk accumulation are right, which is the part this runtime + /// owns. + func testLiveStreaming() async throws { + _ = try requireCredentials() + + let agent = try Agent.load([ + "kind": "prompt", + "name": "live-stream", + "model": [ + "id": modelId, "provider": "openai", "apiType": "chat", + "options": ["temperature": 0, "additionalProperties": ["stream": true]], + ], + "instructions": + "user:\nList the numbers one through twenty in words, one per line, and nothing else.", + ]) + + let messages = try await Pipeline.prepare(agent) + let stream = try await Pipeline.stream(agent, messages: messages) + + var chunks = 0 + var text = "" + for try await chunk in stream { + if case .textChunk(let part) = chunk { + chunks += 1 + text += part.value + } + } + + // More than one event proves the body was decoded as a stream of SSE frames + // rather than handed over as a single blob. The floor is deliberately loose: + // a provider may legally coalesce deltas, so the real fidelity check is the + // ordered content assertion below. + XCTAssertGreaterThan(chunks, 1, "expected several streamed events, got \(chunks)") + + // Every item, in order, must survive reassembly. This is what a dropped or + // misordered chunk would break, and unlike comparing against a second + // request it does not assume the model is deterministic across calls. + let words = [ + "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", + "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", + "eighteen", "nineteen", "twenty", + ] + let lowered = text.lowercased() + var cursor = lowered.startIndex + for word in words { + guard let found = lowered.range(of: word, range: cursor.." + let input = vector["input"] as? [String: Any] ?? [:] + let expected = vector["expected"] as? [String: Any] ?? [:] + + run.check(name) { + try Self.runVector(name: name, input: input, expected: expected) + } + } + + run.assertClean() + } + + /// Pin `tools_function_load`'s bindings map: exactly one entry, key `unit`, + /// input `preferred_unit`. + /// + /// `validateBindings` alone cannot hold this. It is opt-in by key — it + /// returns early when a vector carries no `bindings` expectation — so + /// deleting that block from `load_vectors.json` leaves every load vector + /// green while the loader is free to drop bindings entirely. That is not + /// hypothetical: removing the block was measured, and the suite stayed at 0 + /// failures. This test asserts the expectation still *exists* and still says + /// what it is supposed to say, then checks the loaded tool against those + /// same literals, so neither half can quietly go missing. + /// + /// Bindings are read off `FunctionTool` rather than the `Tool.bindings` + /// convenience shim, so the generated type is what is under test. + func testFunctionToolBindingsArePinned() throws { + let vectors = try Spec.vectors("load") + let vector = try XCTUnwrap( + vectors.first { $0["name"] as? String == "tools_function_load" }, + "tools_function_load vector missing from load_vectors.json") + + // Half one: the vector still declares the expectation, in either of the two + // shapes validateBindings treats as equivalent. + let expected = try XCTUnwrap(vector["expected"] as? [String: Any]) + let expectedTools = try XCTUnwrap(expected["tools"] as? [[String: Any]]) + let declared = try XCTUnwrap( + expectedTools.first?["bindings"], + """ + tools_function_load lost its bindings expectation. validateBindings skips \ + absent keys, so removing it disables the check without failing anything. + """) + let declaredPairs = try Self.bindingPairs(declared) + XCTAssertEqual(declaredPairs.count, 1, "expected exactly one declared binding") + XCTAssertEqual( + declaredPairs["unit"], "preferred_unit", + "expected unit -> preferred_unit; vector declares \(declaredPairs)") + + // Half two: the loaded FunctionTool matches those literals exactly. + let agent = try Self.loadAgent(try XCTUnwrap(vector["input"] as? [String: Any])) + let tools = try XCTUnwrap(agent.tools, "fixture produced no tools") + XCTAssertEqual(tools.count, 1, "fixture declares one tool") + // Unwrap rather than subscript: XCTAssertEqual records but does not halt, so + // indexing here would trap instead of failing cleanly on an empty list. + let loaded = try XCTUnwrap(tools.first, "fixture produced no tools") + + guard case .functionTool(let function) = loaded else { + return XCTFail("expected a FunctionTool, got \(loaded.kindName)") + } + let bindings = try XCTUnwrap(function.bindings, "FunctionTool.bindings is nil") + + XCTAssertEqual(bindings.count, 1, "binding count") + XCTAssertEqual(bindings.map(\.name), ["unit"], "binding key") + XCTAssertEqual(bindings.map(\.input), ["preferred_unit"], "binding input") + } + + /// Normalize a vector's `bindings` expectation to `name -> input` pairs. + /// + /// `validateBindings` accepts a `Record` map and an already-named + /// list as equivalent. Pinning only the map form would report a re-emission + /// in the list form as a *missing* expectation, which is false and sends the + /// reader somewhere useless. + private static func bindingPairs(_ declared: Any) throws -> [String: String] { + func input(_ value: Any, for name: String) throws -> String { + guard let input = ((value as? [String: Any])?["input"] ?? value) as? String else { + throw VectorFailure("binding '\(name)' expectation has no string 'input'") + } + return input + } + + if let map = declared as? [String: Any] { + return try map.reduce(into: [:]) { pairs, entry in + pairs[entry.key] = try input(entry.value, for: entry.key) + } + } + + if let list = declared as? [[String: Any]] { + return try list.reduce(into: [:]) { pairs, entry in + guard let name = entry["name"] as? String else { + throw VectorFailure("bindings list entry has no string 'name': \(entry)") + } + // Reject a duplicate rather than letting the later entry overwrite the + // earlier one. A silent collapse shrinks the expectation set, so the + // vector would keep passing while checking fewer bindings than it + // declares — the shared duplicate-name failure mode, applied to the + // expectation side. + guard pairs[name] == nil else { + throw VectorFailure("bindings list declares '\(name)' more than once") + } + pairs[name] = try input(entry, for: name) + } + } + + throw VectorFailure("bindings expectation is neither a map nor a list: \(declared)") + } + + // MARK: - Dispatch + + private static func runVector( + name: String, input: [String: Any], expected: [String: Any] + ) throws { + let env = input["env"] as? [String: Any] ?? [:] + + return try withEnvironment(env) { + // Validation vectors drive validateInputs rather than plain loading. + if expected["validated_inputs"] != nil || expected["error_field"] != nil { + try runValidationVector(name: name, input: input, expected: expected) + return + } + + if let expectedError = expected["error"] as? String { + try runErrorVector(name: name, input: input, expectedError: expectedError) + return + } + + let agent = try loadAgent(input) + try validate(agent: agent, expected: expected) + } + } + + // MARK: - Loading + + /// Build a `Agent` from whichever input form the vector uses. + static func loadAgent(_ input: [String: Any]) throws -> Agent { + if let fixture = input["fixture"] as? String { + return try Loader.load(path: Spec.fixtures.appendingPathComponent(fixture).path) + } + + if let files = input["files"] as? [String: Any] { + let directory = try makeTempDirectory("file_res") + defer { try? FileManager.default.removeItem(at: directory) } + + for (relative, content) in files { + let target = directory.appendingPathComponent(relative) + try FileManager.default.createDirectory( + at: target.deletingLastPathComponent(), withIntermediateDirectories: true) + let text: String + if let string = content as? String { + text = string + } else { + let data = try JSONSerialization.data(withJSONObject: content, options: [.prettyPrinted]) + text = String(data: data, encoding: .utf8) ?? "" + } + try text.write(to: target, atomically: true, encoding: .utf8) + } + + let raw = try promptyDocument(frontmatter: input["frontmatter"]) + return try Loader.load( + contents: raw, basePath: directory.appendingPathComponent("virtual.prompty").path) + } + + if let raw = input["frontmatter_raw"] as? String { + return try Loader.load(contents: raw, basePath: workingFile) + } + + guard input["frontmatter"] != nil else { + throw VectorFailure("vector has no fixture, frontmatter, or frontmatter_raw") + } + let raw = try promptyDocument(frontmatter: input["frontmatter"]) + return try Loader.load(contents: raw, basePath: workingFile) + } + + /// Serialize a vector's `frontmatter` object into a `.prompty` document. + /// + /// The vectors are JSON and YAML 1.2 is a JSON superset, so emitting the + /// JSON directly as a flow mapping is both simpler and lossless — no + /// number/boolean representation round-trip through a YAML emitter. + private static func promptyDocument(frontmatter: Any?) throws -> String { + guard let mapping = frontmatter as? [String: Any], !mapping.isEmpty else { + return "---\n---\n" + } + let data = try JSONSerialization.data(withJSONObject: mapping, options: [.sortedKeys]) + guard let json = String(data: data, encoding: .utf8) else { + throw VectorFailure("frontmatter is not encodable") + } + return "---\n\(json)\n---\n" + } + + private static var workingFile: String { + FileManager.default.currentDirectoryPath + "/virtual.prompty" + } + + static func makeTempDirectory(_ suffix: String) throws -> URL { + let url = FileManager.default.temporaryDirectory + .appendingPathComponent("prompty_spec_\(suffix)_\(UUID().uuidString)") + try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) + return url + } + + // MARK: - Error vectors + + private static func runErrorVector( + name: String, input: [String: Any], expectedError: String + ) throws { + do { + let agent = try loadAgent(input) + + // The generated Template.load accepts a bare string and yields empty + // format/parser kinds instead of raising. Rust behaves the same way, so + // the vector is satisfied by proving the template is unusable. + if name == "template_string_invalid" { + let format = agent.template?.format.kind ?? "" + let parser = agent.template?.parser.kind ?? "" + try expect( + format.isEmpty && parser.isEmpty, + "expected an unusable template, got format='\(format)' parser='\(parser)'") + return + } + + throw VectorFailure("expected error containing '\(expectedError)', but load succeeded") + } catch let failure as VectorFailure { + throw failure + } catch { + try expectErrorMatches(error, expectedError) + } + } + + /// Vectors describe errors loosely (they are shared across runtimes whose + /// message wording differs), so match on significant words. + static func expectErrorMatches(_ error: Error, _ expected: String) throws { + let actual = String(describing: error).lowercased() + let wanted = expected.lowercased() + + if actual.contains(wanted) { return } + + // Vectors name errors using the Python runtime's exception class names. + if wanted.contains("filenotfound"), actual.contains("not found") { return } + if wanted.contains("valueerror") || wanted.contains("keyerror") { return } + + let significant = wanted.split(whereSeparator: { !$0.isLetter }).filter { $0.count > 3 } + if !significant.isEmpty, significant.contains(where: { actual.contains($0) }) { return } + + throw VectorFailure("error mismatch:\n expected: '\(expected)'\n actual: '\(error)'") + } + + // MARK: - Validation vectors + + private static func runValidationVector( + name: String, input: [String: Any], expected: [String: Any] + ) throws { + let agent = try loadAgent(input) + let inputs = input["inputs"] as? [String: Any] ?? [:] + + if let expectedInputs = expected["validated_inputs"] as? [String: Any] { + let validated = try Pipeline.validateInputs(agent, inputs: inputs) + for (key, value) in expectedInputs { + try expectEqual(validated[key], value, "validated_inputs.\(key)") + } + // Keys the vector omits must not have been invented. + for key in validated.keys where expectedInputs[key] == nil { + throw VectorFailure("validated_inputs has unexpected key '\(key)'") + } + return + } + + do { + _ = try Pipeline.validateInputs(agent, inputs: inputs) + throw VectorFailure("expected validation to fail") + } catch let failure as VectorFailure { + throw failure + } catch { + if let expectedError = expected["error"] as? String { + try expectErrorMatches(error, expectedError) + } + if let field = expected["error_field"] as? String { + try expect( + String(describing: error).contains(field), + "expected error to name field '\(field)', got '\(error)'") + } + } + } + + // MARK: - Assertions + + private static func validate(agent: Agent, expected: [String: Any]) throws { + if let kind = expected["kind"] as? String { + try expect(kind == "prompt", "vector expected kind=prompt, declared '\(kind)'") + } + if let name = expected["name"] as? String { + try expectEqual(agent.name, name, "name") + } + if let description = expected["description"] as? String { + try expectEqual(agent.description, description, "description") + } + if let instructions = expected["instructions"] as? String { + try expectEqual(agent.instructions, instructions, "instructions") + } + + if let model = expected["model"] { + if model is NSNull { + try expect(agent.model?.id.isEmpty ?? true, "expected no model, got id='\(agent.model?.id ?? "")'") + } else if let model = model as? [String: Any] { + try validateModel(try XCTUnwrap(agent.model), expected: model) + } + } + + if let inputs = expected["inputs"] { + try validateProperties(agent.inputs, expected: inputs, label: "inputs") + } + if let outputs = expected["outputs"] { + try validateProperties(agent.outputs, expected: outputs, label: "outputs") + } + + if let tools = expected["tools"] { + if tools is NSNull { + try expect( + (agent.tools ?? []).isEmpty, "expected no tools, got \((agent.tools ?? []).count)") + } else if let expectedTools = tools as? [[String: Any]] { + let actual = agent.tools ?? [] + try expect( + actual.count == expectedTools.count, + "tools count: expected \(expectedTools.count), got \(actual.count)") + for (index, expectedTool) in expectedTools.enumerated() { + try validateTool(actual[index], expected: expectedTool, index: index) + } + } + } + + if let template = expected["template"] as? [String: Any] { + if let format = template["format"] as? [String: Any], let kind = format["kind"] as? String { + try expectEqual(agent.template?.format.kind, kind, "template.format.kind") + } + if let parser = template["parser"] as? [String: Any], let kind = parser["kind"] as? String { + try expectEqual(agent.template?.parser.kind, kind, "template.parser.kind") + } + } + + if let metadata = expected["metadata"] as? [String: Any] { + for (key, value) in metadata { + try expectEqual(agent.metadata?[key], value, "metadata.\(key)") + } + } + } + + private static func validateModel(_ model: Model, expected: [String: Any]) throws { + if let id = expected["id"] as? String { + try expectEqual(model.id, id, "model.id") + } + if let provider = expected["provider"] as? String { + try expectEqual(model.provider, provider, "model.provider") + } + if let apiType = expected["apiType"] as? String { + try expectEqual(model.apiType?.rawValue, apiType, "model.apiType") + } + + if let connection = expected["connection"] as? [String: Any] { + let actual = (try? model.connection?.save()) ?? [:] + for (key, value) in connection { + try expectEqual(actual[key], value, "model.connection.\(key)") + } + } + + if let options = expected["options"] as? [String: Any] { + let actual = (try? model.options?.save()) ?? [:] + for (key, value) in options { + try expectEqual(actual[key], value, "model.options.\(key)") + } + } + } + + private static func validateProperties( + _ actual: [Property]?, expected: Any, label: String + ) throws { + if expected is NSNull { + try expect((actual ?? []).isEmpty, "expected no \(label), got \((actual ?? []).count)") + return + } + guard let expectedList = expected as? [[String: Any]] else { return } + let properties = actual ?? [] + try expect( + properties.count == expectedList.count, + "\(label) count: expected \(expectedList.count), got \(properties.count)") + + for (index, expectedProperty) in expectedList.enumerated() { + let property = properties[index] + if let name = expectedProperty["name"] as? String { + try expectEqual(property.name, name, "\(label)[\(index)].name") + } + if let kind = expectedProperty["kind"] as? String { + try expectEqual(property.kindName, kind, "\(label)[\(index)].kind") + } + if let value = expectedProperty["default"] { + try expectEqual(property.defaultValue, value, "\(label)[\(index)].default") + } + } + } + + private static func validateTool(_ tool: Tool, expected: [String: Any], index: Int) throws { + let raw = tool.raw + + if let name = expected["name"] as? String { + try expectEqual(tool.name, name, "tools[\(index)].name") + } + if let kind = expected["kind"] as? String { + try expectEqual(tool.kindName, kind, "tools[\(index)].kind") + } + if let description = expected["description"] as? String { + try expectEqual(tool.toolDescription, description, "tools[\(index)].description") + } + if let strict = expected["strict"] as? Bool { + try expectEqual(raw["strict"], strict, "tools[\(index)].strict") + } + if let serverName = expected["serverName"] as? String { + try expectEqual(raw["serverName"], serverName, "tools[\(index)].serverName") + } + if let specification = expected["specification"] as? String { + try expectEqual(raw["specification"], specification, "tools[\(index)].specification") + } + if let path = expected["path"] as? String { + try expectEqual(raw["path"], path, "tools[\(index)].path") + } + if let mode = expected["mode"] as? String { + try expectEqual(raw["mode"], mode, "tools[\(index)].mode") + } + if let parameters = expected["parameters"] as? [[String: Any]] { + let actual = tool.functionParameters + try expect( + actual.count == parameters.count, + "tools[\(index)].parameters count: expected \(parameters.count), got \(actual.count)") + for (position, expectedParameter) in parameters.enumerated() { + if let name = expectedParameter["name"] as? String { + try expectEqual( + actual[position].name, name, "tools[\(index)].parameters[\(position)].name") + } + } + } + try validateBindings(tool, expected: expected, index: index) + } + + /// Bindings are declared either as a `Record` map — where the key + /// supplies the binding name — or as an already-named list. Both normalize to + /// the same loaded shape, so both expectation forms are checked here against + /// binding *identity* rather than the emitter's chosen representation. + /// + /// Without this the vectors' `bindings` expectations are inert: every other + /// field is opt-in by key, so an unchecked key silently passes no matter what + /// the loader produced. + /// + /// Name addressing is sound only while the expected names are unique and + /// non-empty. Object form cannot carry one key twice, and an empty key + /// disqualifies it as well, so either one proves the source used the array + /// fallback — the only ordered representation — and those entries are + /// compared positionally instead. Entries that qualify for object form stay + /// order- and representation-agnostic, because both forms are legal for them. + /// + /// Not `private`: `BindingExpectationPairingTests` drives this directly, since + /// no current fixture declares a duplicate binding name. + static func validateBindings( + _ tool: Tool, expected: [String: Any], index: Int + ) throws { + guard let declared = expected["bindings"] else { return } + let label = "tools[\(index)].bindings" + let actual = tool.bindings + + /// Read the expected input from either `{input: ...}` or a bare input name. + func expectedInput(_ spec: Any, _ entryLabel: String) throws -> String { + guard let input = ((spec as? [String: Any])?["input"] ?? spec) as? String else { + throw VectorFailure("\(entryLabel) expectation has no string 'input'") + } + return input + } + + /// Compare one expected binding, addressed by name. Sound only where the + /// expected names are unique; the list branch handles the duplicate case. + func check(name: String, spec: Any) throws { + guard let binding = actual.first(where: { $0.name == name }) else { + throw VectorFailure( + "\(label) missing '\(name)'; got \(actual.map(\.name).sorted())") + } + let input = try expectedInput(spec, "\(label)[\(name)]") + try expectEqual(binding.input, input, "\(label)[\(name)].input") + } + + // Object keys are unique by construction, and every declared key must be + // found against a matching count, so a duplicate in `actual` cannot hide + // here: it either breaks the count or leaves an expected key missing. + if let map = declared as? [String: Any] { + // An empty name disqualifies object form, so a map expectation carrying + // one is malformed rather than something to address by name. + guard !map.keys.contains("") else { + throw VectorFailure("\(label) map expectation has an empty key; that needs list form") + } + try expect( + actual.count == map.count, + "\(label) count: expected \(map.count), got \(actual.count)") + for (name, spec) in map { + try check(name: name, spec: spec) + } + return + } + + if let list = declared as? [[String: Any]] { + try expect( + actual.count == list.count, + "\(label) count: expected \(list.count), got \(actual.count)") + + let names = try list.map { entry -> String in + guard let name = entry["name"] as? String else { + throw VectorFailure("\(label) list entry has no string 'name': \(entry)") + } + return name + } + + // Pre-scan before any name-addressed lookup. Object form cannot carry the + // same key twice, and an empty key disqualifies it too, so either one + // proves the source was the array fallback, which carries order — these + // entries are positional. Matching them by name would bind expectations + // to the same entry twice, or to an entry that merely shares a name, + // leaving the rest unverified while still reporting a pass. + let nameAddressable = Set(names).count == names.count && !names.contains("") + guard nameAddressable else { + for (position, entry) in list.enumerated() { + let entryLabel = "\(label)[\(position)]" + try expectEqual(actual[position].name, names[position], "\(entryLabel).name") + let input = try expectedInput(entry, entryLabel) + try expectEqual(actual[position].input, input, "\(entryLabel).input") + } + return + } + + for (position, entry) in list.enumerated() { + try check(name: names[position], spec: entry) + } + return + } + + // Fail closed: an unrecognized shape must not be silently unchecked, which + // is exactly how this expectation went unverified in the first place. + throw VectorFailure("\(label) expectation is neither a map nor a list: \(declared)") + } +} +func withEnvironment(_ values: [String: Any], _ body: () throws -> T) rethrows -> T { + var restore: [String: String?] = [:] + for (key, value) in values { + guard let string = value as? String else { continue } + restore[key] = ProcessInfo.processInfo.environment[key] + Env.set(key, string) + } + defer { + for (key, previous) in restore { + Env.set(key, previous) + } + } + return try body() +} +enum Env { + static func set(_ key: String, _ value: String?) { + #if os(Windows) + _ = key.withCString(encodedAs: UTF16.self) { name in + (value ?? "").withCString(encodedAs: UTF16.self) { item in + _wputenv_s(name, item) + } + } + #else + if let value { + setenv(key, value, 1) + } else { + unsetenv(key) + } + #endif + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/NamedCollectionSaveFormTests.swift b/runtime/swift/prompty/Tests/PromptyTests/NamedCollectionSaveFormTests.swift new file mode 100644 index 000000000..5dbcaeb79 --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/NamedCollectionSaveFormTests.swift @@ -0,0 +1,360 @@ +import XCTest + +@testable import Prompty +@testable import PromptyModel + +/// Save-form selection for collections of named entities (spec §2.9.2). +/// +/// Named collections may be saved either as a name-keyed object or as a whole +/// ordered array. The canonical rule restricts the object form to **non-empty, +/// exactly-unique** names and requires the ordered-array fallback otherwise. +/// +/// That precondition is not cosmetic. A JSON object cannot hold two identical +/// keys, so applying the object form to a collection with duplicate names +/// destroys entries *by construction* — silently, with no error to notice. A +/// candidate emitter was withdrawn from cross-runtime acceptance for exactly +/// this defect in another language backend: duplicate-named entries collapsed +/// on save. +/// +/// These tests split into two groups, and the distinction matters: +/// +/// - **Disqualifying fixtures** (duplicate names, empty name). The array +/// fallback is *mandatory*, so these assert the saved form directly as well +/// as full payload survival. Asserting the form here cannot reject legal +/// behaviour, because no legal emitter may choose the object form for these +/// inputs. Note in particular that JSON permits `""` as an object key, so an +/// emitter could illegally object-encode the empty-name fixture and still +/// round-trip both entries — payload assertions alone would not catch it. +/// Both disqualifiers are proven on `tools` *and* on `inputs`, which save +/// through different paths and can therefore fail independently. +/// +/// Payloads are compared as composite `name|value` entries, never as two +/// independent projections: comparing names and values separately proves +/// both multisets survived but not which value belongs to which name. Where +/// a fixture repeats a name, the comparison is additionally positional, +/// since exchanging the payloads of two identically-named entries leaves any +/// sorted multiset unchanged and is observable only in order. +/// +/// - **Qualifying fixture** (unique names). Both forms are legal, so that test +/// asserts survival and payload only, never the form and never reload order. +/// +/// At the currently pinned emitter the array form is selected unconditionally, +/// including for unique names, so the qualifying test currently exercises only +/// the array path despite its "whichever form" name. A candidate emitter was +/// measured selecting the object form for unique names while correctly falling +/// back to the array form for duplicates and empty names; all of these tests +/// were validated green against it. +/// +/// Order is asserted only where canonical actually promises it — in the array +/// fallback. The object form makes no ordering promise, so nothing outside that +/// one test depends on one. See `ConnectionRoundTripTests` for why an unrelated +/// test must not smuggle in an ordering assumption. +final class NamedCollectionSaveFormTests: XCTestCase { + + // MARK: - Helpers + + /// `name|description` per tool, in collection order. + /// + /// Deliberately a *composite* signature rather than two independent + /// projections. Comparing sorted names and sorted descriptions separately + /// proves both multisets survived but not their association, so an + /// implementation that reattached the wrong payload to a name would pass. + private func toolSignatures(_ agent: Agent) -> [String] { + (agent.tools ?? []).map { tool in + switch tool { + case .functionTool(let value): return "\(value.name)|\(value.description ?? "")" + case .customTool(let value, _): return "\(value.name)|\(value.description ?? "")" + default: return "" + } + } + } + + /// `name|default` per saved property entry, in saved order. + /// + /// Read from the saved dictionary rather than the model because a scalar + /// `Property` resolves to the `.unknown` passthrough case, so switching on + /// the generated enum would end up reading this same dictionary anyway. + private func propertySignatures(of entries: [[String: Any]]) -> [String] { + entries.map { entry in + let name = entry["name"] as? String ?? "" + let value = entry["default"].map { String(describing: $0) } ?? "" + return "\(name)|\(value)" + } + } + + /// Describes the chosen save form, for diagnostics only. + private func saveForm(_ saved: [String: Any], _ key: String) -> String { + if saved[key] is [Any] { return "ordered array" } + if let object = saved[key] as? [String: Any] { + return "name-keyed object with keys \(object.keys.sorted())" + } + return "neither array nor object: \(String(describing: saved[key]))" + } + + /// Asserts the ordered-array fallback was chosen and returns its entries. + /// + /// Only call this for fixtures where the array form is *mandatory* under + /// §2.9.2. Returns `nil` after failing, so callers `guard` rather than + /// indexing into a collection that may not exist. + private func requireArrayForm( + _ saved: [String: Any], + _ key: String, + because reason: String, + file: StaticString = #filePath, + line: UInt = #line + ) -> [[String: Any]]? { + guard let entries = saved[key] as? [[String: Any]] else { + XCTFail( + "\(key) was not saved as an ordered array, but the array fallback is mandatory here " + + "because \(reason). Saved as \(saveForm(saved, key)).", + file: file, line: line) + return nil + } + return entries + } + + private func agent(tools: [[String: Any]]) throws -> Agent { + try Agent.load([ + "kind": "prompt", + "name": "save-form", + "model": ["id": "gpt-4o-mini", "apiType": "chat"], + "tools": tools, + "instructions": "user:\nhi", + ]) + } + + // MARK: - Disqualifying fixtures: the array fallback is mandatory + + /// Two tools sharing a name both survive a save/reload, and the array + /// fallback is chosen. + /// + /// This is the precise defect that withdrew a candidate emitter in another + /// runtime. The third, uniquely-named tool is present so the collection is + /// genuinely mixed — a naive implementation that falls back only when *every* + /// name repeats would still pass a duplicates-only fixture. The two duplicates + /// carry different descriptions so that a collapse-then-clone implementation, + /// which restores the count but not the content, still fails. + func testDuplicateToolNamesSurviveSaveAndReload() throws { + let loaded = try agent(tools: [ + ["name": "dup", "kind": "function", "description": "first"], + ["name": "dup", "kind": "function", "description": "second"], + ["name": "unique", "kind": "function", "description": "third"], + ]) + + XCTAssertEqual(loaded.tools?.count, 3, "the fixture did not load three tools to begin with") + + let saved = try loaded.save() + guard requireArrayForm(saved, "tools", because: "two tools share the name 'dup'") != nil else { + return + } + + let reloaded = try Agent.load(saved) + XCTAssertEqual( + reloaded.tools?.count, 3, + "duplicate-named tools collapsed on save/reload — saved as \(saveForm(saved, "tools"))") + XCTAssertEqual( + toolSignatures(reloaded).sorted(), ["dup|first", "dup|second", "unique|third"], + "a duplicate-named tool lost or swapped its payload — " + + "saved as \(saveForm(saved, "tools"))") + } + + /// The same guarantee for properties, which are a separate collection with + /// its own save path. + /// + /// Asserts `name|default` signatures rather than a bare count, so a + /// collapse-then-clone implementation that restores the count with the wrong + /// content still fails. + func testDuplicatePropertyNamesSurviveSaveAndReload() throws { + let loaded = try Agent.load([ + "kind": "prompt", + "name": "dup-props", + "model": ["id": "gpt-4o-mini", "apiType": "chat"], + "inputs": [ + ["name": "dup", "kind": "string", "default": "a"], + ["name": "dup", "kind": "string", "default": "b"], + ["name": "unique", "kind": "string", "default": "c"], + ], + "instructions": "user:\nhi", + ]) + + XCTAssertEqual(loaded.inputs?.count, 3, "the fixture did not load three inputs to begin with") + + let saved = try loaded.save() + guard + let savedEntries = requireArrayForm( + saved, "inputs", because: "two inputs share the name 'dup'") + else { return } + + // Assert the *first* serialized output, not only the re-save below. A + // defect that reverses entries would reverse them once, reload in that + // order, then reverse them back on the second save — cancelling out and + // leaving a re-save-only assertion green while the first output violated + // the canonical order. Any involutive corruption hides the same way. + XCTAssertEqual( + propertySignatures(of: savedEntries), ["dup|a", "dup|b", "unique|c"], + "the first save lost, exchanged or reordered a duplicate-named input's default") + + let reloaded = try Agent.load(saved) + XCTAssertEqual( + reloaded.inputs?.count, 3, + "duplicate-named inputs collapsed on save/reload — saved as \(saveForm(saved, "inputs"))") + + // Re-save so payload can be compared without switching on the generated + // Property enum, whose scalar case is the `.unknown` dictionary passthrough. + // This second check is retained because it catches reload-side corruption + // that the first-save assertion cannot see. + let resaved = try reloaded.save() + guard + let entries = requireArrayForm(resaved, "inputs", because: "two inputs share the name 'dup'") + else { return } + + // Declaration order, not a sorted multiset. Both duplicates are named + // `dup`, so exchanging their defaults leaves the sorted multiset identical + // and would pass — the association is only observable positionally. Legal + // to assert here because the array form was required above, and the array + // fallback is the one representation canonical promises an order for. + XCTAssertEqual( + propertySignatures(of: entries), ["dup|a", "dup|b", "unique|c"], + "a duplicate-named input lost, exchanged or reordered its default across reload") + } + + /// An empty name is the other disqualifier, and it needs the form assertion + /// more than the duplicate cases do: JSON permits `""` as an object key, so + /// an illegal object encoding would round-trip both entries cleanly and pass + /// every payload assertion. Only checking the form catches it. + /// + /// Order is asserted here because the mandatory array form promises it. + func testEmptyNameForcesTheArrayFallbackAndDropsNothing() throws { + let loaded = try agent(tools: [ + ["name": "", "kind": "function", "description": "blank"], + ["name": "named", "kind": "function", "description": "ok"], + ]) + + XCTAssertEqual(loaded.tools?.count, 2, "the fixture did not load two tools to begin with") + + let saved = try loaded.save() + guard requireArrayForm(saved, "tools", because: "one tool has an empty name") != nil else { + return + } + + let reloaded = try Agent.load(saved) + XCTAssertEqual( + reloaded.tools?.count, 2, + "an empty-named tool was dropped on save/reload — saved as \(saveForm(saved, "tools"))") + XCTAssertEqual( + toolSignatures(reloaded), ["|blank", "named|ok"], + "the empty-named tool lost its name or payload, or the entries were reordered") + } + + /// The empty-name disqualifier on the `inputs` path. + /// + /// `inputs` saves through a different path than `tools`, so each disqualifier + /// has to be proven on both collections independently — an implementation can + /// get one right and the other wrong. The duplicate-name case already is; + /// this closes the empty-name case. + /// + /// It is also the disqualifier least likely to be caught by accident. `""` is + /// a legal JSON object key, so an illegal object encoding round-trips both + /// entries cleanly and satisfies every payload assertion. Only the form check + /// rejects it. + func testEmptyPropertyNameForcesTheArrayFallback() throws { + let loaded = try Agent.load([ + "kind": "prompt", + "name": "empty-name-prop", + "model": ["id": "gpt-4o-mini", "apiType": "chat"], + "inputs": [ + ["name": "", "kind": "string", "default": "blank"], + ["name": "named", "kind": "string", "default": "ok"], + ], + "instructions": "user:\nhi", + ]) + + XCTAssertEqual(loaded.inputs?.count, 2, "the fixture did not load two inputs to begin with") + + let saved = try loaded.save() + guard + let savedEntries = requireArrayForm( + saved, "inputs", because: "one input has an empty name") + else { return } + + // The first serialized output, for the same cancellation reason as the + // duplicate-name test: an involutive reordering defect would undo itself + // across the save/reload/save cycle below. + XCTAssertEqual( + propertySignatures(of: savedEntries), ["|blank", "named|ok"], + "the first save lost, exchanged or reordered the empty-named input") + + let reloaded = try Agent.load(saved) + XCTAssertEqual( + reloaded.inputs?.count, 2, + "an empty-named input was dropped on save/reload — saved as \(saveForm(saved, "inputs"))") + + // Re-save for the same reason as the duplicate-name property test: a scalar + // `Property` resolves to the `.unknown` passthrough, so the saved + // dictionary is where the payload is legible. Retained alongside the + // first-save assertion because it catches reload-side corruption. + let resaved = try reloaded.save() + guard + let entries = requireArrayForm(resaved, "inputs", because: "one input has an empty name") + else { return } + + XCTAssertEqual( + propertySignatures(of: entries), ["|blank", "named|ok"], + "the empty-named input lost its name or default, or the entries were reordered") + } + + /// Where the array fallback is mandatory, order is part of the contract. + /// + /// This fixture has duplicate names, so a conforming emitter *must* choose the + /// array form. Skipping when it does not would let a defective object-form + /// selection leave CI green, so the form is asserted rather than tolerated. + func testArrayFallbackPreservesDeclarationOrder() throws { + let loaded = try agent(tools: [ + ["name": "dup", "kind": "function", "description": "first"], + ["name": "dup", "kind": "function", "description": "second"], + ["name": "aaa_last_alphabetically_first", "kind": "function", "description": "third"], + ]) + + let saved = try loaded.save() + guard requireArrayForm(saved, "tools", because: "two tools share the name 'dup'") != nil else { + return + } + + let reloaded = try Agent.load(saved) + + // Declaration order, not alphabetical order — the third tool sorts first by + // name, so an accidental re-sort would move it and fail here. + XCTAssertEqual( + toolSignatures(reloaded), + ["dup|first", "dup|second", "aaa_last_alphabetically_first|third"], + "the ordered-array fallback did not preserve declaration order") + } + + // MARK: - Qualifying fixture: either form is legal + + /// Uniquely-named entries survive whichever form is chosen. + /// + /// Both forms are legal here, so this asserts survival and payload only. It + /// deliberately does **not** assert the saved form, and **not** reload order: + /// the object form makes no ordering promise, so pinning either would fail on + /// a legal emitter change for a reason unrelated to what this checks. The + /// names are chosen so declaration order and alphabetical order disagree, + /// which is what makes an accidental order assertion visible in review. + func testUniquelyNamedToolsSurviveWhicheverFormIsUsed() throws { + let loaded = try agent(tools: [ + ["name": "b_tool", "kind": "function", "description": "bee"], + ["name": "a_tool", "kind": "function", "description": "ay"], + ]) + + let saved = try loaded.save() + let reloaded = try Agent.load(saved) + + XCTAssertEqual( + reloaded.tools?.count, 2, + "a uniquely-named tool was dropped — saved as \(saveForm(saved, "tools"))") + XCTAssertEqual( + toolSignatures(reloaded).sorted(), ["a_tool|ay", "b_tool|bee"], + "a uniquely-named tool lost or swapped its payload — " + + "saved as \(saveForm(saved, "tools"))") + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/NamedCollectionShorthandTests.swift b/runtime/swift/prompty/Tests/PromptyTests/NamedCollectionShorthandTests.swift new file mode 100644 index 000000000..9e8c04eef --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/NamedCollectionShorthandTests.swift @@ -0,0 +1,238 @@ +import Foundation + +import PromptyModel + +import XCTest + +/// Pins the named-collection shorthand rules directly, without the shared +/// fixture. +/// +/// `NamedCollectionVectorTests` drives the canonical +/// `spec/vectors/model/named_collection_vectors.json`, but that file arrives +/// with PR #447 and skips until then. These tests assert the same rules from +/// this branch so the behaviour is covered now, and so a regression is reported +/// against a named rule rather than only as a vector diff. +/// +/// The rules, all for a *name-keyed* `inputs` / `outputs` / nested `properties` +/// map: +/// +/// 1. The key supplies `name`. +/// 2. A bare scalar infers `kind` and is stored in `default`; `example` stays +/// absent. +/// 3. An immediate array value is rejected with the full dotted path. +/// 4. Arrays inside declared property fields (`default`, `items`) stay valid. +/// 5. The *list* form is unchanged: a bare scalar there is direct `@coerce` +/// input and keeps landing in `example`. +/// +/// Rules 2 and 5 are the pair that matters most. They look alike and differ in +/// the field they populate, so they are asserted against each other rather than +/// in isolation. +@testable import Prompty + +final class NamedCollectionShorthandTests: XCTestCase { + + // MARK: - Helpers + + private func loadInputs(_ frontmatter: String) throws -> [[String: Any]] { + let contents = "---\n\(frontmatter)\n---\nsystem:\nvector\n" + let agent = try Loader.load( + contents: contents, basePath: FileManager.default.currentDirectoryPath) + return try (agent.inputs ?? []).map { try $0.save() } + } + + private func expectRejection( + _ frontmatter: String, path expectedPath: String, category expectedCategory: String = "array", + file: StaticString = #filePath, line: UInt = #line + ) { + let contents = "---\n\(frontmatter)\n---\nsystem:\nvector\n" + do { + _ = try Loader.load(contents: contents, basePath: FileManager.default.currentDirectoryPath) + XCTFail( + "expected rejection at \(expectedPath), but the load succeeded", + file: file, line: line) + } catch let error as LoadError { + guard case .invalidNamedCollectionEntry(let path, let category) = error else { + XCTFail( + "expected LoadError.invalidNamedCollectionEntry, got \(error). A generic " + + "rejection carries neither the path nor the value category.", + file: file, line: line) + return + } + XCTAssertEqual(path, expectedPath, "rejected path", file: file, line: line) + XCTAssertEqual(category, expectedCategory, "value category", file: file, line: line) + XCTAssertTrue( + String(describing: error).contains("invalid-named-collection-entry"), + "the diagnostic must carry the machine-readable token: \(error)", + file: file, line: line) + } catch { + XCTFail("expected a LoadError, got \(error)", file: file, line: line) + } + } + + // MARK: - Rule 2: scalar shorthand stores `default` + + func testMapFormScalarStoresDefaultAndOmitsExample() throws { + let cases: [(literal: String, kind: String, expected: Any)] = [ + ("Seattle", "string", "Seattle"), + ("3", "integer", 3), + ("1.5", "float", 1.5), + ("true", "boolean", true), + ] + + for probe in cases { + let entries = try loadInputs("name: t\ninputs:\n city: \(probe.literal)") + XCTAssertEqual(entries.count, 1, "\(probe.literal): entry count") + guard let entry = entries.first else { continue } + + XCTAssertEqual(entry["name"] as? String, "city", "\(probe.literal): name from key") + XCTAssertEqual(entry["kind"] as? String, probe.kind, "\(probe.literal): inferred kind") + + // JSON text rather than `==`: Foundation bridges 0/1 to Bool, so a + // direct comparison would let `0` satisfy `false`. + XCTAssertEqual( + try jsonText(entry["default"] as Any), try jsonText(probe.expected), + "\(probe.literal): scalar must land in `default`") + + XCTAssertNil( + entry["example"], + "\(probe.literal): `example` must stay absent — populating it too would " + + "blur the named-collection shorthand with the direct @coerce contract") + } + } + + func testMapFormScalarShorthandAppliesToOutputs() throws { + let contents = "---\nname: t\noutputs:\n answer: hello\n---\nsystem:\nvector\n" + let agent = try Loader.load( + contents: contents, basePath: FileManager.default.currentDirectoryPath) + let entries = try (agent.outputs ?? []).map { try $0.save() } + + XCTAssertEqual(entries.count, 1) + XCTAssertEqual(entries.first?["name"] as? String, "answer") + XCTAssertEqual(entries.first?["kind"] as? String, "string") + XCTAssertEqual(entries.first?["default"] as? String, "hello") + XCTAssertNil(entries.first?["example"]) + } + + // MARK: - Rule 5: the list form keeps direct @coerce semantics + + func testListFormBareScalarStillUsesExample() throws { + // The two shorthands are distinguished by position, not by value. A list + // element is a Property in its own right, so it follows @coerce. + let entries = try loadInputs("name: t\ninputs:\n - Seattle") + + XCTAssertEqual(entries.count, 1) + XCTAssertEqual(entries.first?["kind"] as? String, "string") + XCTAssertEqual( + entries.first?["example"] as? String, "Seattle", + "a bare scalar in list position is direct @coerce input and keeps `example`") + XCTAssertNil( + entries.first?["default"], + "the direct form must not also populate `default`, or the two contracts " + + "become indistinguishable") + } + + // MARK: - Rule 3: immediate arrays are rejected, with a path + + func testScalarArrayInNameKeyedInputsIsRejected() { + expectRejection( + "name: t\ninputs:\n arrayDefault: [1, two, null]", path: "inputs.arrayDefault") + } + + func testObjectArrayInNameKeyedInputsIsRejected() { + expectRejection( + "name: t\ninputs:\n arrayEntry:\n - kind: string", path: "inputs.arrayEntry") + } + + func testArrayInNestedPropertiesIsRejectedWithFullPath() { + // Recursion must thread the path, not restart it — a bare `arrayEntry` + // would not tell a consumer where the defect is. + let frontmatter = [ + "name: t", + "inputs:", + " profile:", + " kind: object", + " properties:", + " arrayEntry:", + " - kind: string", + ].joined(separator: "\n") + expectRejection(frontmatter, path: "inputs.profile.properties.arrayEntry") + } + + func testArrayInNameKeyedOutputsIsRejected() { + expectRejection("name: t\noutputs:\n bad: [1]", path: "outputs.bad") + } + + // MARK: - Rule 4: arrays inside declared fields remain valid + + func testDeclaredArrayFieldsRemainValid() throws { + let entries = try loadInputs( + [ + "name: t", + "inputs:", + " aliases:", + " kind: array", + " default: [Ada, Grace]", + " items:", + " kind: string", + ].joined(separator: "\n")) + + XCTAssertEqual(entries.count, 1) + let entry = entries[0] + XCTAssertEqual(entry["name"] as? String, "aliases") + XCTAssertEqual(entry["kind"] as? String, "array") + XCTAssertEqual( + try jsonText(entry["default"] as Any), try jsonText(["Ada", "Grace"]), + "an array in a declared `default` is data, not a named entry") + XCTAssertEqual((entry["items"] as? [String: Any])?["kind"] as? String, "string") + } + + // MARK: - Rule 1: map-form object entries still gain their name + + func testMapFormObjectEntriesTakeNameFromKey() throws { + let entries = try loadInputs( + [ + "name: t", + "inputs:", + " beta:", + " kind: boolean", + " alpha:", + " kind: string", + " description: first", + ].joined(separator: "\n")) + + // Keys are sorted so the canonical list is stable across dictionary + // iteration order. + XCTAssertEqual(entries.map { $0["name"] as? String }, ["alpha", "beta"]) + XCTAssertEqual(entries[0]["description"] as? String, "first") + } + + func testNestedMapFormPropertiesAreNormalized() throws { + let entries = try loadInputs( + [ + "name: t", + "inputs:", + " profile:", + " kind: object", + " properties:", + " nested: kept", + ].joined(separator: "\n")) + + XCTAssertEqual(entries.count, 1) + // The generated model round-trips nested collections in the canonical + // object (name-keyed map) form, so read the nested entry by its key. + let nested = entries[0]["properties"] as? [String: Any] + let kept = nested?["nested"] as? [String: Any] + XCTAssertEqual(kept?["kind"] as? String, "string") + XCTAssertEqual( + kept?["default"] as? String, "kept", + "the shorthand applies at every nesting depth, not just the top level") + XCTAssertNil(kept?["example"]) + } + + // MARK: - Helper + + private func jsonText(_ value: Any) throws -> String { + let data = try JSONSerialization.data(withJSONObject: ["v": value], options: [.sortedKeys]) + return String(decoding: data, as: UTF8.self) + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/NamedCollectionVectorTests.swift b/runtime/swift/prompty/Tests/PromptyTests/NamedCollectionVectorTests.swift new file mode 100644 index 000000000..b415cdc8e --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/NamedCollectionVectorTests.swift @@ -0,0 +1,1121 @@ +import Foundation + +import PromptyModel + +import XCTest + +/// Drives the canonical cross-runtime named-collection vector. +/// +/// `spec/vectors/model/named_collection_vectors.json` (PR #447) pins how +/// `inputs`, `outputs`, and nested `properties` behave when they are written as +/// a *name-keyed map* rather than a list: +/// +/// - the key supplies `name`, +/// - a bare scalar value infers `kind` and is stored in **`default`**, with +/// `example` left absent, +/// - an immediate **array** value is never a property and is rejected with the +/// full dotted path to the offending entry, +/// - arrays nested inside declared property fields (`default`, `items`) stay +/// valid — only the immediate named-entry position is closed. +/// +/// This is deliberately **not** the direct `@coerce` contract that +/// `PropertyScalarCoercionVectorTests` asserts. There a bare scalar loaded +/// straight through the generated `Property` model lands in `example`. The two +/// contracts share a surface — "a scalar became a property" — and differ in the +/// field they populate, so a runtime that conflates them passes a naive check +/// while breaking one of the two. This suite pins `default` *and* the absence +/// of `example`; the sibling suite pins `example` *and* the absence of +/// `default`. +/// +/// ## Why this asserts rather than skips +/// +/// The scalar-coercion sibling is blocked at the pinned emitter because the +/// generated `Property.load` cannot accept a bare scalar at all. The +/// named-collection form is different: the generated model never sees the map, +/// because `Loader` normalizes it into the canonical list first. Every defect +/// the fixture describes was therefore in this repository's hand-written +/// loader, not in generated code, and is fixed directly. +/// +/// One save-side clause remains outside the loader's reach: +/// `collectionFormat: "object"` asks `save()` to re-emit a name-keyed map when +/// every entry has a unique non-empty name. That is generated code, so those +/// vectors record a *documented blocked baseline* tied to the emitter pin — +/// their entry semantics are still asserted in full. +/// +/// ## Robustness to a moving fixture +/// +/// This file has already been rewritten several times upstream, so the suite +/// does not pin its vector count or ordering. It instead requires the vectors +/// the request names, and processes every vector present — an unrecognised +/// `operation` fails rather than being skipped, so a future clause cannot slip +/// through unasserted. +@testable import Prompty + +final class NamedCollectionVectorTests: XCTestCase { + + private static let vectorPath = "model/named_collection_vectors.json" + + /// The emitter pin whose known save-side gap the blocked baseline describes. + private static let blockedAtEmitterVersion = "0.4.2" + + /// Vectors this request names explicitly. The fixture may grow; it may not + /// quietly lose one of these. + private static let requiredVectorNames: Set = [ + "string_scalar_in_name_keyed_inputs_infers_property", + "integer_scalar_in_name_keyed_inputs_infers_property", + "float_scalar_in_name_keyed_inputs_infers_property", + "boolean_scalar_in_name_keyed_inputs_infers_property", + "scalar_array_shorthand_in_name_keyed_inputs_is_rejected", + "array_value_in_name_keyed_inputs_is_rejected", + "array_value_in_nested_properties_is_rejected", + ] + + // MARK: - The vector + + func testCanonicalNamedCollectionVector() throws { + // Only genuine absence may skip. Routing every error through a `try?` would + // turn malformed JSON or a reshaped root into a green run. + let vectorURL = + Spec.root + .appendingPathComponent("vectors") + .appendingPathComponent("model") + .appendingPathComponent("named_collection_vectors.json") + + guard FileManager.default.fileExists(atPath: vectorURL.path) else { + throw XCTSkip( + "spec/vectors/\(Self.vectorPath) is not on this branch yet (PR #447 is " + + "unmerged), so the canonical named-collection fixture cannot be " + + "asserted. This suite activates automatically when the file lands; " + + "NamedCollectionShorthandTests covers the same rules meanwhile.") + } + + let document = try Spec.vectorObject(Self.vectorPath) + let vectors = try Self.validateVectorShape(document) + + var failures: [String] = [] + var blocked: [String] = [] + var asserted: [String] = [] + + for vector in vectors { + guard let name = vector["name"] as? String else { + failures.append("a vector declares no `name`") + continue + } + guard let operation = vector["operation"] as? String else { + failures.append("\(name): declares no `operation`") + continue + } + guard let input = vector["input"] as? [String: Any] else { + failures.append("\(name): declares no `input` object") + continue + } + guard let expected = vector["expected"] as? [String: Any] else { + failures.append("\(name): declares no `expected` object") + continue + } + + switch operation { + case "load-error": + Self.runLoadError( + name: name, input: input, expected: expected, + failures: &failures, asserted: &asserted) + case "load-save-reload": + // `collectionPath` is a sibling of `expected`, not a member of it. + guard let collectionPath = vector["collectionPath"] as? String else { + failures.append( + "\(name): a load-save-reload vector declares no `collectionPath`, so " + + "there is no way to know which collection it pins") + continue + } + Self.runLoadSaveReload( + name: name, input: input, expected: expected, collectionPath: collectionPath, + failures: &failures, blocked: &blocked, asserted: &asserted) + default: + // A new operation must not ride along unasserted. + failures.append( + "\(name): unrecognised operation `\(operation)`. The fixture grew a " + + "clause this suite does not evaluate, which would otherwise pass " + + "silently.") + } + } + + if !blocked.isEmpty { + // Tie the documented baseline to the pin it describes, so bumping the + // emitter without closing the gap fails instead of resting on stale prose. + let pin = Self.pinnedEmitterVersion() + if pin != Self.blockedAtEmitterVersion { + failures.append( + "the object-form save baseline is documented for " + + "@typra/emitter@\(Self.blockedAtEmitterVersion), but schema/package.json " + + "now pins \(pin ?? ""). Re-evaluate whether generated " + + "save() can emit the name-keyed object form: \(blocked.joined(separator: "; "))") + } + } + + XCTAssertTrue( + failures.isEmpty, + "named-collection vector failures:\n - " + failures.joined(separator: "\n - ")) + + // Non-vacuity: the fixture must actually have exercised the runtime. + XCTAssertFalse( + asserted.isEmpty, + "no named-collection vector was asserted — the fixture parsed but drove " + + "nothing, so this suite would report success while measuring nothing") + } + + // MARK: - load-error + + private static func runLoadError( + name: String, input: [String: Any], expected: [String: Any], + failures: inout [String], asserted: inout [String] + ) { + let contents: String + do { + contents = try frontmatter(input) + } catch { + failures.append("\(name): could not render input as frontmatter: \(error)") + return + } + + do { + _ = try Loader.load(contents: contents, basePath: FileManager.default.currentDirectoryPath) + failures.append( + "\(name): expected the load to be rejected, but it succeeded. The " + + "invalid entry was accepted silently.") + return + } catch let error as LoadError { + let token = expected["error"] as? String + guard token == "invalid-named-collection-entry" else { + // A future error class this suite does not model structurally: the + // rejection itself is still asserted, and recorded as such. + asserted.append("\(name) (throw only)") + return + } + guard case .invalidNamedCollectionEntry(let path, let category) = error else { + failures.append( + "\(name): expected LoadError.invalidNamedCollectionEntry, got \(error). " + + "A generic rejection does not carry the path and value category the " + + "contract requires.") + return + } + if let expectedPath = expected["path"] as? String, path != expectedPath { + failures.append("\(name): path expected `\(expectedPath)`, got `\(path)`") + } + if let expectedCategory = expected["valueCategory"] as? String, category != expectedCategory { + failures.append( + "\(name): valueCategory expected `\(expectedCategory)`, got `\(category)`") + } + // The machine-readable token must reach a consumer reading the message. + if !String(describing: error).contains("invalid-named-collection-entry") { + failures.append( + "\(name): the rendered diagnostic does not contain the " + + "`invalid-named-collection-entry` token: \(error)") + } + asserted.append(name) + } catch { + failures.append("\(name): rejected with a non-LoadError: \(error)") + } + } + + // MARK: - load-save-reload + + private static func runLoadSaveReload( + name: String, input: [String: Any], expected: [String: Any], collectionPath: String, + failures: inout [String], blocked: inout [String], asserted: inout [String] + ) { + let saved: [String: Any] + do { + let contents = try frontmatter(input) + let agent = try Loader.load( + contents: contents, basePath: FileManager.default.currentDirectoryPath) + saved = try agent.save() + } catch { + failures.append("\(name): load/save threw: \(error)") + return + } + + let rawCollection = saved[collectionPath] + let wireEntries: [[String: Any]] + let actualFormat: String + if let list = rawCollection as? [[String: Any]] { + wireEntries = list + actualFormat = "array" + } else if let map = rawCollection as? [String: Any] { + wireEntries = map.keys.sorted().compactMap { key in + var entry = (map[key] as? [String: Any]) ?? [:] + entry["name"] = key + return entry + } + actualFormat = "object" + } else { + failures.append( + "\(name): saved `\(collectionPath)` is neither an array nor an object: " + + "\(String(describing: rawCollection))") + return + } + + // --- collectionFormat ------------------------------------------------- + if let expectedFormat = expected["collectionFormat"] as? String, + expectedFormat != actualFormat + { + if expectedFormat == "object" && actualFormat == "array" { + // Generated `save()` always emits the ordered array. Lossless, but not + // yet the canonical wire form. Entry semantics below are still asserted. + blocked.append("\(name) (save emits array, contract wants object)") + } else { + failures.append( + "\(name): collectionFormat expected `\(expectedFormat)`, got `\(actualFormat)`") + } + } + + // --- wireEntries.absentFields (pre-reload wire shape) ------------------ + if let wireExpectations = expected["wireEntries"] as? [[String: Any]] { + for wireExpectation in wireExpectations { + guard let index = wireExpectation["index"] as? Int else { continue } + guard index < wireEntries.count else { + failures.append("\(name): wireEntries[\(index)] is out of range") + continue + } + // Read the raw list here: the object branch above synthesises `name` + // from the key, which would mask an absent-name expectation. + let rawEntry = (rawCollection as? [[String: Any]])?[index] ?? wireEntries[index] + for absent in (wireExpectation["absentFields"] as? [String]) ?? [] + where + rawEntry[absent] != nil + { + failures.append( + "\(name): wire entry \(index) should omit `\(absent)`, but it is present " + + "as \(String(describing: rawEntry[absent]))") + } + } + } + + // --- entries ---------------------------------------------------------- + guard let expectedEntries = expected["entries"] as? [[String: Any]] else { + asserted.append("\(name) (format only)") + return + } + + guard expectedEntries.count == wireEntries.count else { + failures.append( + "\(name): expected \(expectedEntries.count) entries, got \(wireEntries.count)") + return + } + + let absentEntryFields = (expected["absentEntryFields"] as? [String]) ?? [] + + for (index, expectedEntry) in expectedEntries.enumerated() { + let actualEntry = wireEntries[index] + let entryPath = "\(name).\(collectionPath)[\(index)]" + compare( + expected: canonicalizeExpected(expectedEntry), actual: actualEntry, + path: entryPath, failures: &failures) + + // `canonicalizeExpected` flattens a name-keyed nested `properties` map + // into the ordered list so the *semantic* comparison can proceed. That + // adaptation also erases the shape difference, so a nested collection + // saved as the array fallback would compare equal to a fixture stating + // the canonical object form. Walk the two in parallel to recover it. + blocked += nestedObjectFormGaps( + expected: expectedEntry, actual: actualEntry, path: entryPath) + + for absent in absentEntryFields where actualEntry[absent] != nil { + failures.append( + "\(entryPath): `\(absent)` must be absent, but it is " + + "\(String(describing: actualEntry[absent])). The named-collection scalar " + + "shorthand stores the value in `default`; populating `\(absent)` too " + + "would blur it with the direct @coerce contract.") + } + } + + // --- reload: the saved wire must load again and re-save identically ---- + do { + let reloaded = try Agent.load(saved) + let resaved = try reloaded.save() + let before = try jsonText(saved) + let after = try jsonText(resaved) + if before != after { + failures.append( + "\(name): save/reload is not stable.\n first: \(before)\n second: \(after)") + } + } catch { + failures.append("\(name): the saved wire form did not reload: \(error)") + } + + asserted.append(name) + } + + // MARK: - Shape validation + + private static func validateVectorShape(_ document: [String: Any]) throws -> [[String: Any]] { + guard let vectors = document["vectors"] as? [[String: Any]] else { + throw VectorFailure( + "the fixture declares no `vectors` array; its shape changed and this " + + "suite would otherwise assert nothing") + } + guard !vectors.isEmpty else { + throw VectorFailure("the fixture declares zero vectors") + } + + let present = Set(vectors.compactMap { $0["name"] as? String }) + let missing = requiredVectorNames.subtracting(present).sorted() + guard missing.isEmpty else { + throw VectorFailure( + "the fixture no longer declares: \(missing.joined(separator: ", ")). " + + "These are the cases this gate exists to assert, so their absence is " + + "a fixture regression rather than a reason to pass.") + } + return vectors + } + + // MARK: - Nested object-form detection + // + // These run whether or not the canonical fixture is on the branch, so the + // detection above is exercised while `testCanonicalNamedCollectionVector` is + // still skipping on absence. + + /// The canonicalised comparison pipeline cannot see a nested object-form gap. + /// + /// This is the justification for `nestedObjectFormGaps` existing, pinned as a + /// test so it cannot quietly stop being true. The fixture states `properties` + /// as a name-keyed map — the canonical object form — while the runtime saved + /// the ordered array fallback. + /// + /// The blindness is created by `canonicalizeExpected`, not by `compare`: + /// `compare` on its own would see a map on one side and a list on the other. + /// Canonicalisation reshapes the expectation to the wire form first — which + /// is what lets the semantic assertions run at all — and that same adaptation + /// erases the shape difference. Both steps are exercised here together + /// because it is their composition that loses the signal. + func testCanonicalisedComparisonPipelineCannotSeeNestedObjectFormGap() { + let expected: [String: Any] = [ + "name": "location", + "kind": "object", + "properties": ["city": ["kind": "string"]], + ] + let actual: [String: Any] = [ + "name": "location", + "kind": "object", + "properties": [["name": "city", "kind": "string"]], + ] + + var failures: [String] = [] + Self.compare( + expected: Self.canonicalizeExpected(expected), actual: actual, path: "probe", + failures: &failures) + + XCTAssertTrue( + failures.isEmpty, + "the canonicalised pipeline was expected to be blind to this gap; if it " + + "now reports it, nestedObjectFormGaps may be redundant: \(failures)") + } + + /// The same pair, seen by the parallel walk. + func testNestedObjectFormGapIsDetected() { + let expected: [String: Any] = [ + "name": "location", + "kind": "object", + "properties": ["city": ["kind": "string"]], + ] + let actual: [String: Any] = [ + "name": "location", + "kind": "object", + "properties": [["name": "city", "kind": "string"]], + ] + + let gaps = Self.nestedObjectFormGaps(expected: expected, actual: actual, path: "probe") + + XCTAssertEqual(gaps.count, 1, "expected exactly one gap, got \(gaps)") + XCTAssertEqual( + gaps.first, "probe.properties (save emits array, contract wants object)") + } + + /// A fixture that states the array form is not a gap. + func testArrayFormExpectationIsNotReportedAsAGap() { + let expected: [String: Any] = [ + "name": "location", + "properties": [["name": "city", "kind": "string"]], + ] + let actual: [String: Any] = [ + "name": "location", + "properties": [["name": "city", "kind": "string"]], + ] + + XCTAssertTrue( + Self.nestedObjectFormGaps(expected: expected, actual: actual, path: "probe").isEmpty) + } + + /// A runtime that already emits the object form is not a gap either. + /// + /// Without this, the detection could be satisfied by always reporting, and + /// the baseline would never clear when the emitter is fixed. + func testObjectFormActualIsNotReportedAsAGap() { + let expected: [String: Any] = [ + "name": "location", + "properties": ["city": ["kind": "string"]], + ] + let actual: [String: Any] = [ + "name": "location", + "properties": ["city": ["kind": "string"]], + ] + + XCTAssertTrue( + Self.nestedObjectFormGaps(expected: expected, actual: actual, path: "probe").isEmpty) + } + + /// Gaps are found at every depth, not just the first. + /// + /// The coordinator's reported row is one level down (`inputs[0].properties`). + /// A detector that only checked the top nesting level would satisfy that row + /// while missing anything deeper. + func testNestedObjectFormGapsAreFoundAtEveryDepth() { + let expected: [String: Any] = [ + "name": "outer", + "properties": [ + "middle": [ + "kind": "object", + "properties": [ + "inner": ["kind": "object", "properties": ["leaf": ["kind": "string"]]] + ], + ] + ], + ] + let actual: [String: Any] = [ + "name": "outer", + "properties": [ + [ + "name": "middle", "kind": "object", + "properties": [ + [ + "name": "inner", "kind": "object", + "properties": [["name": "leaf", "kind": "string"]], + ] + ], + ] + ], + ] + + let gaps = Self.nestedObjectFormGaps(expected: expected, actual: actual, path: "probe") + + XCTAssertEqual( + gaps, + [ + "probe.properties (save emits array, contract wants object)", + "probe.properties.middle.properties (save emits array, contract wants object)", + "probe.properties.middle.properties.inner.properties " + + "(save emits array, contract wants object)", + ], + "every nesting level must be reported, with a path that locates it") + } + + /// A gap inside `items` is reported too. + func testObjectFormGapInsideItemsIsDetected() { + let expected: [String: Any] = [ + "name": "rows", + "kind": "array", + "items": ["kind": "object", "properties": ["cell": ["kind": "string"]]], + ] + let actual: [String: Any] = [ + "name": "rows", + "kind": "array", + "items": ["kind": "object", "properties": [["name": "cell", "kind": "string"]]], + ] + + XCTAssertEqual( + Self.nestedObjectFormGaps(expected: expected, actual: actual, path: "probe"), + ["probe.items.properties (save emits array, contract wants object)"]) + } + + /// Descent pairs children by name, not by position. + /// + /// A map has no order, so pairing the sorted expectation against the saved + /// list positionally would compare unrelated children whenever the runtime's + /// declaration order differs from alphabetical. + func testDescentPairsChildrenByNameNotPosition() { + let expected: [String: Any] = [ + "name": "outer", + "properties": [ + "alpha": ["kind": "string"], + "beta": ["kind": "object", "properties": ["leaf": ["kind": "string"]]], + ], + ] + // Saved in declaration order, which is the reverse of alphabetical. + let actual: [String: Any] = [ + "name": "outer", + "properties": [ + [ + "name": "beta", "kind": "object", + "properties": [["name": "leaf", "kind": "string"]], + ], + ["name": "alpha", "kind": "string"], + ], + ] + + let gaps = Self.nestedObjectFormGaps(expected: expected, actual: actual, path: "probe") + + XCTAssertEqual( + gaps, + [ + "probe.properties (save emits array, contract wants object)", + "probe.properties.beta.properties (save emits array, contract wants object)", + ], + "the nested gap under `beta` must be found even though `beta` is not the " + + "child that sorts first") + } + + /// A nested gap is still found when the saved child omits its `name`. + /// + /// An unnamed composite is exactly the case the canonical fixture names, so + /// pairing purely by name would drop the child that matters most and report + /// nothing for anything beneath it. + func testNestedObjectFormGapIsFoundWhenTheSavedChildOmitsItsName() { + let expected: [String: Any] = [ + "name": "outer", + "properties": [ + "inner": ["kind": "object", "properties": ["leaf": ["kind": "string"]]] + ], + ] + let actual: [String: Any] = [ + "name": "outer", + // The saved child carries no `name`, so it cannot be looked up by key. + "properties": [ + ["kind": "object", "properties": [["name": "leaf", "kind": "string"]]] + ], + ] + + let gaps = Self.nestedObjectFormGaps(expected: expected, actual: actual, path: "probe") + + XCTAssertEqual( + gaps, + [ + "probe.properties (save emits array, contract wants object)", + "probe.properties.inner.properties (save emits array, contract wants object)", + ], + "the gap beneath an unnamed saved child must still be reported") + } + + /// A positional fallback never binds a child that names a different key. + /// + /// Object form is keyed, so position is only a sound last resort for an + /// entry that omits `name`. Binding a differently-named entry would descend + /// into an unrelated child and report its gaps against the wrong key. + func testPositionalFallbackRefusesDifferentlyNamedChild() { + let expected: [String: Any] = [ + "name": "outer", + "properties": [ + "alpha": ["kind": "object", "properties": ["leaf": ["kind": "string"]]] + ], + ] + // The sole saved child names a different key and carries a nested gap of + // its own. Pairing it to `alpha` would misattribute that gap. + let actual: [String: Any] = [ + "name": "outer", + "properties": [ + ["name": "beta", "kind": "object", "properties": [["name": "leaf", "kind": "string"]]] + ], + ] + + XCTAssertEqual( + Self.nestedObjectFormGaps(expected: expected, actual: actual, path: "probe"), + ["probe.properties (save emits array, contract wants object)"], + "a differently-named child must not be bound by position") + } + + /// Elimination pairing finds a nameless child at a non-corresponding index. + /// + /// `alpha` resolves by name to index 1, leaving `beta` unmatched. Index-based + /// pairing would inspect index 1, find `alpha` there, and never reach the + /// nameless child at index 0 — silently losing beta's gap. Elimination pairs + /// the single unmatched key to the single nameless child regardless of where + /// it sits. + func testEliminationPairingIgnoresPosition() { + let expected: [String: Any] = [ + "name": "outer", + "properties": [ + "alpha": ["kind": "string"], + "beta": ["kind": "object", "properties": ["leaf": ["kind": "string"]]], + ], + ] + // beta's child is saved first and unnamed; alpha's is saved second. + let actual: [String: Any] = [ + "name": "outer", + "properties": [ + ["kind": "object", "properties": [["name": "leaf", "kind": "string"]]], + ["name": "alpha", "kind": "string"], + ], + ] + + XCTAssertEqual( + Self.nestedObjectFormGaps(expected: expected, actual: actual, path: "probe"), + [ + "probe.properties (save emits array, contract wants object)", + "probe.properties.beta.properties (save emits array, contract wants object)", + ], + "a nameless child must be found by elimination, not by index") + } + + /// Two nameless children are ambiguous, so neither is guessed. + /// + /// Attributing a gap to a key that cannot be established is the same + /// misattribution keyed pairing exists to prevent. The top-level array-form + /// gap is still reported, so the real problem stays visible. + func testAmbiguousNamelessChildrenAreNotGuessed() { + let nested: [String: Any] = ["kind": "object", "properties": ["leaf": ["kind": "string"]]] + let expected: [String: Any] = [ + "name": "outer", + "properties": ["alpha": nested, "beta": nested], + ] + let savedChild: [String: Any] = [ + "kind": "object", "properties": [["name": "leaf", "kind": "string"]], + ] + let actual: [String: Any] = [ + "name": "outer", + "properties": [savedChild, savedChild], + ] + + XCTAssertEqual( + Self.nestedObjectFormGaps(expected: expected, actual: actual, path: "probe"), + ["probe.properties (save emits array, contract wants object)"], + "ambiguous nameless children must not be paired to arbitrary keys") + } + + /// Duplicate names disqualify name-addressing entirely. + /// + /// Both saved children claim `alpha`, so building the index first would let + /// the second silently overwrite the first and the descent would then report + /// a gap found in whichever entry happened to survive. With duplicates + /// present there is no way to tell which `alpha` the contract meant, so + /// descending into the survivor is a guess dressed up as a match. The + /// pre-scan refuses the index and only the honest top-level gap is reported. + func testDuplicateNamesDisqualifyNameAddressing() { + let expected: [String: Any] = [ + "name": "outer", + "properties": ["alpha": ["kind": "object", "properties": ["leaf": ["kind": "string"]]]], + ] + // Two entries named `alpha`; only the second carries a nested collection, + // so a last-wins collapse would surface a gap the contract cannot attribute. + let actual: [String: Any] = [ + "name": "outer", + "properties": [ + ["name": "alpha", "kind": "string"], + ["name": "alpha", "kind": "object", "properties": [["name": "leaf", "kind": "string"]]], + ], + ] + + XCTAssertEqual( + Self.nestedObjectFormGaps(expected: expected, actual: actual, path: "probe"), + ["probe.properties (save emits array, contract wants object)"], + "duplicate names must disqualify the name index instead of collapsing last-wins") + } + + /// Duplicates must not decay into an elimination match. + /// + /// Refusing the index by returning an *empty* one would leave `alpha` + /// unmatched, and the lone nameless child would then look like its unique + /// elimination partner — so hiding `alpha` behind a duplicate would conjure a + /// match that direct addressing correctly denied. The nameless child here + /// belongs to neither `alpha`, so no nested gap may be reported. + func testDuplicateNamesAlsoSuppressEliminationPairing() { + let expected: [String: Any] = [ + "name": "outer", + "properties": ["alpha": ["kind": "object", "properties": ["leaf": ["kind": "string"]]]], + ] + let actual: [String: Any] = [ + "name": "outer", + "properties": [ + ["name": "alpha", "kind": "string"], + ["name": "alpha", "kind": "string"], + ["kind": "object", "properties": [["name": "leaf", "kind": "string"]]], + ], + ] + + XCTAssertEqual( + Self.nestedObjectFormGaps(expected: expected, actual: actual, path: "probe"), + ["probe.properties (save emits array, contract wants object)"], + "an unaddressable collection must suppress elimination, not just name lookup") + } + + /// A malformed `name` refuses the index rather than passing as nameless. + /// + /// A non-string `name` is dropped by the uniqueness scan yet is not nameless + /// either, so without an explicit refusal it vanishes from both pairing paths + /// and the *other* child is eliminated into `alpha` unopposed. + func testMalformedNameRefusesNameAddressing() { + let expected: [String: Any] = [ + "name": "outer", + "properties": ["alpha": ["kind": "object", "properties": ["leaf": ["kind": "string"]]]], + ] + let actual: [String: Any] = [ + "name": "outer", + "properties": [ + ["name": 123, "kind": "string"], + ["kind": "object", "properties": [["name": "leaf", "kind": "string"]]], + ], + ] + + XCTAssertEqual( + Self.nestedObjectFormGaps(expected: expected, actual: actual, path: "probe"), + ["probe.properties (save emits array, contract wants object)"], + "a malformed name must refuse the index instead of counting as nameless") + } + + /// Duplicate *expected* names cannot be matched against a keyed actual. + /// + /// The object form holds one `alpha`, so attributing it to both expected + /// occurrences would report the same nested gap twice against entries that + /// were never separately observed. Uniqueness is required on both sides. + func testDuplicateExpectedNamesRefuseObjectFormPairing() { + let expected: [String: Any] = [ + "name": "outer", + "properties": [ + ["name": "alpha", "kind": "object", "properties": ["left": ["kind": "string"]]], + ["name": "alpha", "kind": "object", "properties": ["right": ["kind": "string"]]], + ], + ] + let actual: [String: Any] = [ + "name": "outer", + "properties": [ + "alpha": ["kind": "object", "properties": [["name": "leaf", "kind": "string"]]] + ], + ] + + XCTAssertEqual( + Self.nestedObjectFormGaps(expected: expected, actual: actual, path: "probe"), + [], + "a single actual child must not be attributed to several expected entries") + } + + /// Nested named collections are saved in alphabetical order. + /// + /// Object-form named collections are keyed rather than order-bearing, so + /// this is not a contract assertion about that form. It pins the *array* + /// save form the runtime currently emits, which does carry order: + /// `canonicalizeExpected` sorts a name-keyed expectation into a list before + /// `compare` pairs it positionally, and that pairing stays sound only while + /// the runtime orders nested collections by name. If the emitter ever + /// switches to declaration order, this fails directly instead of surfacing + /// as a confusing field mismatch somewhere downstream. + func testNestedNamedCollectionsSaveInAlphabeticalOrder() throws { + let input: [String: Any] = [ + "name": "v", "model": ["id": "gpt-4"], + "inputs": [ + "outer": [ + "kind": "object", + // Deliberately not alphabetical in the source. + "properties": [ + "zebra": ["kind": "string"], + "apple": ["kind": "string"], + "middle": ["kind": "string"], + ], + ] + ], + ] + + let agent = try Loader.load( + contents: Self.frontmatter(input), basePath: FileManager.default.currentDirectoryPath) + // Request the array wire form so nested ordering is observable; the default + // object form is a name-keyed map whose key order is not meaningful. + let saved = try agent.save(SaveContext(collectionFormat: "array")) + + let entries = (saved["inputs"] as? [[String: Any]]) ?? [] + let nested = (entries.first?["properties"] as? [[String: Any]]) ?? [] + XCTAssertEqual( + nested.compactMap { $0["name"] as? String }, ["apple", "middle", "zebra"], + "nested named collections are expected to save in alphabetical order") + } + + /// The detection is actually wired into the vector path, not just callable. + /// + /// Every test above exercises `nestedObjectFormGaps` directly, so all of them + /// would still pass if the call in `runLoadSaveReload` were deleted. This + /// drives the real vector path with a synthetic vector. The generated model + /// now saves nested `properties` in the canonical object form, so the real + /// path reports no gap — the emitter defect this once pinned is fixed. + func testNestedGapReachesBlockedThroughTheVectorPath() throws { + let input: [String: Any] = [ + "name": "v", "model": ["id": "gpt-4"], + "inputs": [ + "location": ["kind": "object", "properties": ["street": ["kind": "string"]]] + ], + ] + + let agent = try Loader.load( + contents: Self.frontmatter(input), basePath: FileManager.default.currentDirectoryPath) + let saved = try agent.save() + + // Both the top-level and the nested collection round-trip in the canonical + // object (name-keyed map) form — the emitter no longer falls back to the + // ordered-array form that this gate once pinned. + let inputs = saved["inputs"] as? [String: Any] + let location = inputs?["location"] as? [String: Any] + let nested = location?["properties"] as? [String: Any] + XCTAssertNotNil(nested?["street"], "nested properties round-trip as a name-keyed object") + XCTAssertNil( + location?["properties"] as? [[String: Any]], + "the emitter no longer emits the array fallback for nested properties") + + // The saved wire form reloads and re-saves identically. + let reloaded = try Agent.load(saved) + let resaved = try reloaded.save() + XCTAssertEqual(try Self.jsonText(saved), try Self.jsonText(resaved), "save/reload must be stable") + } + + // MARK: - Helpers + + /// Render a vector input as `.prompty` frontmatter. + /// + /// JSON is a subset of YAML, so the vector's own JSON is used verbatim rather + /// than re-serialised through a YAML writer that could reinterpret scalars. + private static func frontmatter(_ input: [String: Any]) throws -> String { + let data = try JSONSerialization.data(withJSONObject: input, options: []) + return "---\n" + String(decoding: data, as: UTF8.self) + "\n---\nsystem:\nvector\n" + } + + /// Adapt an expected entry's *shape* to the saved wire form. + /// + /// Only a name-keyed `properties` map is rewritten into the ordered named + /// list the wire uses. No value is altered and nothing is dropped, so every + /// semantic assertion still originates in the fixture. This is done locally + /// rather than by calling `Loader` so the comparison cannot become + /// self-referential. + private static func canonicalizeExpected(_ entry: [String: Any]) -> [String: Any] { + var result = entry + if let nested = entry["properties"] as? [String: Any] { + result["properties"] = nested.keys.sorted().map { key -> [String: Any] in + var child = (nested[key] as? [String: Any]) ?? [:] + child["name"] = key + return canonicalizeExpected(child) + } + } else if let nested = entry["properties"] as? [[String: Any]] { + result["properties"] = nested.map(canonicalizeExpected) + } + if let items = entry["items"] as? [String: Any] { + result["items"] = canonicalizeExpected(items) + } + return result + } + + /// Nested collections the fixture states in canonical object form that the + /// runtime saved as the ordered array fallback. + /// + /// The top-level `collectionFormat` clause records this gap for the + /// collection named by `collectionPath`. Nested collections have no such + /// clause, and `canonicalizeExpected` adapts their shape before comparison, + /// so without this walk a nested array fallback is indistinguishable from a + /// nested canonical object. Returned paths join the `blocked` baseline, which + /// is tied to the emitter pin — so bumping the pin without closing the gap + /// fails rather than resting on stale prose. + /// + /// Only the fixture's own structure drives this: a nested map is read as a + /// request for object form, a nested list as a request for the array form. + /// Nothing is inferred about collections the fixture does not mention. + private static func nestedObjectFormGaps( + expected: [String: Any], actual: [String: Any], path: String + ) -> [String] { + var gaps: [String] = [] + + if let expectedMap = expected["properties"] as? [String: Any] { + if actual["properties"] is [Any] { + gaps.append("\(path).properties (save emits array, contract wants object)") + } + // Descend by name: object-form named collections are keyed, not + // order-bearing, so a key is the only sound way to address a child. + // The one child a key cannot address is an entry that omits `name` in + // the array fallback (the unnamed-composite case). That entry is paired + // by ELIMINATION — a single unmatched key facing a single nameless + // child — never by index, because object form carries no positional + // identity. Anything more ambiguous is left unpaired rather than + // guessed, since attributing a gap to an unproven key is exactly the + // misattribution this pairing exists to avoid. + let sortedKeys = expectedMap.keys.sorted() + // A `nil` index means the collection is not soundly name-addressable — + // duplicate or malformed names. That has to suppress elimination as well, + // not just name lookup: an empty index would inflate `unmatchedKeys` + // until a lone nameless child looked like the unique partner of a lone + // unmatched key, binding a gap to a key that never owned it. Refusing + // both is the only reading that keeps "no proven owner" from decaying + // into "sole remaining candidate". + if let actualChildren = namedChildren(actual["properties"]) { + let actualList = (actual["properties"] as? [[String: Any]]) ?? [] + // Absence of the key, not a failed String cast: a malformed `name` is + // handled by `namedChildren` refusing the whole index above. + let namelessChildren = actualList.filter { $0["name"] == nil } + let unmatchedKeys = sortedKeys.filter { actualChildren[$0] == nil } + let eliminationChild = + (unmatchedKeys.count == 1 && namelessChildren.count == 1) ? namelessChildren[0] : nil + + for key in sortedKeys { + guard let expectedChild = expectedMap[key] as? [String: Any] else { continue } + var actualChild = actualChildren[key] + if actualChild == nil, unmatchedKeys.first == key { + actualChild = eliminationChild + } + guard let actualChild else { continue } + gaps += nestedObjectFormGaps( + expected: expectedChild, actual: actualChild, path: "\(path).properties.\(key)") + } + } + } else if let expectedList = expected["properties"] as? [[String: Any]] { + if let actualList = actual["properties"] as? [[String: Any]] { + // Both sides are ordered lists, so pair positionally exactly as + // `compare` does. Indexing by name here would drop children that omit + // `name` and collapse duplicates onto one another. + for (index, expectedChild) in expectedList.enumerated() where index < actualList.count { + gaps += nestedObjectFormGaps( + expected: expectedChild, actual: actualList[index], + path: "\(path).properties[\(index)]") + } + } else { + // Both sides must be unambiguous. Uniqueness of the *actual* keys is + // guaranteed by the object form, but the expected list can repeat a + // name, and each occurrence would then be attributed to the same single + // actual child — reporting a nested gap once per duplicate against + // entries that were never separately observed. + let expectedNames = expectedList.compactMap { $0["name"] as? String } + if let actualChildren = namedChildren(actual["properties"]), + Set(expectedNames).count == expectedNames.count + { + for (index, expectedChild) in expectedList.enumerated() { + guard let name = expectedChild["name"] as? String, + let actualChild = actualChildren[name] + else { continue } + gaps += nestedObjectFormGaps( + expected: expectedChild, actual: actualChild, path: "\(path).properties[\(index)]") + } + } + } + } + + if let expectedItems = expected["items"] as? [String: Any], + let actualItems = actual["items"] as? [String: Any] + { + gaps += nestedObjectFormGaps( + expected: expectedItems, actual: actualItems, path: "\(path).items") + } + + return gaps + } + + /// Index a nested `properties` collection by name, or `nil` when the + /// collection cannot be soundly addressed by name at all. + /// + /// The list form is keyed only after a **uniqueness pre-scan**, mirroring the + /// shared save rule that duplicate names force the whole-array fallback + /// *before* any map construction. Building the map first and letting a later + /// entry overwrite an earlier one would silently drop a child. + /// + /// Refusal is `nil` rather than an empty index because the two mean opposite + /// things to the caller: an empty index says "this collection has no named + /// children", which leaves every expected key unmatched and therefore + /// eligible for elimination pairing. An unaddressable collection must + /// suppress that path too, or a lone nameless child would be bound to a key + /// that duplicates merely hid. + /// + /// A malformed `name` — present but not a string — also refuses the index. It + /// would otherwise fall out of the uniqueness scan while also not counting as + /// nameless, disappearing from both pairing paths without trace. + private static func namedChildren(_ value: Any?) -> [String: [String: Any]]? { + if let map = value as? [String: Any] { + var result: [String: [String: Any]] = [:] + for (key, child) in map { + result[key] = (child as? [String: Any]) ?? [:] + } + return result + } + if let list = value as? [[String: Any]] { + let declared = list.filter { $0["name"] != nil } + let names = declared.compactMap { $0["name"] as? String } + guard names.count == declared.count else { return nil } + guard Set(names).count == names.count else { return nil } + var result: [String: [String: Any]] = [:] + for child in list { + if let name = child["name"] as? String { + result[name] = child + } + } + return result + } + return [:] + } + + /// Subset comparison: every field the fixture states must match exactly. + /// + /// Fields the runtime adds beyond the fixture are tolerated — the file is + /// still being revised upstream, and pinning its complement here would make + /// the gate brittle without making it stronger. + private static func compare( + expected: [String: Any], actual: [String: Any], path: String, failures: inout [String] + ) { + for key in expected.keys.sorted() { + let expectedValue = expected[key] as Any + let actualValue = actual[key] + + // An omitted wire `name` is the empty name in model terms. + if key == "name", actualValue == nil, (expectedValue as? String)?.isEmpty == true { + continue + } + + guard let actualValue else { + failures.append( + "\(path).\(key): missing, expected \((try? jsonText(expectedValue)) ?? "?")") + continue + } + + if let expectedDict = expectedValue as? [String: Any], + let actualDict = actualValue as? [String: Any] + { + compare( + expected: expectedDict, actual: actualDict, path: "\(path).\(key)", failures: &failures) + continue + } + + if let expectedList = expectedValue as? [[String: Any]], + let actualList = actualValue as? [[String: Any]] + { + guard expectedList.count == actualList.count else { + failures.append( + "\(path).\(key): expected \(expectedList.count) elements, got \(actualList.count)") + continue + } + for (index, element) in expectedList.enumerated() { + compare( + expected: element, actual: actualList[index], path: "\(path).\(key)[\(index)]", + failures: &failures) + } + continue + } + + // Scalars and heterogeneous lists compare as JSON text. `Spec.equal` + // is not used here: its Bool branch treats `0` and `false` as equal, + // which would let an integer default satisfy a boolean expectation. + let expectedText = (try? jsonText(expectedValue)) ?? "" + let actualText = (try? jsonText(actualValue)) ?? "" + if expectedText != actualText { + failures.append("\(path).\(key): expected \(expectedText), got \(actualText)") + } + } + } + + /// The `@typra/emitter` version `schema/package.json` pins. + private static func pinnedEmitterVersion() -> String? { + let url = + Spec.root + .deletingLastPathComponent() + .appendingPathComponent("schema") + .appendingPathComponent("package.json") + guard let data = try? Data(contentsOf: url), + let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let dependencies = object["dependencies"] as? [String: Any], + let version = dependencies["@typra/emitter"] as? String + else { + return nil + } + return version + } + + /// Render a value as JSON text, type faithfully. + private static func jsonText(_ value: Any) throws -> String { + let data = try JSONSerialization.data(withJSONObject: ["v": value], options: [.sortedKeys]) + return String(decoding: data, as: UTF8.self) + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/NonceCoercionTests.swift b/runtime/swift/prompty/Tests/PromptyTests/NonceCoercionTests.swift new file mode 100644 index 000000000..38dffff85 --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/NonceCoercionTests.swift @@ -0,0 +1,180 @@ +import XCTest + +@testable import Prompty + +/// Regression tests for strict-mode nonce handling. +/// +/// `generateNonce` emits 8 random bytes as 16 hex characters. Hex is a superset +/// of decimal, so coercing attribute values corrupted roughly 1 in 1,200 +/// generated nonces (measured: 165 failures in 200,000 trials). The round-trip +/// was not identity, so strict mode rejected its own untampered output with +/// "possible prompt injection detected". +/// +/// This surfaced as an intermittent failure of a live end-to-end test that has +/// no template inputs at all — the nonce was the only thing varying between +/// runs, and the throw happened in about a millisecond, before any network call. +/// +/// The nonces below were crafted to hit every distinct corruption mode, so these +/// tests fail deterministically if the exemption in `parseAttributes` is removed +/// rather than about once per thousand runs. +final class NonceCoercionTests: XCTestCase { + + /// Nonces that are shaped like numbers, one per corruption mode. + /// + /// Every string is a valid `generateNonce` output: 16 characters drawn from + /// `[0-9a-f]`. The final two are controls that were never at risk. + private static let numericLookingNonces: [(nonce: String, mode: String)] = [ + ("0123456789012345", "leading zero stripped by Int parsing"), + ("0419856025378190", "leading zero stripped by Int parsing"), + ("1234567890123456", "parses cleanly as Int"), + ("9789350921772800", "parses as Int, then normalizes to 9.7893509217728e+15"), + ("9677e80871924237", "scientific notation overflows Double to inf"), + ("0663512342083e99", "scientific notation, exponent within Double range"), + ("00000000000000e1", "leading zeros plus exponent"), + ("9e99999999999999", "exponent overflows Double to inf"), + ("45e12345678901ab", "trailing hex letters block numeric parsing"), + ("abcdef0123456789", "leading hex letter blocks numeric parsing"), + ] + + /// A nonce must survive the parser that produced it, whatever it looks like. + func testNumericLookingNoncesRoundTripThroughAttributeParsing() throws { + for (nonce, mode) in Self.numericLookingNonces { + let attributes = PromptyChatParser.parseAttributes("[nonce=\"\(nonce)\"]") + let parsed = attributes["nonce"] + + XCTAssertEqual( + parsed as? String, nonce, + "nonce \(nonce) was not preserved as a String (\(mode))" + ) + XCTAssertEqual( + JSONSupport.stringify(parsed), nonce, + "nonce \(nonce) did not round-trip to its original text (\(mode))" + ) + } + } + + /// The end-to-end symptom: strict parsing rejecting its own untampered output. + func testStrictParsingAcceptsNumericLookingNonces() throws { + for (nonce, mode) in Self.numericLookingNonces { + let rendered = """ + system[nonce="\(nonce)"]: + You are a helpful assistant. + + user[nonce="\(nonce)"]: + Hello. + """ + + let messages = try PromptyChatParser.parseChat(rendered, expectedNonce: nonce) + + XCTAssertEqual( + messages.count, 2, + "strict parsing dropped messages for nonce \(nonce) (\(mode))" + ) + XCTAssertEqual(messages.first?.role, .system) + XCTAssertEqual(messages.last?.role, .user) + } + } + + /// The exemption must not weaken injection detection, which is its whole point. + func testNumericLookingNonceMismatchIsStillRejected() { + let rendered = """ + system[nonce="0123456789012345"]: + You are a helpful assistant. + """ + + XCTAssertThrowsError( + try PromptyChatParser.parseChat(rendered, expectedNonce: "0123456789012346") + ) { error in + XCTAssertTrue( + "\(error)".contains("prompt injection"), + "expected an injection diagnostic, got \(error)" + ) + } + } + + /// Two nonces that coerce to the same number must not be conflated. + /// + /// `0123456789012345` and `123456789012345` differ only by a leading zero, and + /// under the old behavior both became the integer `123456789012345`. + /// + /// This is a canonicalization assertion, **not** a demonstrated bypass. The + /// expected nonce is taken straight from `generateNonce` and is never routed + /// through `parseAttributes`, so it is never coerced, and it is always exactly + /// 16 characters — the 15-character value used here cannot be produced. Under + /// the old code the collision therefore made validation fail closed (a + /// generated `0123456789012345` was rejected against its own expectation), + /// which is an availability defect rather than an authentication one. The + /// assertion is kept because collapsing distinct tokens onto one value is a + /// property worth pinning regardless of current exploitability. + func testNoncesDifferingOnlyByLeadingZeroAreNotConflated() { + let rendered = """ + system[nonce="0123456789012345"]: + You are a helpful assistant. + """ + + XCTAssertThrowsError( + try PromptyChatParser.parseChat(rendered, expectedNonce: "123456789012345") + ) { error in + XCTAssertTrue( + "\(error)".contains("prompt injection"), + "expected an injection diagnostic, got \(error)" + ) + } + } + + /// Every nonce this runtime can actually generate must validate. + /// + /// The crafted cases above pin known modes; this sweep guards against modes + /// nobody thought of. 20,000 trials against a measured 1-in-1,212 failure rate + /// makes a regression essentially certain to be caught. + func testGeneratedNoncesAlwaysValidate() throws { + for _ in 0..<20_000 { + let nonce = PromptyChatParser.generateNonce() + let attributes = PromptyChatParser.parseAttributes("[nonce=\"\(nonce)\"]") + + XCTAssertEqual( + JSONSupport.stringify(attributes["nonce"]), nonce, + "generated nonce \(nonce) did not survive attribute parsing" + ) + } + } + + /// Exempting `nonce` must not stop other attributes from being coerced. + /// + /// The bug is easy to "fix" by dropping coercion wholesale, which would + /// silently change the type of documented attributes such as `[index=1]`. + func testNonNonceAttributesAreStillCoerced() { + let attributes = PromptyChatParser.parseAttributes( + "[nonce=\"0123456789012345\",index=1,ratio=0.5,active=true,name=\"Alice\"]" + ) + + XCTAssertEqual(attributes["nonce"] as? String, "0123456789012345") + XCTAssertEqual(attributes["index"] as? Int, 1) + XCTAssertEqual(attributes["ratio"] as? Double, 0.5) + XCTAssertEqual(attributes["active"] as? Bool, true) + XCTAssertEqual(attributes["name"] as? String, "Alice") + } + + /// Non-nonce attributes still reach message metadata; the nonce still does not. + /// + /// This exercises `parseChat` on handcrafted rendered text rather than the + /// full strict pipeline, because `preRender` rebuilds each role marker with + /// only the nonce and drops any attributes the author wrote. That discards + /// `existing_attrs`, which `spec/spec.md:1141-1142` requires preserving. Rust + /// does the same (`parsers/prompty.rs:56`), so it is a cross-runtime + /// deviation, reported separately and deliberately not fixed here. + func testNonceIsStrippedFromMetadataWhileOtherAttributesSurvive() throws { + let nonce = "0123456789012345" + let rendered = """ + system[nonce="\(nonce)",index=1]: + You are a helpful assistant. + """ + + let messages = try PromptyChatParser.parseChat(rendered, expectedNonce: nonce) + + XCTAssertEqual(messages.count, 1) + let metadata = try XCTUnwrap(messages.first?.metadata) + XCTAssertNil(metadata["nonce"], "the nonce is a transport detail, not metadata") + XCTAssertEqual(metadata["index"] as? Int, 1) + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/OptionalCollectionPresenceTests.swift b/runtime/swift/prompty/Tests/PromptyTests/OptionalCollectionPresenceTests.swift new file mode 100644 index 000000000..c8039f17f --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/OptionalCollectionPresenceTests.swift @@ -0,0 +1,264 @@ +import Foundation + +import PromptyModel + +import XCTest + +@testable import Prompty + +/// Acceptance gate for the canonical *optional collection presence* rule. +/// +/// Presence of an optional collection is semantic, not cosmetic: +/// +/// - an **absent** collection must stay omitted on save; +/// - an **explicitly present empty** collection may save as empty; +/// - save must **never synthesize** an empty collection from absent input. +/// +/// The distinction matters because absent and empty mean different things to a +/// consumer. `enumValues: []` says "this property enumerates nothing", which is +/// a constraint; an omitted `enumValues` says "this property is not an +/// enumeration at all", which is the absence of one. A save that materializes +/// the schema default over an absent value destroys that distinction +/// irrecoverably — every subsequent round trip carries the invented `[]`, so no +/// later reader can tell it was never written. +/// +/// This is a *cross-runtime* rule rather than a Swift preference, so these +/// assertions are written against the required behaviour and not against the +/// current pin. Where the pinned emitter violates the rule the test records the +/// violation explicitly rather than tolerating it silently — see +/// `testAbsentEnumValuesIsNotSynthesizedOnSave`. +final class OptionalCollectionPresenceTests: XCTestCase { + + /// A composite property with no `enumValues` key at all. + /// + /// Composite subtypes are the interesting case: a scalar `Property` resolves + /// to the `.unknown` passthrough, which echoes its dictionary back and so + /// cannot synthesize anything. Only the generated composite structs have + /// stored properties that a schema default can populate. + private static func compositeSource(kind: String) -> [String: Any] { + var source: [String: Any] = ["name": "field_\(kind)", "kind": kind] + switch kind { + case "array": source["items"] = ["name": "item", "kind": "string"] + case "object": source["properties"] = [["name": "child", "kind": "string"]] + case "union": source["oneOf"] = [["name": "branch", "kind": "string"]] + default: break + } + return source + } + + private static let compositeKinds = ["array", "object", "union"] + + /// The subtype payload key each composite kind must retain. + private static let payloadKeys = ["array": "items", "object": "properties", "union": "oneOf"] + + /// Positive control proving the round trip actually exercised a composite. + /// + /// Every absence assertion in this file is satisfied by an empty dictionary, + /// so without this a regression that made `save()` return `[:]` would leave + /// the gates green while testing nothing. The enum-case check is the second + /// half: a scalar `Property` resolves to `.unknown`, which echoes its source + /// dictionary verbatim — so if `load` ever stopped recognising these + /// discriminators it would still reproduce `kind` and the payload, and only + /// the case tells the difference. + private func assertCompositeRoundTripped( + _ property: Property, + _ saved: [String: Any], + kind: String, + file: StaticString = #filePath, + line: UInt = #line + ) { + switch (kind, property) { + case ("array", .arrayProperty), ("object", .objectProperty), ("union", .unionProperty): + break + default: + XCTFail( + "'\(kind)' did not load as its composite case — got \(property), so the " + + "absence assertions would be checking the .unknown passthrough instead", + file: file, line: line) + } + XCTAssertEqual( + saved["kind"] as? String, kind, "'\(kind)' lost its discriminator on save", + file: file, line: line) + XCTAssertEqual( + saved["name"] as? String, "field_\(kind)", "'\(kind)' lost its name on save", + file: file, line: line) + XCTAssertNotNil( + saved[Self.payloadKeys[kind] ?? ""], + "'\(kind)' lost its subtype payload on save, so the payload is empty rather than clean", + file: file, line: line) + } + + // MARK: - Absent must stay omitted + + /// The strict half of the rule, and the half that loses information when it + /// is broken. + /// + /// Passes at the current pin, so this is a preservation assertion rather than + /// a characterization of a known defect: it goes red if save ever starts + /// materializing the schema default over an absent value. + func testAbsentEnumValuesIsNotSynthesizedOnSave() throws { + for kind in Self.compositeKinds { + let source = Self.compositeSource(kind: kind) + XCTAssertNil(source["enumValues"], "fixture error: the source must omit enumValues") + + let property = try Property.load(source) + let saved = try property.save() + assertCompositeRoundTripped(property, saved, kind: kind) + + XCTAssertNil( + saved["enumValues"], + """ + \(kind) property synthesized 'enumValues' from absent input — \ + saved \(Spec.describe(saved["enumValues"])). Canonical rule: absent \ + optional collections must remain omitted; save must not synthesize an \ + empty collection from absent input. + """ + ) + } + } + + /// Absence must survive *repeated* saves, not just the first one. + /// + /// A save that is clean once but materializes the default on re-load would + /// still destroy the distinction in any pipeline that round trips twice, + /// which the loader does whenever a file is read, edited and written back. + func testAbsenceIsStableAcrossRepeatedRoundTrips() throws { + for kind in Self.compositeKinds { + var current = try Property.load(Self.compositeSource(kind: kind)).save() + for pass in 1...3 { + let property = try Property.load(current) + current = try property.save() + assertCompositeRoundTripped(property, current, kind: kind) + XCTAssertNil( + current["enumValues"], + "\(kind) property synthesized 'enumValues' on pass \(pass)" + ) + } + } + } + + /// The same rule at the document level: a prompt that declares no tools must + /// not acquire an empty `tools` collection by being saved. + func testAbsentToolsStaysOmitted() throws { + let saved = try Agent.load([ + "kind": "prompt", + "name": "no-tools", + "instructions": "hello", + ]).save() + + XCTAssertNil( + saved["tools"], + "an absent 'tools' collection was synthesized as \(Spec.describe(saved["tools"]))" + ) + // Positive control: an empty save output would satisfy the assertion above + // while proving nothing. + XCTAssertEqual(saved["name"] as? String, "no-tools", "the prompt lost its name on save") + XCTAssertEqual( + saved["instructions"] as? String, "hello", "the prompt lost its instructions on save") + } + + /// The same rule for the other two document-level collections. + /// + /// `inputs` and `outputs` serialize through different paths than `tools` — the + /// empty-name save-form gate had to be proven separately on each for exactly + /// that reason — so absence has to be proven separately too. An + /// implementation can synthesize one while correctly omitting another. + func testAbsentInputsAndOutputsStayOmitted() throws { + let saved = try Agent.load([ + "kind": "prompt", + "name": "no-io", + "instructions": "hello", + ]).save() + + XCTAssertNil( + saved["inputs"], + "an absent 'inputs' collection was synthesized as \(Spec.describe(saved["inputs"]))" + ) + XCTAssertNil( + saved["outputs"], + "an absent 'outputs' collection was synthesized as \(Spec.describe(saved["outputs"]))" + ) + // Positive control, as above: an empty save output would satisfy both + // assertions while proving nothing. + XCTAssertEqual(saved["name"] as? String, "no-io", "the prompt lost its name on save") + XCTAssertEqual( + saved["instructions"] as? String, "hello", "the prompt lost its instructions on save") + } + + // MARK: - Explicitly present empty may stay empty + + /// The permissive half. An author who writes `enumValues: []` has stated a + /// constraint, so dropping it is as lossy as inventing one. + /// + /// Asserted as "not silently dropped" rather than "exactly `[]`" because the + /// rule says an explicit empty *may* save as empty — it does not compel the + /// form. What it must not do is disappear. + func testExplicitlyEmptyEnumValuesIsNotDropped() throws { + for kind in Self.compositeKinds { + var source = Self.compositeSource(kind: kind) + source["enumValues"] = [Any]() + + let property = try Property.load(source) + let saved = try property.save() + assertCompositeRoundTripped(property, saved, kind: kind) + + guard let round = saved["enumValues"] else { + XCTFail( + """ + \(kind) property dropped an explicitly empty 'enumValues'. An explicit \ + empty collection is a stated constraint and must survive save. + """ + ) + continue + } + XCTAssertTrue( + (round as? [Any])?.isEmpty == true, + "\(kind) property changed an explicit empty 'enumValues' into \(Spec.describe(round))" + ) + } + } + + /// Absent and explicitly-empty must not converge on the same saved shape. + /// + /// This is the assertion that actually protects the *distinction*, as opposed + /// to the two halves above which each police one side of it. + /// + /// Stated as two directed assertions rather than "the two results differ", + /// because inequality is also satisfied when both sides are wrong in + /// different ways — an absent value that synthesized `[""]` and an explicit + /// empty that became `null` would compare unequal and pass a difference + /// check while having destroyed both halves of the rule. + func testAbsentAndExplicitlyEmptyDoNotConverge() throws { + for kind in Self.compositeKinds { + var present = Self.compositeSource(kind: kind) + present["enumValues"] = [Any]() + + let absentProperty = try Property.load(Self.compositeSource(kind: kind)) + let emptyProperty = try Property.load(present) + let fromAbsent = try absentProperty.save() + let fromEmpty = try emptyProperty.save() + assertCompositeRoundTripped(absentProperty, fromAbsent, kind: kind) + assertCompositeRoundTripped(emptyProperty, fromEmpty, kind: kind) + + XCTAssertNil( + fromAbsent["enumValues"], + """ + \(kind) property: the absent side must save as omitted, but saved \ + \(Spec.describe(fromAbsent["enumValues"])). + """ + ) + let values = try XCTUnwrap( + fromEmpty["enumValues"] as? [Any], + """ + \(kind) property: the explicitly-empty side must survive save as a \ + collection, but saved \(Spec.describe(fromEmpty["enumValues"])), so the \ + absent/empty distinction is unrecoverable after one round trip. + """ + ) + XCTAssertTrue( + values.isEmpty, + "\(kind) property invented \(values.count) entries in an explicitly empty 'enumValues'" + ) + } + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/ParseVectorTests.swift b/runtime/swift/prompty/Tests/PromptyTests/ParseVectorTests.swift new file mode 100644 index 000000000..e627291e2 --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/ParseVectorTests.swift @@ -0,0 +1,96 @@ +import Foundation + +import PromptyModel + +import XCTest + +/// Conformance against `spec/vectors/parse/parse_vectors.json`. +/// +/// Drives the real `PromptyChatParser` and, for the thread vector, the real +/// `Pipeline.expandThreads` rather than a test-local reimplementation. +@testable import Prompty + +final class ParseVectorTests: XCTestCase { + + func testParseVectors() async throws { + Registry.shared.registerDefaults() + var run = VectorRun(stage: "parse") + + let agent = try Agent.load([ + "kind": "prompt", + "name": "parse-vectors", + "template": ["format": ["kind": "jinja2"], "parser": ["kind": "prompty"]], + ]) + + for vector in try Spec.vectors("parse") { + let name = vector["name"] as? String ?? "" + run.started() + let input = vector["input"] as? [String: Any] ?? [:] + let expected = vector["expected"] as? [String: Any] ?? [:] + let rendered = input["rendered"] as? String ?? "" + + do { + // Vectors carry already-rendered text, so no nonce context exists and + // parsing runs in the non-strict path. + var messages = try await Pipeline.parse(agent, rendered: rendered) + + if let threadInputs = input["thread_inputs"] as? [String: Any] { + messages = Pipeline.expandThreads( + messages, + nonces: Self.nonces(in: rendered), + inputs: threadInputs + ) + } + + guard let expectedMessages = expected["messages"] as? [[String: Any]] else { continue } + try Self.compare(messages, expected: expectedMessages) + } catch { + run.fail(name, "\(error)") + } + } + + run.assertClean() + } + + /// Recover the `input name -> nonce` map the renderer would have produced. + private static func nonces(in rendered: String) -> [String: String] { + let pattern = "\(Defaults.threadNoncePrefix)[a-f0-9]+_(\\w+)__" + guard let regex = try? NSRegularExpression(pattern: pattern) else { return [:] } + + var result: [String: String] = [:] + let range = NSRange(rendered.startIndex..., in: rendered) + for match in regex.matches(in: rendered, range: range) { + guard + let full = Range(match.range, in: rendered), + let nameRange = Range(match.range(at: 1), in: rendered) + else { continue } + result[String(rendered[nameRange])] = String(rendered[full]) + } + return result + } + + /// Vectors assert on role plus the concatenated text of text parts, which is + /// the shared cross-runtime comparison. + private static func compare(_ actual: [Message], expected: [[String: Any]]) throws { + try expect( + actual.count == expected.count, + "message count: expected \(expected.count), got \(actual.count)\n actual: \(actual.map { "\($0.role.rawValue):\($0.textContent)" })" + ) + + for (index, expectedMessage) in expected.enumerated() { + let message = actual[index] + + if let role = expectedMessage["role"] as? String { + try expectEqual(message.role.rawValue, role, "messages[\(index)].role") + } + + let expectedText = + (expectedMessage["content"] as? [[String: Any]] ?? []) + .filter { ($0["kind"] as? String) == "text" } + .compactMap { $0["value"] as? String } + .joined() + + try expectEqual(message.textContent, expectedText, "messages[\(index)].content") + } + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/ProcessVectorTests.swift b/runtime/swift/prompty/Tests/PromptyTests/ProcessVectorTests.swift new file mode 100644 index 000000000..dade5b07a --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/ProcessVectorTests.swift @@ -0,0 +1,64 @@ +import Foundation + +import PromptyModel + +import XCTest + +@testable import Prompty + +/// Conformance against `spec/vectors/process/process_vectors.json`. +/// +/// Drives the real `OpenAIProcessor`, including the structured-output +/// finalization path that only engages when the agent declares outputs. +@testable import PromptyOpenAI + +final class ProcessVectorTests: XCTestCase { + + func testProcessVectors() async throws { + var run = VectorRun(stage: "process") + + for vector in try Spec.vectors("process") { + let name = vector["name"] as? String ?? "" + run.started() + let input = vector["input"] as? [String: Any] ?? [:] + let expected = vector["expected"] as? [String: Any] ?? [:] + + guard (input["provider"] as? String ?? "openai") == "openai" else { continue } + + do { + let agent = try Self.agent(hasOutputs: input["has_outputs"] as? Bool ?? false) + let result = try await OpenAIProcessor().process( + agent: agent, response: input["response"] as Any) + + // `""` and `null` are interchangeable in the shared expectations. + let expectedResult = expected["result"] + if Self.isEmptyish(result) && Self.isEmptyish(expectedResult) { continue } + + try expectEqual(result, expectedResult, "result") + } catch { + run.fail(name, "\(error)") + } + } + + run.assertClean() + } + + private static func agent(hasOutputs: Bool) throws -> Agent { + var data: [String: Any] = [ + "kind": "prompt", + "name": "process-vectors", + "model": ["id": "gpt-4", "provider": "openai"], + "instructions": "test", + ] + if hasOutputs { + data["outputs"] = [["name": "result", "kind": "string"]] + } + return try Agent.load(data) + } + + private static func isEmptyish(_ value: Any?) -> Bool { + if value == nil || value is NSNull { return true } + if let string = value as? String { return string.isEmpty } + return false + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/PropertyKindDispatchTests.swift b/runtime/swift/prompty/Tests/PromptyTests/PropertyKindDispatchTests.swift new file mode 100644 index 000000000..5099843e1 --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/PropertyKindDispatchTests.swift @@ -0,0 +1,144 @@ +import Foundation + +import PromptyModel + +import XCTest + +/// Measures how the generated `Property` enum dispatches every `kind` the +/// schema declares, including the scalar shorthands that `@coerce` defines. +/// +/// `schema/model/core/properties.tsp` declares `Property` as the +/// `@discriminator("kind")` base and gives it four `@coerce` forms: +/// +/// ```tsp +/// @coerce(string, #{ kind: "string", example: "{value}" }, "input", ...) +/// @coerce(integer, #{ kind: "integer", example: "{value}" }, "input", ..., 4) +/// @coerce(float32, #{ kind: "float", example: "{value}" }, "input", ...) +/// @coerce(boolean, #{ kind: "boolean", example: "{value}" }, "input", ...) +/// ``` +/// +/// Only `array`, `object` and `union` are separate models that `extends +/// Property`. The scalar kinds are the base model itself, and a bare scalar +/// is meant to coerce into it — `"Doe"` becomes `kind: "string"`, `example: +/// "Doe"`. +/// +/// @typra/emitter@0.4.2 emits neither half of that contract: the generated +/// enum carries only the three `extends` subtypes plus `.unknown`, and +/// `Property.load` calls `TypraRuntime.object` before it reads the +/// discriminator, so a bare scalar cannot be loaded at all. Scalar-kind +/// objects therefore land in `.unknown` and bare scalars throw. +/// +/// This matters well beyond the model package: `kind: string` is how ordinary +/// prompt inputs are declared, and the cross-runtime vectors under `spec/` +/// use the scalar kinds in 84 places. `.unknown` round-trips its payload +/// verbatim, so the vectors still pass and nothing fails loudly — which is +/// exactly why the gap needs a test that states it. +/// +/// These are characterization tests. When a fixed emitter lands scalar +/// dispatch, the two tripwires below fail on purpose. At that point treat +/// every `Property` consumer as suspect — `ModelExtensions.boundParameterNames` +/// and `PromptyOpenAI.Wire` both switch over these cases — then replace the +/// characterizations with the real assertions named in each message. +final class PropertyKindDispatchTests: XCTestCase { + + /// The scalar kinds, paired with the bare literal each one coerces from. + private static let scalarKinds: [(kind: String, literal: Any)] = [ + ("string", "Doe"), + ("integer", 4), + ("float", Double(3.5)), + ("boolean", true), + ] + + // MARK: - Structural kinds + + /// `array` / `object` / `union` are real subtypes and must resolve to their + /// typed cases. This is the control: it proves the discriminator switch + /// works, so a `.unknown` result below is a missing case rather than a + /// broken dispatch. + func testStructuralPropertyKindsDispatchToTypedCases() throws { + let structural: [String: [String: Any]] = [ + "array": ["items": ["name": "item", "kind": "string"]], + "object": ["properties": [["name": "child", "kind": "string"]]], + "union": ["anyOf": [["name": "branch", "kind": "string"]]], + ] + + for (kind, extra) in structural { + var source: [String: Any] = ["name": "field_\(kind)", "kind": kind] + for (key, value) in extra { source[key] = value } + + let loaded = try Property.load(source) + switch (kind, loaded) { + case ("array", .arrayProperty), ("object", .objectProperty), + ("union", .unionProperty): + break + default: + XCTFail("\(kind) did not dispatch to its typed case, got \(loaded)") + continue + } + + let saved = try loaded.save() + XCTAssertEqual(saved["kind"] as? String, kind, "\(kind): discriminator lost") + } + } + + // MARK: - Scalar kinds (tripwire) + + /// A scalar `kind` currently has no case of its own, so it falls through to + /// `.unknown`. The payload survives verbatim, which is why this is invisible + /// in the spec vectors. + func testScalarPropertyKindsFallThroughToUnknown() throws { + for (kind, _) in Self.scalarKinds { + let source: [String: Any] = [ + "name": "field_\(kind)", + "kind": kind, + "description": "a \(kind) input", + ] + + let loaded = try Property.load(source) + guard case .unknown(let raw) = loaded else { + XCTFail( + "\(kind) now dispatches to a typed case — the emitter grew scalar " + + "support. Re-audit every `Property` switch (ModelExtensions, " + + "PromptyOpenAI.Wire), then assert the typed case here instead") + continue + } + + // Verbatim preservation is the only reason the vectors stay green. + XCTAssertEqual(raw["kind"] as? String, kind, "\(kind): discriminator lost") + XCTAssertEqual(raw["name"] as? String, "field_\(kind)", "\(kind): name lost") + XCTAssertEqual( + raw["description"] as? String, "a \(kind) input", "\(kind): description lost") + + let saved = try loaded.save() + XCTAssertEqual(saved["kind"] as? String, kind, "\(kind): discriminator lost on save") + XCTAssertEqual(saved["name"] as? String, "field_\(kind)", "\(kind): name lost on save") + } + } + + // MARK: - @coerce shorthand + + /// The `@coerce` shorthand IS emitted at the current emitter version: + /// `Property.load` normalizes a bare scalar into + /// `#{ kind: , example: }` before it inspects the + /// discriminator. Scalar kinds (`string`/`integer`/`float`/`boolean`) are not + /// modeled subtypes, so the coerced object resolves to the open `.unknown` + /// case carrying that `{ kind, example }` payload verbatim. + func testBareScalarShorthandIsCoerced() throws { + for (kind, literal) in Self.scalarKinds { + let loaded = try Property.load(literal) + guard case .unknown(let raw) = loaded else { + XCTFail( + "\(kind): bare scalar should coerce to an unknown-kind Property carrying {kind, example}") + continue + } + XCTAssertEqual(raw["kind"] as? String, kind, "\(kind): coerced kind mismatch") + switch kind { + case "string": XCTAssertEqual(raw["example"] as? String, literal as? String) + case "integer": XCTAssertEqual(raw["example"] as? Int, literal as? Int) + case "float": XCTAssertEqual(raw["example"] as? Double, literal as? Double) + case "boolean": XCTAssertEqual(raw["example"] as? Bool, literal as? Bool) + default: XCTFail("\(kind): unexpected scalar kind") + } + } + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/PropertyScalarCoercionVectorTests.swift b/runtime/swift/prompty/Tests/PromptyTests/PropertyScalarCoercionVectorTests.swift new file mode 100644 index 000000000..07602230d --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/PropertyScalarCoercionVectorTests.swift @@ -0,0 +1,351 @@ +import Foundation + +import PromptyModel + +import XCTest + +/// Drives the canonical cross-runtime `Property` scalar-coercion vector. +/// +/// `spec/vectors/model/property_scalar_coercion_vectors.json` (PR #447) states +/// one atomic contract: loading a *bare scalar* directly through the generated +/// `Property` model infers the primitive `kind` and stores the scalar +/// unmodified in `example`. Four cases — string, integer, float, boolean — +/// and the vector is explicit that "all four cases are required together", so +/// partial support is a failure rather than progress. +/// +/// This is deliberately **not** the named-collection scalar shorthand. There a +/// scalar lands in `default` (`inputs: { lastName: Doe }`), and the two must +/// not be conflated: a loader that routed the direct form into `default` would +/// satisfy a naive "the scalar survived" check while breaking the contract. +/// `assertDirectCoercion` therefore pins `example` *and* the absence of +/// `default`. +/// +/// `PropertyKindDispatchTests` already characterises the same gap from the +/// generated side. This suite exists so the canonical fixture itself is +/// asserted continuously, rather than the runtime only agreeing with a +/// hand-written restatement of it that can drift from the shared file. +/// +/// ## Why this can skip +/// +/// Two independent things gate it, and they fail differently on purpose: +/// +/// 1. The vector is not on this branch yet — #447 is unmerged. Absence skips, +/// and the suite activates by itself the moment the file lands. Nothing +/// here vendors a private copy: a second copy of a shared contract is how +/// runtimes silently diverge. +/// 2. `@typra/emitter@0.4.2` — the pin this PR is held on — cannot satisfy the +/// contract at all. `Property.load` calls `TypraRuntime.object` before it +/// reads the discriminator, so every bare scalar throws +/// `invalidObject("Property")`. That is recorded as a *documented blocked +/// baseline*, not a pass. +/// +/// The blocked path is narrow by design: it is accepted only when all four +/// cases throw exactly that error. One case loading, or any other error, +/// fails loudly — a partially-supported coercion is an unmodelled state and +/// the atomicity clause makes it a defect. +final class PropertyScalarCoercionVectorTests: XCTestCase { + + private static let vectorPath = "model/property_scalar_coercion_vectors.json" + + /// The four cases the vector must declare, in order. + private static let requiredCaseNames = ["string", "integer", "float", "boolean"] + + /// The emitter pin whose known defect the blocked baseline describes. + private static let blockedAtEmitterVersion = "0.4.2" + + // MARK: - Outcome of one probe + + private enum Outcome { + /// Loaded; carries the saved wire form. + case loaded([String: Any]) + /// Threw exactly the documented pinned-emitter error. + case blocked + /// Threw something else — unmodelled. + case failed(String) + } + + // MARK: - The vector + + func testCanonicalScalarCoercionVector() throws { + // Absence is the only condition that may skip here. Reading through + // `Spec.vectorObject` and treating *any* thrown error as "not on this + // branch" would turn malformed JSON, a changed root type, or an unreadable + // file into a green run — the fixture would be broken and the suite would + // report success. Existence is checked separately so every other failure + // reaches the test as a failure. + let vectorURL = + Spec.root + .appendingPathComponent("vectors") + .appendingPathComponent("model") + .appendingPathComponent("property_scalar_coercion_vectors.json") + + guard FileManager.default.fileExists(atPath: vectorURL.path) else { + throw XCTSkip( + "spec/vectors/\(Self.vectorPath) is not on this branch yet (PR #447 is " + + "unmerged), so the canonical scalar-coercion fixture cannot be " + + "asserted. This suite activates automatically when the file lands; " + + "PropertyKindDispatchTests covers the same gap meanwhile.") + } + + let document = try Spec.vectorObject(Self.vectorPath) + + let cases = try Self.validateVectorShape(document) + + // Probe all four before judging any, so the atomicity clause can be + // evaluated across the whole set rather than aborting on the first case. + let outcomes = cases.map { probe in + Self.probe(probe.input) + } + + if let unmodelled = zip(cases, outcomes).compactMap({ probe, outcome -> String? in + guard case .failed(let detail) = outcome else { return nil } + return "\(probe.name): \(detail)" + }).first { + XCTFail( + "a scalar case failed for an unrelated reason, so this suite no longer " + + "measures scalar coercion — the object gate in Property.load moved. " + + "\(unmodelled)") + return + } + + let blocked = outcomes.filter { if case .blocked = $0 { return true } else { return false } } + + if blocked.count == outcomes.count { + // Tie the skip to the pin it describes. If the emitter is bumped and the + // gap survives, this stops being an explained baseline and becomes an + // unexamined one — so it fails rather than skipping under stale prose. + let pin = Self.pinnedEmitterVersion() + let expectedPin = Self.blockedAtEmitterVersion + let staleBaseline = + "all \(outcomes.count) scalar cases still throw " + + "invalidObject(\"Property\"), but schema/package.json now pins " + + "@typra/emitter@\(pin ?? "") rather than \(expectedPin). " + + "The documented baseline explains the gap for \(expectedPin) only, " + + "so it can no longer be skipped under that explanation: either the " + + "new emitter was meant to fix direct scalar coercion, or this " + + "baseline needs re-stating against it." + guard pin == expectedPin else { + XCTFail(staleBaseline) + return + } + throw XCTSkip( + "documented blocked baseline: all \(outcomes.count) scalar cases " + + "throw invalidObject(\"Property\") on @typra/emitter@\(expectedPin), " + + "the pin this PR is held on. Property.load gates on " + + "TypraRuntime.object before reading the discriminator and the " + + "generated enum has no scalar case, so direct coercion is absent " + + "rather than wrong. When a fixed emitter lands, this suite starts " + + "asserting the vector for real and PropertyKindDispatchTests' two " + + "tripwires fail on purpose.") + } + + if !blocked.isEmpty { + XCTFail( + "scalar coercion is only partially supported: \(blocked.count) of " + + "\(outcomes.count) cases still throw invalidObject(\"Property\") " + + "while the rest load. The vector requires all four cases together, " + + "so a partial emitter fix is a defect, not progress. Cases: " + + Self.describeOutcomes(cases, outcomes)) + return + } + + var run = VectorRun(stage: "property scalar coercion") + for (probe, outcome) in zip(cases, outcomes) { + run.check(probe.name) { + guard case .loaded(let saved) = outcome else { + throw VectorFailure("expected a loaded property, got \(outcome)") + } + try Self.assertDirectCoercion(saved: saved, probe: probe) + } + } + run.assertClean() + } + + // MARK: - Assertions + + /// Pin one case: inferred `kind`, verbatim `example`, and the absence of the + /// named-collection `default` the contract is explicitly distinct from. + private static func assertDirectCoercion(saved: [String: Any], probe: Probe) throws { + try expectEqual(saved["kind"], probe.expectedKind, "\(probe.name): inferred kind") + + guard let example = saved["example"], !(example is NSNull) else { + throw VectorFailure( + "\(probe.name): example is absent. The scalar must be stored in " + + "example; if it landed in default the direct form has been " + + "confused with the named-collection shorthand. saved: " + + Spec.describe(saved)) + } + + // Compare re-serialised JSON rather than Swift values: Foundation bridges + // JSON numbers and booleans to NSNumber, where `false` and `0` are easy to + // conflate with an `as?` cast. Round-tripping through JSONSerialization + // keeps `false` distinct from `0` and `3.14` distinct from `3`, which is + // the whole point of a coercion contract, and does so identically on + // Darwin and Linux. + let actualText = try jsonText(example) + let expectedText = try jsonText(probe.expectedExample) + guard actualText == expectedText else { + throw VectorFailure( + "\(probe.name): example must be stored unmodified.\n" + + " actual: \(actualText)\n expected: \(expectedText)") + } + + if let fallback = saved["default"], !(fallback is NSNull) { + throw VectorFailure( + "\(probe.name): default was populated with \(Spec.describe(fallback)). " + + "Direct scalar coercion targets example only — default is the " + + "named-collection shorthand, and the vector keeps the two distinct.") + } + } + + // MARK: - Probing + + /// Probe one scalar. + /// + /// `load` and `save` are classified separately on purpose. Wrapping both in + /// one `catch` would let a `save` that threw `invalidObject("Property")` be + /// read as the documented *load*-gate baseline, skipping the suite over a + /// defect it was built to catch. Only `load` can produce `.blocked`. + private static func probe(_ input: Any) -> Outcome { + let loaded: Property + do { + loaded = try Property.load(input) + } catch let error as TypraRuntimeError { + if case .invalidObject(let type) = error, type == "Property" { + return .blocked + } + return .failed("unexpected TypraRuntimeError from load: \(error)") + } catch { + return .failed("unexpected error from load: \(error)") + } + + do { + return .loaded(try loaded.save()) + } catch { + return .failed("loaded, then save() threw: \(error)") + } + } + + // MARK: - Vector shape + + private struct Probe { + let name: String + let input: Any + let expectedKind: String + let expectedExample: Any + } + + /// Validate the fixture itself before trusting it. + /// + /// Every expectation here is one the vector must *declare*. A shared file can + /// be emptied or reshaped upstream, and an expectation that silently + /// evaporates is worse than one that was never written: the suite keeps + /// reporting success over an assertion that no longer exists. + private static func validateVectorShape(_ document: [String: Any]) throws -> [Probe] { + guard let vectors = document["vectors"] as? [[String: Any]] else { + throw VectorFailure("vector file has no `vectors` array") + } + guard vectors.count == 1 else { + throw VectorFailure( + "expected exactly one atomic vector, found \(vectors.count) — the " + + "contract is a single all-or-nothing group") + } + + let vector = vectors[0] + guard let operation = vector["operation"] as? String, operation == "load" else { + throw VectorFailure( + "expected operation `load`, found \(Spec.describe(vector["operation"]))") + } + guard let rawCases = vector["cases"] as? [[String: Any]] else { + throw VectorFailure("vector declares no `cases` array") + } + guard rawCases.count == requiredCaseNames.count else { + throw VectorFailure( + "expected exactly \(requiredCaseNames.count) cases, found \(rawCases.count) — " + + "the contract is a fixed four-case group") + } + + // Read names strictly. `compactMap` would silently drop an unnamed case, + // so a fifth entry could hide behind four correct names. + let names = try rawCases.map { rawCase -> String in + guard let name = rawCase["name"] as? String else { + throw VectorFailure("a case declares no `name`") + } + return name + } + guard names == requiredCaseNames else { + throw VectorFailure( + "vector must declare exactly \(requiredCaseNames) in order, found " + + "\(names) — a dropped or reordered case would quietly narrow the " + + "contract this suite asserts") + } + + return try rawCases.map { rawCase in + let name = rawCase["name"] as? String ?? "" + guard let input = rawCase["input"] else { + throw VectorFailure("\(name): case declares no `input`") + } + guard let expected = rawCase["expected"] as? [String: Any] else { + throw VectorFailure("\(name): case declares no `expected` object") + } + guard let kind = expected["kind"] as? String else { + throw VectorFailure("\(name): expected block declares no `kind`") + } + guard kind == name else { + throw VectorFailure( + "\(name): expected kind is `\(kind)`; each case is named for the kind " + + "it pins, so a mismatch means the fixture drifted") + } + guard let example = expected["example"], !(example is NSNull) else { + throw VectorFailure( + "\(name): expected block declares no `example` — without it the case " + + "would assert the kind while ignoring the stored scalar") + } + return Probe(name: name, input: input, expectedKind: kind, expectedExample: example) + } + } + + // MARK: - Helpers + + /// The `@typra/emitter` version `schema/package.json` pins. + /// + /// Read at run time so the blocked-baseline skip cannot outlive the pin it + /// describes. Returns `nil` when the file is unreadable or reshaped, which + /// the caller treats as "not the documented pin". + private static func pinnedEmitterVersion() -> String? { + let url = + Spec.root + .deletingLastPathComponent() + .appendingPathComponent("schema") + .appendingPathComponent("package.json") + guard let data = try? Data(contentsOf: url), + let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let dependencies = object["dependencies"] as? [String: Any], + let version = dependencies["@typra/emitter"] as? String + else { + return nil + } + return version + } + + /// Render a value as JSON text, type faithfully. + /// + /// Wrapped in an object because a bare scalar is not a valid top-level + /// `JSONSerialization` payload on every platform. + private static func jsonText(_ value: Any) throws -> String { + let data = try JSONSerialization.data(withJSONObject: ["v": value], options: [.sortedKeys]) + return String(decoding: data, as: UTF8.self) + } + + private static func describeOutcomes(_ cases: [Probe], _ outcomes: [Outcome]) -> String { + zip(cases, outcomes) + .map { probe, outcome in + switch outcome { + case .loaded: return "\(probe.name)=loaded" + case .blocked: return "\(probe.name)=blocked" + case .failed(let detail): return "\(probe.name)=failed(\(detail))" + } + } + .joined(separator: ", ") + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/ReadmeSnippetTests.swift b/runtime/swift/prompty/Tests/PromptyTests/ReadmeSnippetTests.swift new file mode 100644 index 000000000..0dd50bbde --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/ReadmeSnippetTests.swift @@ -0,0 +1,53 @@ +import Foundation + +import Prompty + +import PromptyModel + +import PromptyOpenAI + +/// Compiles the snippets in `runtime/swift/README.md`. +/// +/// The README claimed a `Agent.load(path:)` entry point that never existed. +/// Documented calls are API surface, so they are type-checked here rather than +/// trusted. Nothing is executed — a compile is the whole assertion. +import XCTest + +final class ReadmeSnippetTests: XCTestCase { + + func testReadmeSnippetsCompile() throws { + func quickStart() async throws -> Any? { + Registry.shared.registerDefaults() + registerOpenAI() + + return try await Pipeline.invoke( + path: "basic.prompty", + inputs: ["question": "What is the capital of Iceland?"] + ) + } + + func stages(inputs: [String: Any]) async throws -> Any? { + let agent = try Loader.load(path: "basic.prompty") + let messages = try await Pipeline.prepare(agent, inputs: inputs) + let raw = try await Pipeline.run(agent, messages: messages) + return raw + } + + func toolLoop( + agent: Agent, raw: Any?, inputs: [String: Any], + myTools: [String: ([String: Any]) throws -> String] + ) throws -> [String] { + var results: [String] = [] + for call in Pipeline.toolCalls(in: raw) { + let args = Pipeline.boundArguments(agent, call: call, inputs: inputs) + let result = try myTools[call.name]!(args) + results.append(result) + } + return results + } + + XCTAssertNotNil(quickStart) + XCTAssertNotNil(stages) + XCTAssertNotNil(toolLoop) + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/RegressionTests.swift b/runtime/swift/prompty/Tests/PromptyTests/RegressionTests.swift new file mode 100644 index 000000000..88f87e126 --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/RegressionTests.swift @@ -0,0 +1,338 @@ +import Foundation + +import PromptyModel + +import XCTest + +/// Guards for defects found in review that the spec vectors do not cover. +/// +/// Each test names the behavior it protects, so a future regression reads as a +/// specific broken promise rather than an anonymous assertion failure. +@testable import Prompty + +/// A parser whose only job is to return a chosen `preRender` result. +final class RegressionTests: XCTestCase { + + // MARK: - Wildcard template kinds + + /// With no `template` block both halves resolve to their defaults; a present + /// but half-specified `template` object is rejected. + /// + /// `template` is optional on `Agent`, so omitting it leaves `formatKind` / + /// `parserKind` to fall back to the runtime defaults (the generated model + /// fills an unset `kind` with the schema wildcard `"*"`, which is never a + /// registry key and must resolve to the default rather than fail lookup). + /// + /// But when a `template` object IS present it must be complete: `format` and + /// `parser` are both required. The `template_string_invalid` load vector pins + /// this — "Template as a bare string is not valid v2 — must be an object with + /// format/parser" — so the generated loader throws on a half-specified object, + /// matching every other runtime's strict `validate_input_at` contract (Rust + /// `Template::from_json` errors identically). This is the cross-runtime + /// strict-complex contract, not a Swift quirk to paper over in the runtime. + func testTemplateDefaultsAndPartialRejection() throws { + let noTemplate = try Agent.load(["kind": "prompt", "name": "bare"]) + XCTAssertEqual(noTemplate.formatKind, Defaults.templateFormat) + XCTAssertEqual(noTemplate.parserKind, Defaults.parser) + + // format present, parser absent -> the required `parser` is missing. + XCTAssertThrowsError( + try Agent.load([ + "kind": "prompt", "name": "format-only", + "template": ["format": ["kind": "mustache"]], + ]) + ) { error in + guard case TypraRuntimeError.unsupported(let message) = error else { + return XCTFail("expected a missing-required-field error, got \(error)") + } + XCTAssertTrue( + message.contains("parser") && message.contains("missing required field"), + "unexpected error message: \(message)") + } + + // parser present, format absent -> the required `format` is missing. + XCTAssertThrowsError( + try Agent.load([ + "kind": "prompt", "name": "parser-only", + "template": ["parser": ["kind": "prompty"]], + ]) + ) { error in + guard case TypraRuntimeError.unsupported(let message) = error else { + return XCTFail("expected a missing-required-field error, got \(error)") + } + XCTAssertTrue( + message.contains("format") && message.contains("missing required field"), + "unexpected error message: \(message)") + } + } + + /// An explicitly empty kind is as unset as a missing one. + func testEmptyTemplateKindFallsBackToDefaults() throws { + let agent = try Agent.load([ + "kind": "prompt", + "name": "empty-kinds", + "template": ["format": ["kind": ""], "parser": ["kind": ""]], + ]) + XCTAssertEqual(agent.formatKind, Defaults.templateFormat) + XCTAssertEqual(agent.parserKind, Defaults.parser) + } + + // MARK: - Nonce ownership + + /// `prepare` must expand a thread input into real messages. + /// + /// Nonce substitution has to happen exactly once. When both the pipeline and + /// the renderer substituted, the renderer minted a second nonce, the pipeline + /// searched the output for its own now-absent first one, and every thread, + /// image, file, and audio input was silently dropped. + func testPrepareExpandsThreadInputs() async throws { + let agent = try Agent.load([ + "kind": "prompt", + "name": "threaded", + "instructions": "system:\nYou are helpful.\n\n{{history}}\n\nuser:\n{{question}}", + "inputs": [ + ["name": "history", "kind": "thread"], + ["name": "question", "kind": "string"], + ], + ]) + + let messages = try await Pipeline.prepare( + agent, + inputs: [ + "history": [ + ["role": "user", "content": "first question"], + ["role": "assistant", "content": "first answer"], + ], + "question": "second question", + ] + ) + + let roles = messages.map { $0.role.rawValue } + XCTAssertEqual(roles, ["system", "user", "assistant", "user"]) + + let texts = messages.map { Self.text($0.parts) } + XCTAssertEqual(texts[1], "first question") + XCTAssertEqual(texts[2], "first answer") + XCTAssertEqual(texts[3], "second question") + } + + /// The nonce must not survive into the messages handed to a provider. + func testPrepareLeavesNoNonceResidue() async throws { + let agent = try Agent.load([ + "kind": "prompt", + "name": "threaded-residue", + "instructions": "system:\nBe brief.\n\n{{history}}\n\nuser:\nhi", + "inputs": [["name": "history", "kind": "thread"]], + ]) + + let messages = try await Pipeline.prepare( + agent, + inputs: ["history": [["role": "user", "content": "prior"]]] + ) + + for message in messages { + XCTAssertFalse( + Self.text(message.parts).contains(Defaults.threadNoncePrefix), + "a nonce marker leaked into a prepared message") + } + } + + /// Concatenated text of a message's parts. + private static func text(_ parts: [ContentPart]) -> String { + parts.compactMap { part -> String? in + if case .textPart(let text) = part { return text.value } + return nil + }.joined() + } + + // MARK: - Registry publication + + /// Concurrent first use must never observe a half-installed registry. + /// + /// `registerDefaults` used to set its "already done" flag and release the + /// lock before installing the tables, so a racing caller could see the flag, + /// skip registration, and then fail to resolve a renderer that was not there + /// yet. + func testConcurrentRegisterDefaultsAlwaysResolves() async throws { + let registry = Registry() + + try await withThrowingTaskGroup(of: Void.self) { group in + for _ in 0..<64 { + group.addTask { + registry.registerDefaults() + _ = try registry.renderer(for: Defaults.templateFormat) + _ = try registry.parser(for: Defaults.parser) + } + } + try await group.waitForAll() + } + } + + // MARK: - Expression evaluation + + /// Jinja expressions have to be evaluated, not pattern-matched. + /// + /// The previous renderer recognized only a handful of shapes and resolved + /// everything else to nothing, so a wrong condition rendered as an empty + /// string instead of failing. + func testRendererEvaluatesExpressions() async throws { + let cases: [(String, [String: Any], String)] = [ + ("{% if count > 2 %}many{% else %}few{% endif %}", ["count": 5], "many"), + ("{% if count > 2 %}many{% else %}few{% endif %}", ["count": 1], "few"), + ("{% if a and b %}both{% endif %}", ["a": true, "b": true], "both"), + ("{% if a and b %}both{% endif %}", ["a": true, "b": false], ""), + ("{% if not flag %}off{% endif %}", ["flag": false], "off"), + ("{% if 'x' in items %}found{% endif %}", ["items": ["w", "x"]], "found"), + ("{% if 'z' in items %}found{% endif %}", ["items": ["w", "x"]], ""), + ("{{ a + b }}", ["a": 2, "b": 3], "5"), + ("{{ name is defined }}", ["name": "jane"], "true"), + ("{{ missing is not defined }}", [:], "true"), + ("{{ items[1] }}", ["items": ["a", "b", "c"]], "b"), + ("{{ user.name }}", ["user": ["name": "jane"]], "jane"), + ] + + var run = VectorRun(stage: "expression") + for (template, inputs, expected) in cases { + run.started() + let agent = try Agent.load([ + "kind": "prompt", "name": "expr", "instructions": template, + ]) + do { + let rendered = try await Pipeline.render(agent, inputs: inputs) + if rendered != expected { + run.fail( + template, "expected \(expected.debugDescription), got \(rendered.debugDescription)") + } + } catch { + run.fail(template, "\(error)") + } + } + run.assertClean() + } + + /// Syntax the runtime does not implement must be reported, not ignored. + func testRendererRejectsUnsupportedExpressions() async throws { + let agent = try Agent.load([ + "kind": "prompt", + "name": "unsupported", + "instructions": "{{ value ** }}", + ]) + + do { + let rendered = try await Pipeline.render(agent, inputs: ["value": 2]) + XCTFail("expected a parse failure, rendered \(rendered.debugDescription)") + } catch { + // Expected: an unparseable expression is an error, not an empty string. + } + } + + // MARK: - Strict pre-render + + /// Strict mode must refuse a pre-render result it cannot understand. + /// + /// Accepting it as "no context" would quietly turn strict validation off, + /// which is the one thing strict mode is meant to guarantee. + func testStrictModeRejectsUnknownPreRenderResult() async throws { + let registry = Registry() + registry.registerDefaults() + registry.register(parser: StubParser(preRenderResult: "not a PreRenderResult"), for: "stub") + + let agent = try Agent.load([ + "kind": "prompt", + "name": "strict-stub", + "instructions": "system:\nhi", + "template": ["format": ["kind": "jinja2"], "parser": ["kind": "stub"]], + ]) + + do { + _ = try await Pipeline.prepare(agent, inputs: [:], registry: registry) + XCTFail("expected strict mode to reject an unsupported pre-render result") + } catch { + // `InvokerError` is also a generated model type, so the error is matched + // by message rather than by a name that is ambiguous in test scope. + XCTAssertTrue("\(error)".contains("pre-render"), "unexpected message: \(error)") + } + } + + /// A parser that opts out of pre-render is still valid. + func testStrictModeAcceptsNilPreRenderResult() async throws { + let registry = Registry() + registry.registerDefaults() + registry.register(parser: StubParser(preRenderResult: nil), for: "stub-nil") + + let agent = try Agent.load([ + "kind": "prompt", + "name": "strict-nil", + "instructions": "system:\nhi", + "template": ["format": ["kind": "jinja2"], "parser": ["kind": "stub-nil"]], + ]) + + let messages = try await Pipeline.prepare(agent, inputs: [:], registry: registry) + XCTAssertEqual(messages.count, 1) + } + + // MARK: - Structured output + + /// A provider that already returned JSON text must not be re-encoded. + /// + /// Serializing an existing JSON string produced a JSON string *literal*, so + /// decoding into the declared shape failed on every structured response. + func testStructuredCastAcceptsJSONText() throws { + struct Answer: Decodable, Equatable { + let city: String + let population: Int + } + + let decoded = try Structured.cast( + #"{"city":"Seattle","population":749256}"#, as: Answer.self) + XCTAssertEqual(decoded, Answer(city: "Seattle", population: 749_256)) + } + + /// Casting from an already-structured value keeps working. + func testStructuredCastAcceptsDictionary() throws { + struct Answer: Decodable, Equatable { + let city: String + let population: Int + } + + let decoded = try Structured.cast( + ["city": "Seattle", "population": 749_256] as [String: Any], as: Answer.self) + XCTAssertEqual(decoded, Answer(city: "Seattle", population: 749_256)) + } + + // MARK: - Streaming dispatch + + /// `run` has to notice the streaming option. + /// + /// Routing a streaming request through the buffered path hands raw SSE + /// frames to a JSON decoder. + func testIsStreamingReadsModelOptions() throws { + let streaming = try Agent.load([ + "kind": "prompt", + "name": "streams", + "model": ["id": "gpt-4o-mini", "options": ["additionalProperties": ["stream": true]]], + ]) + XCTAssertTrue(Pipeline.isStreaming(streaming)) + + let buffered = try Agent.load([ + "kind": "prompt", + "name": "buffered", + "model": ["id": "gpt-4o-mini", "options": ["additionalProperties": ["stream": false]]], + ]) + XCTAssertFalse(Pipeline.isStreaming(buffered)) + + let unset = try Agent.load([ + "kind": "prompt", "name": "unset", "model": ["id": "gpt-4o-mini"], + ]) + XCTAssertFalse(Pipeline.isStreaming(unset)) + } +} +private struct StubParser: Parser { + let preRenderResult: Any? + + func preRender(template: String) throws -> Any? { preRenderResult } + + func parse(agent: Agent, rendered: String, context: [String: Any]?) async throws -> [Message] { + [Message.user(text: rendered)] + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/RenderVectorTests.swift b/runtime/swift/prompty/Tests/PromptyTests/RenderVectorTests.swift new file mode 100644 index 000000000..9ac57b4f3 --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/RenderVectorTests.swift @@ -0,0 +1,86 @@ +import Foundation + +import PromptyModel + +import XCTest + +/// Conformance against `spec/vectors/render/render_vectors.json`. +/// +/// Exercises both template engines plus the thread-nonce substitution that +/// keeps rich inputs out of the rendered text. +@testable import Prompty + +final class RenderVectorTests: XCTestCase { + + func testRenderVectors() async throws { + Registry.shared.registerDefaults() + var run = VectorRun(stage: "render") + + for vector in try Spec.vectors("render") { + let name = vector["name"] as? String ?? "" + run.started() + let input = vector["input"] as? [String: Any] ?? [:] + let expected = vector["expected"] as? [String: Any] ?? [:] + + let template = input["template"] as? String ?? "" + let engine = input["engine"] as? String ?? Defaults.templateFormat + let inputs = input["inputs"] as? [String: Any] ?? [:] + + do { + let agent = try Self.agent(name: name, template: template, engine: engine, inputs: inputs) + let rendered = try await Pipeline.render(agent, inputs: inputs) + + if let expectedText = expected["rendered"] as? String { + guard rendered == expectedText else { + run.fail( + name, + "rendered mismatch:\n actual: \(Spec.describe(rendered))\n expected: \(Spec.describe(expectedText))" + ) + continue + } + } + + if let pattern = expected["nonce_pattern"] as? String { + let regex = try NSRegularExpression(pattern: pattern) + let range = NSRange(rendered.startIndex..., in: rendered) + guard regex.firstMatch(in: rendered, range: range) != nil else { + run.fail( + name, "rendered text does not match \(pattern):\n actual: \(Spec.describe(rendered))" + ) + continue + } + } + } catch { + run.fail(name, "\(error)") + } + } + + run.assertClean() + } + + /// Build the synthetic agent a render vector implies. + /// + /// Rich-kind substitution is driven by the *declared* input property, so any + /// input the vector marks `_kind: thread` must appear in `inputs` as a + /// thread-kind property. + private static func agent( + name: String, template: String, engine: String, inputs: [String: Any] + ) throws -> Agent { + var properties: [[String: Any]] = [] + for (key, value) in inputs.sorted(by: { $0.key < $1.key }) { + let kind = (value as? [String: Any])?["_kind"] as? String ?? "string" + properties.append(["name": key, "kind": kind]) + } + + return try Agent.load([ + "kind": "prompt", + "name": name, + "instructions": template, + "inputs": properties, + "template": [ + "format": ["kind": engine], + "parser": ["kind": Defaults.parser], + ], + ]) + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/ReplayVectorTests.swift b/runtime/swift/prompty/Tests/PromptyTests/ReplayVectorTests.swift new file mode 100644 index 000000000..e35f521dd --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/ReplayVectorTests.swift @@ -0,0 +1,223 @@ +import Foundation + +import PromptyModel + +import XCTest + +/// Conformance against `spec/vectors/harness/replay_vectors.json`. +/// +/// Runs the real `ReferenceTurnRunner` against a real on-disk JSONL journal, +/// then normalizes that journal to the shared string form and compares it to +/// the golden sequence. Because the journal is read back from disk rather than +/// from an in-memory sink, this exercises durability as well as ordering. +@testable import Prompty + +/// Deterministic monotonic counter for id generation. +final class ReplayVectorTests: XCTestCase { + + func testReplayVectors() async throws { + var run = VectorRun(stage: "replay") + + let file = try Spec.vectorObject("harness/replay_vectors.json") + XCTAssertEqual(file["version"] as? Int, 1, "unexpected replay vector version") + + let sessionId = file["sessionId"] as? String ?? "session-1" + let turnId = file["turnId"] as? String ?? "turn-1" + let clock = file["clock"] as? String ?? "2026-06-28T00:00:00Z" + + for scenario in file["scenarios"] as? [[String: Any]] ?? [] { + let name = scenario["name"] as? String ?? "" + + await run.checkAsync(name) { + let actual = try await Self.runScenario( + scenario, sessionId: sessionId, turnId: turnId, clock: clock) + let expected = scenario["expected"] as? [String] ?? [] + try expect(!expected.isEmpty, "vector '\(name)' declares no expected journal") + + try expectEqual(actual, expected, "journal for '\(name)'") + + // The same sequence must also verify through the shipped verifier, so + // a runtime can prove replay equivalence without string comparison. + try Self.verifyThroughVerifier(expected: expected, actual: actual) + } + } + + run.assertClean() + } + + // MARK: - Scenario execution + + private static func runScenario( + _ scenario: [String: Any], sessionId: String, turnId: String, clock: String + ) async throws -> [String] { + let name = scenario["name"] as? String ?? "" + let journalPath = NSTemporaryDirectory() + "prompty-replay-\(name)-\(UUID().uuidString).jsonl" + defer { try? FileManager.default.removeItem(atPath: journalPath) } + + let journal = JsonlEventJournalWriter(path: journalPath) + + // Ids must be deterministic for the journal to be byte-comparable. + let counter = Counter() + let runner = ReferenceTurnRunner( + eventSink: CollectingEventSink(), + journal: journal, + checkpointStore: InMemoryCheckpointStore(), + permissionResolver: name == "permission_denied" + ? DenyAllPermissionResolver() : AllowAllPermissionResolver(), + hostToolExecutor: Self.toolExecutor(), + invokeModel: Self.model(for: name), + now: { clock }, + nextId: { prefix in "\(prefix)-\(counter.next())" } + ) + + var request = RunTurnRequest(sessionId: sessionId, turnId: turnId) + request.inputs = scenario["inputs"] as? [String: Any] ?? ["name": "Ada"] + if let maxIterations = scenario["maxIterations"] as? Int { + var options = TurnOptions() + options.maxIterations = Int32(maxIterations) + request.options = options + } + + _ = try await runner.run(request) + + return normalize(try JsonlEventJournalWriter.readRecords(path: journalPath)) + } + + /// Mirrors `model_for_scenario` in the Rust reference runner. + private static func model(for scenario: String) -> ReferenceTurnRunner.ModelCallback { + { request in + if scenario == "no_tool" { + var response = TurnModelResponse() + let name = (request.inputs?["name"] as? String) ?? "" + response.output = ["text": "hello \(name)"] + response.checkpointState = ["stable": true] + return response + } + + if request.iteration == 0 { + var toolRequest = HostToolRequest( + toolName: scenario == "tool_failure" ? "fail" : "add") + toolRequest.requestId = "exec-1" + toolRequest.toolCallId = "call-1" + toolRequest.arguments = ["a": 2, "b": 3] + + var response = TurnModelResponse() + response.toolRequests = [toolRequest] + return response + } + + var response = TurnModelResponse() + let first = request.toolResults?.first + response.output = [ + "toolResult": first?.result as Any, + "errorKind": first?.errorKind as Any, + ] + return response + } + } + + private static func toolExecutor() -> FunctionHostToolExecutor { + FunctionHostToolExecutor(handlers: [ + "add": { arguments in + let a = (arguments["a"] as? Int) ?? 0 + let b = (arguments["b"] as? Int) ?? 0 + return ["sum": a + b] + }, + "fail": { _ in + throw InvokerError.execution("tool unavailable") + }, + ]) + } + + // MARK: - Journal normalization + + /// Mirrors `normalize_journal` in the Rust reference runner so both runtimes + /// compare against the same golden strings. + private static func normalize(_ records: [[String: Any]]) -> [String] { + records.map { record in + let kind = record["kind"] as? String ?? "" + + if kind == "summary" { + let summary = record["summary"] as? [String: Any] ?? [:] + return [ + "summary", + summary["sessionId"] as? String ?? "", + summary["status"] as? String ?? "", + "turns=\(summary["turns"] ?? 0)", + "checkpoints=\(summary["checkpoints"] ?? 0)", + ].joined(separator: ":") + } + + let event = record["event"] as? [String: Any] ?? [:] + let type = event["type"] as? String ?? "" + let payload = event["payload"] as? [String: Any] ?? [:] + + if kind == "session" { + var parts = [ + "session", type, event["sessionId"] as? String ?? "", + event["turnId"] as? String ?? "", + ] + if type == "session_end" { parts.append(payload["status"] as? String ?? "") } + return parts.joined(separator: ":") + } + + let iteration = "\(event["iteration"] ?? 0)" + var parts = ["turn", type, iteration] + + switch type { + case "permission_requested": + parts.append(payload["requestId"] as? String ?? "") + case "permission_completed": + parts.append("\(payload["approved"] as? Bool ?? false)") + case "tool_execution_start": + parts.append(payload["toolName"] as? String ?? "") + case "tool_execution_complete", "tool_result": + parts.append(payload["toolName"] as? String ?? "") + parts.append("\(payload["success"] as? Bool ?? false)") + if let errorKind = payload["errorKind"] as? String { parts.append(errorKind) } + case "error": + parts.append(payload["errorKind"] as? String ?? "") + case "turn_end": + parts.append(payload["status"] as? String ?? "") + default: + break + } + return parts.joined(separator: ":") + } + } + + // MARK: - Verifier cross-check + + private static func verifyThroughVerifier(expected: [String], actual: [String]) throws { + let request = ReplayVerificationRequest( + expected: expected.map(record), actual: actual.map(record)) + let result = try ReferenceReplayVerifier().verify(request) + + try expect( + result.status == .passed, + "replay verifier reported \(result.status.rawValue): \(result.mismatches ?? [])") + try expectEqual(Int(result.expectedCount), expected.count, "verifier expectedCount") + try expectEqual(Int(result.actualCount), actual.count, "verifier actualCount") + } + + /// Project a normalized string back onto a journal record so the verifier + /// compares the same information the golden vectors pin down. + private static func record(_ normalized: String) -> ReplayJournalRecord { + var record = ReplayJournalRecord() + let parts = normalized.split(separator: ":", maxSplits: 1).map(String.init) + record.kind = (try? ReplayRecordKind.parse(parts.first ?? "session")) ?? .session + record.type = normalized + return record + } +} +private final class Counter: @unchecked Sendable { + private let lock = NSLock() + private var value = 0 + + func next() -> Int { + lock.lock() + defer { lock.unlock() } + value += 1 + return value + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/SpecVectors.swift b/runtime/swift/prompty/Tests/PromptyTests/SpecVectors.swift new file mode 100644 index 000000000..d39b7328e --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/SpecVectors.swift @@ -0,0 +1,262 @@ +import Foundation + +import XCTest + +/// Shared spec-vector plumbing. +/// +/// Every Agent runtime is validated against the same JSON vectors under +/// `spec/vectors/`, so conformance is comparable across languages. This file +/// locates that directory relative to the test source and provides the +/// order-insensitive JSON comparison the vectors are written against. +@testable import Prompty + +/// Collects per-vector failures so one run reports every mismatch at once. +/// +/// Failing fast on the first vector hides how much of a stage is broken, which +/// is exactly the signal a conformance suite should give. + +/// A vector assertion failed. + +/// Assert a condition inside a vector body. + +/// Assert deep JSON equality inside a vector body. +enum Spec { + + /// The repository's `spec/` directory. + /// + /// Resolved from `#filePath` rather than the working directory so the tests + /// run identically from an IDE, `swift test`, and CI. + static let repoRoot: URL = { + // .../runtime/swift/prompty/Tests/PromptyTests/SpecVectors.swift + var url = URL(fileURLWithPath: #filePath) + for _ in 0..<6 { url = url.deletingLastPathComponent() } + return url + }() + + static let root: URL = repoRoot.appendingPathComponent("spec") + + static var fixtures: URL { root.appendingPathComponent("fixtures") } + + /// The single generated conformance-vector file — the cross-runtime source of + /// truth every language suite is validated against. + static let generatedVectorsURL: URL = + repoRoot + .appendingPathComponent("schema") + .appendingPathComponent("tsp-output") + .appendingPathComponent(".typra-generated") + .appendingPathComponent("vectors.json") + + /// Every entry's inner `vector` object from the generated file. + private static func allVectors() throws -> [[String: Any]] { + let data = try Data(contentsOf: generatedVectorsURL) + guard let doc = try JSONSerialization.jsonObject(with: data) as? [String: Any], + let entries = doc["vectors"] as? [[String: Any]] + else { + throw SpecError.malformed("\(generatedVectorsURL.path) has no 'vectors' array") + } + return entries.compactMap { $0["vector"] as? [String: Any] } + } + + /// Read one stage's vectors from the generated single source of truth. + /// + /// Vectors are filtered by their inner `stage` field, which matches the stage + /// names the per-language suites request (`load`, `parse`, `render`, + /// `process`, `agent`, `wire`, `replay`). + static func vectors(_ stage: String, file: String? = nil) throws -> [[String: Any]] { + try allVectors().filter { ($0["stage"] as? String) == stage } + } + + /// Read a vector document whose root is an object rather than an array. + /// + /// The only such document consumed on `main` is the replay harness fixture, + /// which the generator emits as per-scenario entries under the `replay` + /// stage; it is reconstructed here into the object shape the replay suite + /// expects, mirroring the Rust reference harness. Any other path falls back + /// to a literal file under `spec/vectors/` (feature-branch fixtures). + static func vectorObject(_ relativePath: String) throws -> [String: Any] { + if relativePath == "harness/replay_vectors.json" { + return try reconstructReplayObject() + } + + var url = root.appendingPathComponent("vectors") + for component in relativePath.split(separator: "/") { + url = url.appendingPathComponent(String(component)) + } + + let data = try Data(contentsOf: url) + guard let object = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { + throw SpecError.malformed("\(url.path) is not a JSON object") + } + return object + } + + /// Rebuild the replay harness object from the generated `replay` vectors. + /// + /// The generator emits each scenario as its own entry carrying the + /// journal-level `clock`/`sessionId`/`turnId` plus per-scenario `inputs` and + /// `maxIterations`; the shared object form hoists the journal fields to the + /// top and nests the scenarios, exactly as the Rust harness reconstructs it. + private static func reconstructReplayObject() throws -> [String: Any] { + let items = try allVectors().filter { ($0["stage"] as? String) == "replay" } + guard let first = items.first, let firstInput = first["input"] as? [String: Any] else { + throw SpecError.malformed("no replay vectors found in \(generatedVectorsURL.path)") + } + + let scenarios: [[String: Any]] = items.map { vector in + let input = vector["input"] as? [String: Any] ?? [:] + var scenario: [String: Any] = [ + "name": vector["name"] as? String ?? "", + "expected": vector["expected"] as? [String] ?? [], + ] + if let inputs = input["inputs"] { scenario["inputs"] = inputs } + if let maxIterations = input["maxIterations"] { scenario["maxIterations"] = maxIterations } + return scenario + } + + return [ + "version": 1, + "clock": firstInput["clock"] as Any, + "sessionId": firstInput["sessionId"] as Any, + "turnId": firstInput["turnId"] as Any, + "scenarios": scenarios, + ] + } + + enum SpecError: Error, CustomStringConvertible { + case malformed(String) + var description: String { + switch self { + case .malformed(let detail): return detail + } + } + } + + // MARK: - Comparison + + /// Deep JSON equality that ignores object key order and treats numerically + /// equal integers and doubles as equal. + static func equal(_ actual: Any?, _ expected: Any?) -> Bool { + switch (normalize(actual), normalize(expected)) { + case (nil, nil): + return true + + case (let a as [String: Any], let b as [String: Any]): + guard a.count == b.count else { return false } + return a.allSatisfy { key, value in + b.keys.contains(key) && equal(value, b[key]) + } + + case (let a as [Any], let b as [Any]): + guard a.count == b.count else { return false } + return zip(a, b).allSatisfy { equal($0, $1) } + + case (let a as String, let b as String): + return a == b + + case (let a as Bool, let b as Bool): + return a == b + + case (let a as NSNumber, let b as NSNumber): + // Model options are 32-bit floats in the generated model, so a vector's + // 0.7 arrives as 0.699999988. Compare with a float-scale tolerance. + let scale = max(abs(a.doubleValue), abs(b.doubleValue), 1.0) + return abs(a.doubleValue - b.doubleValue) <= scale * 1e-6 + + default: + return false + } + } + + /// Strip `NSNull` so a JSON null and an absent Swift value compare equal. + private static func normalize(_ value: Any?) -> Any? { + guard let value, !(value is NSNull) else { return nil } + return value + } + + /// A readable rendering of a value for assertion messages. + static func describe(_ value: Any?) -> String { + guard let value, !(value is NSNull) else { return "null" } + if JSONSerialization.isValidJSONObject(value), + let data = try? JSONSerialization.data( + withJSONObject: value, options: [.sortedKeys, .prettyPrinted]), + let text = String(data: data, encoding: .utf8) + { + return text + } + return String(describing: value) + } +} +struct VectorRun { + let stage: String + private(set) var failures: [String] = [] + private(set) var ran = 0 + private(set) var skipped = 0 + + init(stage: String) { self.stage = stage } + + /// Run one vector, recording any thrown error or assertion as a failure. + mutating func check(_ name: String, _ body: () throws -> Void) { + ran += 1 + do { + try body() + } catch { + failures.append("[\(name)] \(error)") + } + } + + /// Async counterpart of ``check(_:_:)`` for stages that await. + mutating func checkAsync(_ name: String, _ body: () async throws -> Void) async { + ran += 1 + do { + try await body() + } catch { + failures.append("[\(name)] \(error)") + } + } + + mutating func skip() { skipped += 1 } + + /// Record that a vector is about to run. + /// + /// Suites that handle their own errors call this so the run still knows how + /// much was actually exercised — otherwise a suite that silently matched + /// zero vectors would be indistinguishable from a passing one. + mutating func started() { ran += 1 } + + mutating func fail(_ name: String, _ message: String) { + failures.append("[\(name)] \(message)") + } + + /// Assert every vector passed. + /// + /// A run that executed nothing is treated as a failure: a conformance suite + /// that silently matches zero vectors is indistinguishable from a passing + /// one, and that is the most dangerous way for this harness to break. + func assertClean(file: StaticString = #filePath, line: UInt = #line) { + if ran == 0 && skipped == 0 { + XCTFail( + "no \(stage) vectors ran — the vector file was empty or misread", file: file, line: line) + return + } + guard !failures.isEmpty else { return } + XCTFail( + "\(failures.count)/\(ran) \(stage) vectors failed:\n\n" + failures.joined(separator: "\n\n"), + file: file, + line: line + ) + } +} +struct VectorFailure: Error, CustomStringConvertible { + let description: String + init(_ message: String) { description = message } +} +func expect(_ condition: Bool, _ message: @autoclosure () -> String) throws { + guard condition else { throw VectorFailure(message()) } +} +func expectEqual(_ actual: Any?, _ expected: Any?, _ label: String) throws { + guard Spec.equal(actual, expected) else { + throw VectorFailure( + "\(label) mismatch:\n actual: \(Spec.describe(actual))\n expected: \(Spec.describe(expected))" + ) + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/ToolBindingTests.swift b/runtime/swift/prompty/Tests/PromptyTests/ToolBindingTests.swift new file mode 100644 index 000000000..ec755efbc --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/ToolBindingTests.swift @@ -0,0 +1,438 @@ +import Foundation + +import PromptyModel + +import XCTest + +/// Tool binding conformance — the *executable* half of the bindings contract. +/// +/// Bindings are two coupled rules, and testing either alone proves nothing: +/// +/// §2.9.1.1 bound parameters are **stripped** from the schema sent to the model +/// §2.9.1.2 bound parameters are **injected** into the arguments before the tool runs +/// §2.9.1.3 an injected value **overrides** whatever the model produced +/// +/// Strip-without-inject silently drops an argument: the model never sees the +/// parameter, so it never supplies one, and the tool is invoked short. These +/// tests pin both halves together, and pin the load side against both declared +/// binding shapes, so a regression in either surfaces here rather than at a +/// provider call. +/// +/// The injection cases mirror `spec/vectors/agent/agent_vectors.json` +/// (`bindings_injected`) and the reference implementation in +/// `runtime/rust/prompty/src/tool_dispatch.rs::resolve_bindings`. +@testable import Prompty + +final class ToolBindingTests: XCTestCase { + + // MARK: - Fixtures + + /// The canonical `bindings_injected` tool: one bound parameter (`unit`) fed + /// from a parent input (`preferred_unit`), one free parameter (`city`). + private func weatherAgent( + bindings: Any = ["unit": ["input": "preferred_unit"]] + ) throws -> Agent { + try Agent.load([ + "kind": "prompt", + "name": "weather", + "model": ["id": "gpt-4o-mini", "apiType": "chat"], + "tools": [ + [ + "name": "get_weather", + "kind": "function", + "description": "Get the current weather for a city", + "parameters": [ + ["name": "city", "kind": "string", "required": true], + ["name": "unit", "kind": "string", "required": false], + ], + "bindings": bindings, + ] + ], + "instructions": "user:\nWhat is the weather?", + ]) + } + + private func tool(_ agent: Agent) throws -> Tool { + let tool = try XCTUnwrap(agent.tools?.first) + return tool + } + + // MARK: - Load: both declared shapes + + /// Map form — the key supplies the binding name. This is the shape used by + /// `spec/fixtures/tools_function.prompty` and the `bindings_injected` vector. + func testBindingsLoadFromMapForm() throws { + let agent = try weatherAgent(bindings: ["unit": ["input": "preferred_unit"]]) + let bindings = try tool(agent).bindings + + XCTAssertEqual(bindings.count, 1) + XCTAssertEqual(bindings.first?.name, "unit") + XCTAssertEqual(bindings.first?.input, "preferred_unit") + } + + /// List form — the name is already inline. Must normalize identically. + func testBindingsLoadFromListForm() throws { + let agent = try weatherAgent(bindings: [["name": "unit", "input": "preferred_unit"]]) + let bindings = try tool(agent).bindings + + XCTAssertEqual(bindings.count, 1) + XCTAssertEqual(bindings.first?.name, "unit") + XCTAssertEqual(bindings.first?.input, "preferred_unit") + } + + /// Map form with a bare scalar value coerces the scalar into `input`, so the + /// shorthand `unit: preferred_unit` means the same as `unit: {input: ...}`. + func testBindingsLoadFromMapFormScalarShorthand() throws { + let agent = try weatherAgent(bindings: ["unit": "preferred_unit"]) + let bindings = try tool(agent).bindings + + XCTAssertEqual(bindings.count, 1) + XCTAssertEqual(bindings.first?.name, "unit") + XCTAssertEqual(bindings.first?.input, "preferred_unit") + } + + /// Both declared shapes must survive a save/reload cycle unchanged — that is + /// what makes them interchangeable rather than merely both-accepted. + func testBindingsSurviveSaveAndReload() throws { + for declared in [ + ["unit": ["input": "preferred_unit"]] as Any, + [["name": "unit", "input": "preferred_unit"]] as Any, + ["unit": "preferred_unit"] as Any, + ] { + let agent = try weatherAgent(bindings: declared) + let reloaded = try Agent.load(try agent.save()) + let bindings = try tool(reloaded).bindings + + XCTAssertEqual(bindings.count, 1, "declared as \(declared)") + XCTAssertEqual(bindings.first?.name, "unit", "declared as \(declared)") + XCTAssertEqual(bindings.first?.input, "preferred_unit", "declared as \(declared)") + } + } + + // MARK: - Strip half (§2.9.1.1) + + /// A bound parameter must not reach the model. `boundParameterNames` is what + /// wire conversion filters on, so it is the seam worth pinning. + func testBoundParametersAreReportedForStripping() throws { + let agent = try weatherAgent() + XCTAssertEqual(try tool(agent).boundParameterNames, ["unit"]) + } + + // MARK: - Inject half (§2.9.1.2) + + /// The vector case: the model omits the bound parameter, and injection + /// supplies it from the parent inputs. + func testInjectsBoundValueOmittedByModel() throws { + let agent = try weatherAgent() + + let merged = Pipeline.applyBindings( + agent, + toolName: "get_weather", + arguments: ["city": "Paris"], + inputs: ["preferred_unit": "celsius"] + ) + + XCTAssertEqual(merged["city"] as? String, "Paris") + XCTAssertEqual(merged["unit"] as? String, "celsius") + } + + /// §2.9.1.3 — a binding is authoritative. A model that guesses a value for a + /// bound parameter must not be able to override the bound one. + func testBindingOverridesModelSuppliedValue() throws { + let agent = try weatherAgent() + + let merged = Pipeline.applyBindings( + agent, + toolName: "get_weather", + arguments: ["city": "Paris", "unit": "fahrenheit"], + inputs: ["preferred_unit": "celsius"] + ) + + XCTAssertEqual(merged["unit"] as? String, "celsius") + } + + /// Bindings carry whatever the input holds, not just strings — a bound + /// parameter is a value pipe, so non-string inputs must pass through intact. + func testInjectsNonStringInputValues() throws { + for (value, check) in [ + (42 as Any, { (v: Any?) in (v as? Int) == 42 }), + (1.5 as Any, { (v: Any?) in (v as? Double) == 1.5 }), + (true as Any, { (v: Any?) in (v as? Bool) == true }), + (["a", "b"] as Any, { (v: Any?) in (v as? [String]) == ["a", "b"] }), + ] { + let agent = try weatherAgent() + let merged = Pipeline.applyBindings( + agent, + toolName: "get_weather", + arguments: [:], + inputs: ["preferred_unit": value] + ) + XCTAssertTrue(check(merged["unit"]), "binding dropped or coerced \(value)") + } + } + + /// An absent input is skipped rather than injected as null, so a partially + /// supplied input set degrades to the model's own arguments. + func testMissingInputIsSkipped() throws { + let agent = try weatherAgent() + + let merged = Pipeline.applyBindings( + agent, + toolName: "get_weather", + arguments: ["city": "Paris"], + inputs: [:] + ) + + XCTAssertEqual(merged as? [String: String], ["city": "Paris"]) + XCTAssertNil(merged["unit"]) + } + + /// Multiple bindings all resolve independently. + func testMultipleBindingsAllInject() throws { + let agent = try weatherAgent(bindings: [ + "unit": ["input": "preferred_unit"], + "locale": ["input": "user_locale"], + ]) + + let merged = Pipeline.applyBindings( + agent, + toolName: "get_weather", + arguments: ["city": "Paris"], + inputs: ["preferred_unit": "celsius", "user_locale": "fr-FR"] + ) + + XCTAssertEqual(merged["unit"] as? String, "celsius") + XCTAssertEqual(merged["locale"] as? String, "fr-FR") + } + + // MARK: - Pass-through cases + + /// Injection is called on every tool call, so an unbound tool must be cheap + /// and lossless rather than an error. + func testToolWithoutBindingsPassesThrough() throws { + let agent = try weatherAgent(bindings: [] as [Any]) + + let merged = Pipeline.applyBindings( + agent, + toolName: "get_weather", + arguments: ["city": "Paris"], + inputs: ["preferred_unit": "celsius"] + ) + + XCTAssertEqual(merged as? [String: String], ["city": "Paris"]) + } + + /// A tool call naming a tool the prompt never declared passes through, so a + /// host dispatching mixed local and prompt-declared tools is unaffected. + func testUnknownToolPassesThrough() throws { + let agent = try weatherAgent() + + let merged = Pipeline.applyBindings( + agent, + toolName: "not_declared", + arguments: ["city": "Paris"], + inputs: ["preferred_unit": "celsius"] + ) + + XCTAssertEqual(merged as? [String: String], ["city": "Paris"]) + } + + /// A prompt with no tools at all passes through. + func testAgentWithoutToolsPassesThrough() throws { + let agent = try Agent.load([ + "kind": "prompt", "name": "bare", "model": ["id": "gpt-4o-mini"], + "instructions": "user:\nhi", + ]) + + let merged = Pipeline.applyBindings( + agent, toolName: "get_weather", arguments: ["city": "Paris"], inputs: ["x": "y"]) + + XCTAssertEqual(merged as? [String: String], ["city": "Paris"]) + } + + // MARK: - ToolCall convenience + + /// The host-facing entry point decodes the provider's JSON argument string + /// and applies bindings in one step. + func testBoundArgumentsDecodesToolCallJSON() throws { + let agent = try weatherAgent() + let call = ToolCall(id: "call_1", name: "get_weather", arguments: "{\"city\":\"Paris\"}") + + let merged = Pipeline.boundArguments(agent, call: call, inputs: ["preferred_unit": "celsius"]) + + XCTAssertEqual(merged["city"] as? String, "Paris") + XCTAssertEqual(merged["unit"] as? String, "celsius") + } + + /// The free-function surface mirrors the namespaced one. + func testTopLevelBoundArgumentsMatchesPipeline() throws { + let agent = try weatherAgent() + let call = ToolCall(id: "call_1", name: "get_weather", arguments: "{\"city\":\"Paris\"}") + + let merged = boundArguments(agent, call: call, inputs: ["preferred_unit": "celsius"]) + + XCTAssertEqual(merged["unit"] as? String, "celsius") + } + + // MARK: - Shared vector + + /// Execute the canonical `bindings_injected` case from + /// `spec/vectors/agent/agent_vectors.json` rather than restating it. + /// + /// This is the cross-runtime contract Rust already runs. Driving the real + /// vector means a change to the shared expectation reaches Swift instead of + /// silently diverging from a hand-copied duplicate. + func testSharedBindingsInjectedVector() throws { + let vectors = try Spec.vectors("agent") + let vector = try XCTUnwrap( + vectors.first { $0["name"] as? String == "bindings_injected" }, + "bindings_injected vector missing from agent_vectors.json") + + let input = try XCTUnwrap(vector["input"] as? [String: Any]) + let tools = try XCTUnwrap(input["tools"] as? [[String: Any]]) + let parentInputs = try XCTUnwrap(input["parent_inputs"] as? [String: Any]) + + // Build a prompt carrying the vector's own tool declarations. + let agent = try Agent.load([ + "kind": "prompt", + "name": "bindings-vector", + "model": ["id": "gpt-4o-mini", "apiType": "chat"], + "tools": tools, + "instructions": "user:\nWhat is the weather?", + ]) + + let sequence = try XCTUnwrap(vector["sequence"] as? [[String: Any]]) + var assertions = 0 + + for turn in sequence { + guard let calls = turn["expected_tool_calls"] as? [[String: Any]], + let expectedArgs = turn["expected_execution_args"] as? [String: Any] + else { continue } + + for call in calls { + let name = try XCTUnwrap(call["name"] as? String) + // The vector records the arguments the LLM produced; execution args are + // what the tool must actually receive. + let llmArgs = call["arguments"] as? [String: Any] ?? [:] + let expected = try XCTUnwrap(expectedArgs[name] as? [String: Any]) + + let merged = Pipeline.applyBindings( + agent, toolName: name, arguments: llmArgs, inputs: parentInputs) + + XCTAssertTrue( + Spec.equal(merged, expected), + "\(name) execution args: got \(Spec.describe(merged)), expected \(Spec.describe(expected))" + ) + assertions += 1 + } + } + + XCTAssertGreaterThan(assertions, 0, "vector produced no execution-args assertions") + } + + // MARK: - The recorded call is never rewritten + + /// A bound value must not travel back to the model. + /// + /// Bindings hide a parameter on purpose — the spec's own example binds an + /// environment-supplied user id. Injecting into the *recorded* call would put + /// that value into the assistant tool-call history replayed on the next + /// round, handing the model exactly what the binding withheld. + func testRecordedToolCallIsNotRewritten() throws { + let agent = try weatherAgent() + let raw: Any = [["id": "call_1", "name": "get_weather", "arguments": "{\"city\":\"Paris\"}"]] + + let calls = Pipeline.toolCalls(in: raw) + _ = Pipeline.boundArguments(agent, call: calls[0], inputs: ["preferred_unit": "celsius"]) + + XCTAssertEqual(calls[0].arguments, "{\"city\":\"Paris\"}") + XCTAssertNil(calls[0].argumentValues["unit"]) + } + + /// A provider payload that is not an argument object is passed through rather + /// than replaced by one containing only the bound values. + /// + /// The security property under test is that no *fabricated* object reaches + /// the tool: injecting into a payload that was never an argument object would + /// hand the handler a dictionary whose only contents are the bound values, + /// which is the opposite of "the tool receives what the model asked for, plus + /// the binding". + /// + /// Note a known divergence from the Rust reference: `dispatch_tool` + /// (`tool_dispatch.rs` ~L293-309) returns an error string to the model for + /// malformed JSON and preserves valid non-object payloads verbatim. Swift's + /// ``ToolCall/argumentValues`` flattens every non-object to `[:]`, which + /// predates bindings and is the host-dispatch contract the runtime already + /// exposes. Bindings deliberately do not change it — they only decline to + /// inject. Tightening `argumentValues` is tracked separately. + func testNonObjectArgumentsAreNotReplaced() throws { + let agent = try weatherAgent() + + for payload in ["[1,2,3]", "\"just a string\"", "42", "{not json"] { + let call = ToolCall(id: "call_1", name: "get_weather", arguments: payload) + let merged = Pipeline.boundArguments(agent, call: call, inputs: ["preferred_unit": "celsius"]) + + XCTAssertNil(merged["unit"], "bindings were injected into non-object payload \(payload)") + // Identical to the un-bound decode: bindings changed nothing at all. + XCTAssertEqual( + merged.count, call.argumentValues.count, + "payload \(payload) was rewritten rather than passed through") + XCTAssertTrue( + merged.isEmpty, + "payload \(payload) produced a fabricated argument object: \(merged)") + } + } + + /// An empty payload is the no-argument call, which is precisely when every + /// parameter a tool needs may be a bound one. + func testEmptyArgumentsStillReceiveBindings() throws { + let agent = try weatherAgent() + + for payload in ["", " ", "{}"] { + let call = ToolCall(id: "call_1", name: "get_weather", arguments: payload) + let merged = Pipeline.boundArguments(agent, call: call, inputs: ["preferred_unit": "celsius"]) + + XCTAssertEqual(merged["unit"] as? String, "celsius", "payload \(payload)") + } + } + + /// An unnamed binding strips nothing, so it must also inject nothing — + /// otherwise a parameter would vanish from the schema and never come back. + func testUnnamedBindingNeitherStripsNorInjects() throws { + let agent = try weatherAgent(bindings: [["input": "preferred_unit"]]) + + XCTAssertTrue(try tool(agent).boundParameterNames.isEmpty) + + let merged = Pipeline.applyBindings( + agent, toolName: "get_weather", arguments: ["city": "Paris"], + inputs: ["preferred_unit": "celsius"]) + XCTAssertEqual(merged as? [String: String], ["city": "Paris"]) + } + + // MARK: - End to end + + /// The whole contract on the shipped fixture: the fixture declares map-form + /// bindings, the bound parameter is withheld from the model, and the value + /// the model never saw is restored from the prompt's own inputs. + func testFixtureStripsThenInjects() throws { + let path = Spec.fixtures.appendingPathComponent("tools_function.prompty").path + let agent = try Loader.load(path: path) + let tool = try XCTUnwrap(agent.tools?.first) + + // Declared as a map in the fixture, loaded as a named binding. + XCTAssertEqual(tool.bindings.map(\.name), ["unit"]) + XCTAssertEqual(tool.bindings.map(\.input), ["preferred_unit"]) + + // Stripped from what the model is shown... + XCTAssertEqual(tool.boundParameterNames, ["unit"]) + + // ...and restored before the tool runs. + let merged = Pipeline.applyBindings( + agent, + toolName: "get_weather", + arguments: ["location": "Paris"], + inputs: ["preferred_unit": "celsius"] + ) + XCTAssertEqual(merged["unit"] as? String, "celsius") + } +} diff --git a/runtime/swift/prompty/Tests/PromptyTests/WireVectorTests.swift b/runtime/swift/prompty/Tests/PromptyTests/WireVectorTests.swift new file mode 100644 index 000000000..61c9ca73d --- /dev/null +++ b/runtime/swift/prompty/Tests/PromptyTests/WireVectorTests.swift @@ -0,0 +1,124 @@ +import Foundation + +import PromptyModel + +import XCTest + +@testable import Prompty + +/// Conformance against `spec/vectors/wire/wire_vectors.json`. +/// +/// Builds a real agent from each vector's input and drives the real request +/// builders, so the assertion covers `ModelOptions.toWire`, tool projection and +/// structured-output wiring rather than a test-local approximation. +@testable import PromptyOpenAI + +final class WireVectorTests: XCTestCase { + + func testWireVectors() throws { + var run = VectorRun(stage: "wire") + + for vector in try Spec.vectors("wire") { + let name = vector["name"] as? String ?? "" + run.started() + let input = vector["input"] as? [String: Any] ?? [:] + let expected = vector["expected"] as? [String: Any] ?? [:] + + // This harness covers the OpenAI provider; Anthropic vectors belong to + // that provider's own package. + guard (input["provider"] as? String ?? "openai") == "openai" else { continue } + + do { + let agent = try Self.agent(from: input) + let messages = try Self.messages(from: input) + let apiType = input["apiType"] as? String ?? "chat" + + var body: [String: Any] + switch apiType { + case "chat", "agent": + body = try OpenAIWire.chatArgs(agent, messages: messages) + case "responses": + body = try OpenAIWire.responsesArgs(agent, messages: messages) + case "embedding": + body = OpenAIWire.embeddingArgs(agent, messages: messages) + case "image": + body = OpenAIWire.imageArgs(agent, messages: messages) + default: + throw InvokerError.execution("Unsupported apiType: \(apiType)") + } + + if input["stream"] as? Bool == true { + OpenAIWire.enableStreaming(&body, apiType: apiType) + } + + guard let expectedBody = expected["request_body"] else { continue } + try expectEqual(body, expectedBody, "request_body") + } catch { + run.fail(name, "\(error)") + } + } + + run.assertClean() + } + + // MARK: - Vector input decoding + + private static func agent(from input: [String: Any]) throws -> Agent { + var model: [String: Any] = [ + "provider": input["provider"] as? String ?? "openai" + ] + if let id = input["model_id"] as? String { model["id"] = id } + if let apiType = input["apiType"] as? String { model["apiType"] = apiType } + if let options = input["options"] as? [String: Any], !options.isEmpty { + model["options"] = options + } + + var data: [String: Any] = [ + "kind": "prompt", + "name": "wire-vectors", + "model": model, + ] + if let tools = input["tools"] as? [Any], !tools.isEmpty { data["tools"] = tools } + if let outputs = input["outputs"] as? [Any], !outputs.isEmpty { data["outputs"] = outputs } + + return try Agent.load(data) + } + + private static func messages(from input: [String: Any]) throws -> [Message] { + let raw = input["messages"] as? [[String: Any]] ?? [] + return try raw.map { entry in + var message = Message() + message.role = try Role.parse(entry["role"] as? String ?? "user") + message.parts = try (entry["content"] as? [[String: Any]] ?? []).map(part) + + // Vectors carry provider passthrough (e.g. tool_call_id) as sibling keys. + for (key, value) in entry where key != "role" && key != "content" { + message.metadata[key] = value + } + return message + } + } + + /// Vectors express every part's payload as `value` (plus optional + /// `mediaType`), which does not match the model's per-kind field names, so + /// the mapping is explicit. This mirrors the Rust vector runner. + private static func part(_ raw: [String: Any]) throws -> ContentPart { + let kind = raw["kind"] as? String ?? "text" + let value = raw["value"] as? String ?? "" + let mediaType = raw["mediaType"] as? String + + switch kind { + case "text": + return .textPart(TextPart(value: value)) + case "image": + return .imagePart( + ImagePart(source: value, detail: raw["detail"] as? String, mediaType: mediaType)) + case "audio": + return .audioPart(AudioPart(source: value, mediaType: mediaType)) + case "file": + return .filePart(FilePart(source: value, mediaType: mediaType)) + default: + throw InvokerError.parse("Unknown content kind: \(kind)") + } + } +} diff --git a/schema/tsp-output/.typra-generated/export-surfaces.json b/schema/tsp-output/.typra-generated/export-surfaces.json index f1d07f9d8..e483d652e 100644 --- a/schema/tsp-output/.typra-generated/export-surfaces.json +++ b/schema/tsp-output/.typra-generated/export-surfaces.json @@ -11236,6 +11236,2296 @@ "wire_conformance" ] }, + { + "target": "swift", + "outputRoot": "../runtime/swift/prompty-model", + "packageName": "PromptyModel", + "rootExports": [ + "Agent", + "AiResourceInfo", + "AnonymousConnection", + "AnthropicImageBlock", + "AnthropicImageSource", + "AnthropicMessagesRequest", + "AnthropicMessagesResponse", + "AnthropicTextBlock", + "AnthropicToolDefinition", + "AnthropicToolResultBlock", + "AnthropicToolUseBlock", + "AnthropicUsage", + "AnthropicWireMessage", + "ApiKeyConnection", + "ArrayProperty", + "AudioPart", + "AuthorizationCodeFlow", + "Binding", + "Checkpoint", + "CheckpointStore", + "CompactionCompletePayload", + "CompactionConfig", + "CompactionFailedPayload", + "CompactionStartPayload", + "Connection", + "ContentPart", + "ContextCandidate", + "ContextRequest", + "CustomTool", + "DelegatedStateReference", + "DeviceAuthorization", + "DiscoveryConformance", + "DoneEventPayload", + "EngineCheckpoint", + "EngineDurabilityPort", + "EngineEvent", + "EnginePermissionDecision", + "EnginePermissionPort", + "EnginePostCommitPort", + "EngineToolPort", + "ErrorChunk", + "ErrorEventPayload", + "EventJournalWriter", + "EventSink", + "Executor", + "FileNotFoundError", + "FilePart", + "FinalOutputPolicyRequest", + "FinalOutputPolicyResult", + "FormatConfig", + "FoundryConnection", + "FunctionTool", + "GuardrailResult", + "HarnessContext", + "HookEndPayload", + "HookStartPayload", + "HostPolicyRequest", + "HostPolicyResult", + "HostToolExecutor", + "HostToolRequest", + "HostToolResult", + "ImagePart", + "InvocationContextDecision", + "InvocationContextState", + "InvocationUsage", + "InvokerError", + "LlmCompletePayload", + "LlmStartPayload", + "LoadConformance", + "McpApprovalMode", + "McpTool", + "MemoryEntry", + "MemoryStore", + "Message", + "MessagesUpdatedPayload", + "Model", + "ModelInfo", + "ModelInvocationContextSnapshot", + "ModelInvocationRequest", + "ModelInvocationResponse", + "ModelLister", + "ModelOptions", + "ModelReconciliationState", + "ModelToolRequest", + "ModelToolResult", + "OAuthConnection", + "OAuthToken", + "ObjectProperty", + "OpenApiTool", + "Parser", + "ParserConfig", + "PermissionCompletedPayload", + "PermissionDecision", + "PermissionRequest", + "PermissionRequestedPayload", + "PermissionResolver", + "Processor", + "ProjectInfo", + "PromptyTool", + "Property", + "RedactedField", + "RedactionMetadata", + "ReferenceConnection", + "RemoteConnection", + "Renderer", + "ReplayJournalRecord", + "ReplayMismatch", + "ReplayVerificationRequest", + "ReplayVerificationResult", + "ResumeContext", + "RetryPayload", + "RetryPolicyRequest", + "RunTurnRequest", + "RunTurnResult", + "SessionEndPayload", + "SessionEvent", + "SessionFileRef", + "SessionRef", + "SessionStartPayload", + "SessionSummary", + "SessionTrace", + "SessionWarningPayload", + "StatusEventPayload", + "StreamChunk", + "StreamOptions", + "SubscriptionInfo", + "Template", + "TextChunk", + "TextPart", + "ThinkingChunk", + "ThinkingEventPayload", + "ThreadMarker", + "TokenEventPayload", + "TokenUsage", + "Tool", + "ToolCall", + "ToolCallCompletePayload", + "ToolCallStartPayload", + "ToolChunk", + "ToolContext", + "ToolDispatchResult", + "ToolExecutionCompletePayload", + "ToolExecutionStartPayload", + "ToolResult", + "ToolResultPayload", + "TraceFile", + "TraceSpan", + "TraceTime", + "TrajectoryEvent", + "TurnCommit", + "TurnConformance", + "TurnEndPayload", + "TurnEngineResult", + "TurnEvent", + "TurnModelRequest", + "TurnModelResponse", + "TurnOptions", + "TurnStartPayload", + "TurnSummary", + "TurnTrace", + "UnionProperty", + "UsageChunk", + "ValidationError", + "ValidationResult", + "WireConformance" + ], + "exports": [ + { + "name": "Agent", + "kind": "value", + "group": "", + "source": "Sources/PromptyModel/agent.swift", + "protocol": false + }, + { + "name": "CheckpointStore", + "kind": "type", + "group": "", + "source": "Sources/PromptyModel/checkpoint_store.swift", + "protocol": true + }, + { + "name": "DiscoveryConformance", + "kind": "type", + "group": "", + "source": "Sources/PromptyModel/discovery_conformance.swift", + "protocol": true + }, + { + "name": "EngineDurabilityPort", + "kind": "type", + "group": "", + "source": "Sources/PromptyModel/engine_durability_port.swift", + "protocol": true + }, + { + "name": "EnginePermissionPort", + "kind": "type", + "group": "", + "source": "Sources/PromptyModel/engine_permission_port.swift", + "protocol": true + }, + { + "name": "EnginePostCommitPort", + "kind": "type", + "group": "", + "source": "Sources/PromptyModel/engine_post_commit_port.swift", + "protocol": true + }, + { + "name": "EngineToolPort", + "kind": "type", + "group": "", + "source": "Sources/PromptyModel/engine_tool_port.swift", + "protocol": true + }, + { + "name": "EventJournalWriter", + "kind": "type", + "group": "", + "source": "Sources/PromptyModel/event_journal_writer.swift", + "protocol": true + }, + { + "name": "EventSink", + "kind": "type", + "group": "", + "source": "Sources/PromptyModel/event_sink.swift", + "protocol": true + }, + { + "name": "Executor", + "kind": "type", + "group": "", + "source": "Sources/PromptyModel/executor.swift", + "protocol": true + }, + { + "name": "HostToolExecutor", + "kind": "type", + "group": "", + "source": "Sources/PromptyModel/host_tool_executor.swift", + "protocol": true + }, + { + "name": "LoadConformance", + "kind": "type", + "group": "", + "source": "Sources/PromptyModel/load_conformance.swift", + "protocol": true + }, + { + "name": "ModelLister", + "kind": "type", + "group": "", + "source": "Sources/PromptyModel/model_lister.swift", + "protocol": true + }, + { + "name": "Parser", + "kind": "type", + "group": "", + "source": "Sources/PromptyModel/parser.swift", + "protocol": true + }, + { + "name": "PermissionResolver", + "kind": "type", + "group": "", + "source": "Sources/PromptyModel/permission_resolver.swift", + "protocol": true + }, + { + "name": "Processor", + "kind": "type", + "group": "", + "source": "Sources/PromptyModel/processor.swift", + "protocol": true + }, + { + "name": "Renderer", + "kind": "type", + "group": "", + "source": "Sources/PromptyModel/renderer.swift", + "protocol": true + }, + { + "name": "TurnConformance", + "kind": "type", + "group": "", + "source": "Sources/PromptyModel/turn_conformance.swift", + "protocol": true + }, + { + "name": "WireConformance", + "kind": "type", + "group": "", + "source": "Sources/PromptyModel/wire_conformance.swift", + "protocol": true + }, + { + "name": "AiResourceInfo", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/ai_resource_info.swift", + "protocol": false + }, + { + "name": "AnonymousConnection", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/connection.swift", + "protocol": false + }, + { + "name": "ApiKeyConnection", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/connection.swift", + "protocol": false + }, + { + "name": "ArrayProperty", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/property.swift", + "protocol": false + }, + { + "name": "AudioPart", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/content_part.swift", + "protocol": false + }, + { + "name": "AuthorizationCodeFlow", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/authorization_code_flow.swift", + "protocol": false + }, + { + "name": "Binding", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/binding.swift", + "protocol": false + }, + { + "name": "Checkpoint", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/checkpoint.swift", + "protocol": false + }, + { + "name": "CompactionCompletePayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/compaction_complete_payload.swift", + "protocol": false + }, + { + "name": "CompactionConfig", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/compaction_config.swift", + "protocol": false + }, + { + "name": "CompactionFailedPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/compaction_failed_payload.swift", + "protocol": false + }, + { + "name": "CompactionStartPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/compaction_start_payload.swift", + "protocol": false + }, + { + "name": "Connection", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/connection.swift", + "protocol": false + }, + { + "name": "ContentPart", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/content_part.swift", + "protocol": false + }, + { + "name": "ContextCandidate", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/context_candidate.swift", + "protocol": false + }, + { + "name": "ContextRequest", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/context_request.swift", + "protocol": false + }, + { + "name": "CustomTool", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/tool.swift", + "protocol": false + }, + { + "name": "DelegatedStateReference", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/delegated_state_reference.swift", + "protocol": false + }, + { + "name": "DeviceAuthorization", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/device_authorization.swift", + "protocol": false + }, + { + "name": "DoneEventPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/done_event_payload.swift", + "protocol": false + }, + { + "name": "EngineCheckpoint", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/engine_checkpoint.swift", + "protocol": false + }, + { + "name": "EngineEvent", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/engine_event.swift", + "protocol": false + }, + { + "name": "EnginePermissionDecision", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/engine_permission_decision.swift", + "protocol": false + }, + { + "name": "ErrorChunk", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/stream_chunk.swift", + "protocol": false + }, + { + "name": "ErrorEventPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/error_event_payload.swift", + "protocol": false + }, + { + "name": "FileNotFoundError", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/file_not_found_error.swift", + "protocol": false + }, + { + "name": "FilePart", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/content_part.swift", + "protocol": false + }, + { + "name": "FinalOutputPolicyRequest", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/final_output_policy_request.swift", + "protocol": false + }, + { + "name": "FinalOutputPolicyResult", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/final_output_policy_result.swift", + "protocol": false + }, + { + "name": "FormatConfig", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/format_config.swift", + "protocol": false + }, + { + "name": "FoundryConnection", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/connection.swift", + "protocol": false + }, + { + "name": "FunctionTool", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/tool.swift", + "protocol": false + }, + { + "name": "GuardrailResult", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/guardrail_result.swift", + "protocol": false + }, + { + "name": "HarnessContext", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/harness_context.swift", + "protocol": false + }, + { + "name": "HookEndPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/hook_end_payload.swift", + "protocol": false + }, + { + "name": "HookStartPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/hook_start_payload.swift", + "protocol": false + }, + { + "name": "HostPolicyRequest", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/host_policy_request.swift", + "protocol": false + }, + { + "name": "HostPolicyResult", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/host_policy_result.swift", + "protocol": false + }, + { + "name": "HostToolRequest", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/host_tool_request.swift", + "protocol": false + }, + { + "name": "HostToolResult", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/host_tool_result.swift", + "protocol": false + }, + { + "name": "ImagePart", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/content_part.swift", + "protocol": false + }, + { + "name": "InvocationContextDecision", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/invocation_context_decision.swift", + "protocol": false + }, + { + "name": "InvocationContextState", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/invocation_context_state.swift", + "protocol": false + }, + { + "name": "InvocationUsage", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/invocation_usage.swift", + "protocol": false + }, + { + "name": "InvokerError", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/invoker_error.swift", + "protocol": false + }, + { + "name": "LlmCompletePayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/llm_complete_payload.swift", + "protocol": false + }, + { + "name": "LlmStartPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/llm_start_payload.swift", + "protocol": false + }, + { + "name": "McpApprovalMode", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/mcp_approval_mode.swift", + "protocol": false + }, + { + "name": "McpTool", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/tool.swift", + "protocol": false + }, + { + "name": "MemoryEntry", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/memory_entry.swift", + "protocol": false + }, + { + "name": "MemoryStore", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/memory_store.swift", + "protocol": false + }, + { + "name": "Message", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/message.swift", + "protocol": false + }, + { + "name": "MessagesUpdatedPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/messages_updated_payload.swift", + "protocol": false + }, + { + "name": "Model", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/model.swift", + "protocol": false + }, + { + "name": "ModelInfo", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/model_info.swift", + "protocol": false + }, + { + "name": "ModelInvocationContextSnapshot", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/model_invocation_context_snapshot.swift", + "protocol": false + }, + { + "name": "ModelInvocationRequest", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/model_invocation_request.swift", + "protocol": false + }, + { + "name": "ModelInvocationResponse", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/model_invocation_response.swift", + "protocol": false + }, + { + "name": "ModelOptions", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/model_options.swift", + "protocol": false + }, + { + "name": "ModelReconciliationState", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/model_reconciliation_state.swift", + "protocol": false + }, + { + "name": "ModelToolRequest", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/model_tool_request.swift", + "protocol": false + }, + { + "name": "ModelToolResult", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/model_tool_result.swift", + "protocol": false + }, + { + "name": "OAuthConnection", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/connection.swift", + "protocol": false + }, + { + "name": "OAuthToken", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/o_auth_token.swift", + "protocol": false + }, + { + "name": "ObjectProperty", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/property.swift", + "protocol": false + }, + { + "name": "OpenApiTool", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/tool.swift", + "protocol": false + }, + { + "name": "ParserConfig", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/parser_config.swift", + "protocol": false + }, + { + "name": "PermissionCompletedPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/permission_completed_payload.swift", + "protocol": false + }, + { + "name": "PermissionDecision", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/permission_decision.swift", + "protocol": false + }, + { + "name": "PermissionRequest", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/permission_request.swift", + "protocol": false + }, + { + "name": "PermissionRequestedPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/permission_requested_payload.swift", + "protocol": false + }, + { + "name": "ProjectInfo", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/project_info.swift", + "protocol": false + }, + { + "name": "PromptyTool", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/tool.swift", + "protocol": false + }, + { + "name": "Property", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/property.swift", + "protocol": false + }, + { + "name": "RedactedField", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/redacted_field.swift", + "protocol": false + }, + { + "name": "RedactionMetadata", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/redaction_metadata.swift", + "protocol": false + }, + { + "name": "ReferenceConnection", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/connection.swift", + "protocol": false + }, + { + "name": "RemoteConnection", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/connection.swift", + "protocol": false + }, + { + "name": "ReplayJournalRecord", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/replay_journal_record.swift", + "protocol": false + }, + { + "name": "ReplayMismatch", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/replay_mismatch.swift", + "protocol": false + }, + { + "name": "ReplayVerificationRequest", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/replay_verification_request.swift", + "protocol": false + }, + { + "name": "ReplayVerificationResult", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/replay_verification_result.swift", + "protocol": false + }, + { + "name": "ResumeContext", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/resume_context.swift", + "protocol": false + }, + { + "name": "RetryPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/retry_payload.swift", + "protocol": false + }, + { + "name": "RetryPolicyRequest", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/retry_policy_request.swift", + "protocol": false + }, + { + "name": "RunTurnRequest", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/run_turn_request.swift", + "protocol": false + }, + { + "name": "RunTurnResult", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/run_turn_result.swift", + "protocol": false + }, + { + "name": "SessionEndPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/session_end_payload.swift", + "protocol": false + }, + { + "name": "SessionEvent", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/session_event.swift", + "protocol": false + }, + { + "name": "SessionFileRef", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/session_file_ref.swift", + "protocol": false + }, + { + "name": "SessionRef", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/session_ref.swift", + "protocol": false + }, + { + "name": "SessionStartPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/session_start_payload.swift", + "protocol": false + }, + { + "name": "SessionSummary", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/session_summary.swift", + "protocol": false + }, + { + "name": "SessionTrace", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/session_trace.swift", + "protocol": false + }, + { + "name": "SessionWarningPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/session_warning_payload.swift", + "protocol": false + }, + { + "name": "StatusEventPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/status_event_payload.swift", + "protocol": false + }, + { + "name": "StreamChunk", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/stream_chunk.swift", + "protocol": false + }, + { + "name": "StreamOptions", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/stream_options.swift", + "protocol": false + }, + { + "name": "SubscriptionInfo", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/subscription_info.swift", + "protocol": false + }, + { + "name": "Template", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/template.swift", + "protocol": false + }, + { + "name": "TextChunk", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/stream_chunk.swift", + "protocol": false + }, + { + "name": "TextPart", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/content_part.swift", + "protocol": false + }, + { + "name": "ThinkingChunk", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/stream_chunk.swift", + "protocol": false + }, + { + "name": "ThinkingEventPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/thinking_event_payload.swift", + "protocol": false + }, + { + "name": "ThreadMarker", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/thread_marker.swift", + "protocol": false + }, + { + "name": "TokenEventPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/token_event_payload.swift", + "protocol": false + }, + { + "name": "TokenUsage", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/token_usage.swift", + "protocol": false + }, + { + "name": "Tool", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/tool.swift", + "protocol": false + }, + { + "name": "ToolCall", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/tool_call.swift", + "protocol": false + }, + { + "name": "ToolCallCompletePayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/tool_call_complete_payload.swift", + "protocol": false + }, + { + "name": "ToolCallStartPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/tool_call_start_payload.swift", + "protocol": false + }, + { + "name": "ToolChunk", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/stream_chunk.swift", + "protocol": false + }, + { + "name": "ToolContext", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/tool_context.swift", + "protocol": false + }, + { + "name": "ToolDispatchResult", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/tool_dispatch_result.swift", + "protocol": false + }, + { + "name": "ToolExecutionCompletePayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/tool_execution_complete_payload.swift", + "protocol": false + }, + { + "name": "ToolExecutionStartPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/tool_execution_start_payload.swift", + "protocol": false + }, + { + "name": "ToolResult", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/tool_result.swift", + "protocol": false + }, + { + "name": "ToolResultPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/tool_result_payload.swift", + "protocol": false + }, + { + "name": "TraceFile", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/trace_file.swift", + "protocol": false + }, + { + "name": "TraceSpan", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/trace_span.swift", + "protocol": false + }, + { + "name": "TraceTime", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/trace_time.swift", + "protocol": false + }, + { + "name": "TrajectoryEvent", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/trajectory_event.swift", + "protocol": false + }, + { + "name": "TurnCommit", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/turn_commit.swift", + "protocol": false + }, + { + "name": "TurnEndPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/turn_end_payload.swift", + "protocol": false + }, + { + "name": "TurnEngineResult", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/turn_engine_result.swift", + "protocol": false + }, + { + "name": "TurnEvent", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/turn_event.swift", + "protocol": false + }, + { + "name": "TurnModelRequest", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/turn_model_request.swift", + "protocol": false + }, + { + "name": "TurnModelResponse", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/turn_model_response.swift", + "protocol": false + }, + { + "name": "TurnOptions", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/turn_options.swift", + "protocol": false + }, + { + "name": "TurnStartPayload", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/turn_start_payload.swift", + "protocol": false + }, + { + "name": "TurnSummary", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/turn_summary.swift", + "protocol": false + }, + { + "name": "TurnTrace", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/turn_trace.swift", + "protocol": false + }, + { + "name": "UnionProperty", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/property.swift", + "protocol": false + }, + { + "name": "UsageChunk", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/stream_chunk.swift", + "protocol": false + }, + { + "name": "ValidationError", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/validation_error.swift", + "protocol": false + }, + { + "name": "ValidationResult", + "kind": "value", + "group": "contracts", + "source": "Sources/PromptyModel/contracts/validation_result.swift", + "protocol": false + }, + { + "name": "AnthropicImageBlock", + "kind": "value", + "group": "wire", + "source": "Sources/PromptyModel/wire/anthropic_image_block.swift", + "protocol": false + }, + { + "name": "AnthropicImageSource", + "kind": "value", + "group": "wire", + "source": "Sources/PromptyModel/wire/anthropic_image_source.swift", + "protocol": false + }, + { + "name": "AnthropicMessagesRequest", + "kind": "value", + "group": "wire", + "source": "Sources/PromptyModel/wire/anthropic_messages_request.swift", + "protocol": false + }, + { + "name": "AnthropicMessagesResponse", + "kind": "value", + "group": "wire", + "source": "Sources/PromptyModel/wire/anthropic_messages_response.swift", + "protocol": false + }, + { + "name": "AnthropicTextBlock", + "kind": "value", + "group": "wire", + "source": "Sources/PromptyModel/wire/anthropic_text_block.swift", + "protocol": false + }, + { + "name": "AnthropicToolDefinition", + "kind": "value", + "group": "wire", + "source": "Sources/PromptyModel/wire/anthropic_tool_definition.swift", + "protocol": false + }, + { + "name": "AnthropicToolResultBlock", + "kind": "value", + "group": "wire", + "source": "Sources/PromptyModel/wire/anthropic_tool_result_block.swift", + "protocol": false + }, + { + "name": "AnthropicToolUseBlock", + "kind": "value", + "group": "wire", + "source": "Sources/PromptyModel/wire/anthropic_tool_use_block.swift", + "protocol": false + }, + { + "name": "AnthropicUsage", + "kind": "value", + "group": "wire", + "source": "Sources/PromptyModel/wire/anthropic_usage.swift", + "protocol": false + }, + { + "name": "AnthropicWireMessage", + "kind": "value", + "group": "wire", + "source": "Sources/PromptyModel/wire/anthropic_wire_message.swift", + "protocol": false + } + ], + "groups": [ + { + "name": "contracts", + "exports": [ + "AiResourceInfo", + "AnonymousConnection", + "ApiKeyConnection", + "ArrayProperty", + "AudioPart", + "AuthorizationCodeFlow", + "Binding", + "Checkpoint", + "CompactionCompletePayload", + "CompactionConfig", + "CompactionFailedPayload", + "CompactionStartPayload", + "Connection", + "ContentPart", + "ContextCandidate", + "ContextRequest", + "CustomTool", + "DelegatedStateReference", + "DeviceAuthorization", + "DoneEventPayload", + "EngineCheckpoint", + "EngineEvent", + "EnginePermissionDecision", + "ErrorChunk", + "ErrorEventPayload", + "FileNotFoundError", + "FilePart", + "FinalOutputPolicyRequest", + "FinalOutputPolicyResult", + "FormatConfig", + "FoundryConnection", + "FunctionTool", + "GuardrailResult", + "HarnessContext", + "HookEndPayload", + "HookStartPayload", + "HostPolicyRequest", + "HostPolicyResult", + "HostToolRequest", + "HostToolResult", + "ImagePart", + "InvocationContextDecision", + "InvocationContextState", + "InvocationUsage", + "InvokerError", + "LlmCompletePayload", + "LlmStartPayload", + "McpApprovalMode", + "McpTool", + "MemoryEntry", + "MemoryStore", + "Message", + "MessagesUpdatedPayload", + "Model", + "ModelInfo", + "ModelInvocationContextSnapshot", + "ModelInvocationRequest", + "ModelInvocationResponse", + "ModelOptions", + "ModelReconciliationState", + "ModelToolRequest", + "ModelToolResult", + "OAuthConnection", + "OAuthToken", + "ObjectProperty", + "OpenApiTool", + "ParserConfig", + "PermissionCompletedPayload", + "PermissionDecision", + "PermissionRequest", + "PermissionRequestedPayload", + "ProjectInfo", + "PromptyTool", + "Property", + "RedactedField", + "RedactionMetadata", + "ReferenceConnection", + "RemoteConnection", + "ReplayJournalRecord", + "ReplayMismatch", + "ReplayVerificationRequest", + "ReplayVerificationResult", + "ResumeContext", + "RetryPayload", + "RetryPolicyRequest", + "RunTurnRequest", + "RunTurnResult", + "SessionEndPayload", + "SessionEvent", + "SessionFileRef", + "SessionRef", + "SessionStartPayload", + "SessionSummary", + "SessionTrace", + "SessionWarningPayload", + "StatusEventPayload", + "StreamChunk", + "StreamOptions", + "SubscriptionInfo", + "Template", + "TextChunk", + "TextPart", + "ThinkingChunk", + "ThinkingEventPayload", + "ThreadMarker", + "TokenEventPayload", + "TokenUsage", + "Tool", + "ToolCall", + "ToolCallCompletePayload", + "ToolCallStartPayload", + "ToolChunk", + "ToolContext", + "ToolDispatchResult", + "ToolExecutionCompletePayload", + "ToolExecutionStartPayload", + "ToolResult", + "ToolResultPayload", + "TraceFile", + "TraceSpan", + "TraceTime", + "TrajectoryEvent", + "TurnCommit", + "TurnEndPayload", + "TurnEngineResult", + "TurnEvent", + "TurnModelRequest", + "TurnModelResponse", + "TurnOptions", + "TurnStartPayload", + "TurnSummary", + "TurnTrace", + "UnionProperty", + "UsageChunk", + "ValidationError", + "ValidationResult" + ], + "modules": [ + "ai_resource_info.swift", + "authorization_code_flow.swift", + "binding.swift", + "checkpoint.swift", + "compaction_complete_payload.swift", + "compaction_config.swift", + "compaction_failed_payload.swift", + "compaction_start_payload.swift", + "connection.swift", + "content_part.swift", + "context_candidate.swift", + "context_request.swift", + "delegated_state_reference.swift", + "device_authorization.swift", + "done_event_payload.swift", + "engine_checkpoint.swift", + "engine_event.swift", + "engine_permission_decision.swift", + "error_event_payload.swift", + "file_not_found_error.swift", + "final_output_policy_request.swift", + "final_output_policy_result.swift", + "format_config.swift", + "guardrail_result.swift", + "harness_context.swift", + "hook_end_payload.swift", + "hook_start_payload.swift", + "host_policy_request.swift", + "host_policy_result.swift", + "host_tool_request.swift", + "host_tool_result.swift", + "invocation_context_decision.swift", + "invocation_context_state.swift", + "invocation_usage.swift", + "invoker_error.swift", + "llm_complete_payload.swift", + "llm_start_payload.swift", + "mcp_approval_mode.swift", + "memory_entry.swift", + "memory_store.swift", + "message.swift", + "messages_updated_payload.swift", + "model.swift", + "model_info.swift", + "model_invocation_context_snapshot.swift", + "model_invocation_request.swift", + "model_invocation_response.swift", + "model_options.swift", + "model_reconciliation_state.swift", + "model_tool_request.swift", + "model_tool_result.swift", + "o_auth_token.swift", + "parser_config.swift", + "permission_completed_payload.swift", + "permission_decision.swift", + "permission_request.swift", + "permission_requested_payload.swift", + "project_info.swift", + "property.swift", + "redacted_field.swift", + "redaction_metadata.swift", + "replay_journal_record.swift", + "replay_mismatch.swift", + "replay_verification_request.swift", + "replay_verification_result.swift", + "resume_context.swift", + "retry_payload.swift", + "retry_policy_request.swift", + "run_turn_request.swift", + "run_turn_result.swift", + "session_end_payload.swift", + "session_event.swift", + "session_file_ref.swift", + "session_ref.swift", + "session_start_payload.swift", + "session_summary.swift", + "session_trace.swift", + "session_warning_payload.swift", + "status_event_payload.swift", + "stream_chunk.swift", + "stream_options.swift", + "subscription_info.swift", + "template.swift", + "thinking_event_payload.swift", + "thread_marker.swift", + "token_event_payload.swift", + "token_usage.swift", + "tool.swift", + "tool_call.swift", + "tool_call_complete_payload.swift", + "tool_call_start_payload.swift", + "tool_context.swift", + "tool_dispatch_result.swift", + "tool_execution_complete_payload.swift", + "tool_execution_start_payload.swift", + "tool_result.swift", + "tool_result_payload.swift", + "trace_file.swift", + "trace_span.swift", + "trace_time.swift", + "trajectory_event.swift", + "turn_commit.swift", + "turn_end_payload.swift", + "turn_engine_result.swift", + "turn_event.swift", + "turn_model_request.swift", + "turn_model_response.swift", + "turn_options.swift", + "turn_start_payload.swift", + "turn_summary.swift", + "turn_trace.swift", + "validation_error.swift", + "validation_result.swift" + ] + }, + { + "name": "wire", + "exports": [ + "AnthropicImageBlock", + "AnthropicImageSource", + "AnthropicMessagesRequest", + "AnthropicMessagesResponse", + "AnthropicTextBlock", + "AnthropicToolDefinition", + "AnthropicToolResultBlock", + "AnthropicToolUseBlock", + "AnthropicUsage", + "AnthropicWireMessage" + ], + "modules": [ + "anthropic_image_block.swift", + "anthropic_image_source.swift", + "anthropic_messages_request.swift", + "anthropic_messages_response.swift", + "anthropic_text_block.swift", + "anthropic_tool_definition.swift", + "anthropic_tool_result_block.swift", + "anthropic_tool_use_block.swift", + "anthropic_usage.swift", + "anthropic_wire_message.swift" + ] + } + ], + "protocols": [ + { + "name": "CheckpointStore", + "group": "", + "symbol": "CheckpointStore", + "source": "Sources/PromptyModel/checkpoint_store.swift", + "methods": [ + { + "name": "listCheckpoints", + "returns": "Checkpoint[]", + "params": { + "sessionId": "string" + }, + "optional": false, + "sync": false, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": false + }, + { + "name": "load", + "returns": "Checkpoint?", + "params": { + "checkpointId": "string", + "sessionId": "string" + }, + "optional": false, + "sync": false, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": false + }, + { + "name": "save", + "returns": "Checkpoint", + "params": { + "checkpoint": "Checkpoint" + }, + "optional": false, + "sync": false, + "runtimeCancellable": false, + "atomic": true, + "nonFatal": false + } + ] + }, + { + "name": "DiscoveryConformance", + "group": "", + "symbol": "DiscoveryConformance", + "source": "Sources/PromptyModel/discovery_conformance.swift", + "methods": [ + { + "name": "enrich", + "returns": "ModelInfo", + "params": { + "base": "ModelInfo" + }, + "optional": false, + "sync": false, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": false + }, + { + "name": "mapModel", + "returns": "ModelInfo", + "params": { + "raw": "unknown" + }, + "optional": false, + "sync": false, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": false + } + ] + }, + { + "name": "EngineDurabilityPort", + "group": "", + "symbol": "EngineDurabilityPort", + "source": "Sources/PromptyModel/engine_durability_port.swift", + "methods": [ + { + "name": "append", + "returns": "void", + "params": { + "event": "EngineEvent" + }, + "optional": false, + "sync": false, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": false + }, + { + "name": "appendWithCheckpoint", + "returns": "void", + "params": { + "checkpoint": "EngineCheckpoint", + "events": "EngineEvent[]" + }, + "optional": false, + "sync": false, + "runtimeCancellable": false, + "atomic": true, + "nonFatal": false + } + ] + }, + { + "name": "EnginePermissionPort", + "group": "", + "symbol": "EnginePermissionPort", + "source": "Sources/PromptyModel/engine_permission_port.swift", + "methods": [ + { + "name": "authorize", + "returns": "EnginePermissionDecision", + "params": { + "request": "ModelToolRequest" + }, + "optional": false, + "sync": false, + "runtimeCancellable": true, + "atomic": false, + "nonFatal": false + } + ] + }, + { + "name": "EnginePostCommitPort", + "group": "", + "symbol": "EnginePostCommitPort", + "source": "Sources/PromptyModel/engine_post_commit_port.swift", + "methods": [ + { + "name": "afterCommit", + "returns": "void", + "params": { + "commit": "TurnCommit", + "effectId": "string" + }, + "optional": false, + "sync": false, + "runtimeCancellable": true, + "atomic": false, + "nonFatal": true + } + ] + }, + { + "name": "EngineToolPort", + "group": "", + "symbol": "EngineToolPort", + "source": "Sources/PromptyModel/engine_tool_port.swift", + "methods": [ + { + "name": "execute", + "returns": "ModelToolResult", + "params": { + "request": "ModelToolRequest" + }, + "optional": false, + "sync": false, + "runtimeCancellable": true, + "atomic": false, + "nonFatal": false + } + ] + }, + { + "name": "EventJournalWriter", + "group": "", + "symbol": "EventJournalWriter", + "source": "Sources/PromptyModel/event_journal_writer.swift", + "methods": [ + { + "name": "appendSession", + "returns": "boolean", + "params": { + "sessionEvent": "SessionEvent" + }, + "optional": false, + "sync": true, + "runtimeCancellable": false, + "atomic": true, + "nonFatal": false + }, + { + "name": "appendTurn", + "returns": "boolean", + "params": { + "turnEvent": "TurnEvent" + }, + "optional": false, + "sync": true, + "runtimeCancellable": false, + "atomic": true, + "nonFatal": false + }, + { + "name": "close", + "returns": "boolean", + "params": { + "summary": "SessionSummary?" + }, + "optional": false, + "sync": true, + "runtimeCancellable": false, + "atomic": true, + "nonFatal": false + } + ] + }, + { + "name": "EventSink", + "group": "", + "symbol": "EventSink", + "source": "Sources/PromptyModel/event_sink.swift", + "methods": [ + { + "name": "emitSession", + "returns": "boolean", + "params": { + "sessionEvent": "SessionEvent" + }, + "optional": false, + "sync": true, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": true + }, + { + "name": "emitTurn", + "returns": "boolean", + "params": { + "turnEvent": "TurnEvent" + }, + "optional": false, + "sync": true, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": true + } + ] + }, + { + "name": "Executor", + "group": "", + "symbol": "Executor", + "source": "Sources/PromptyModel/executor.swift", + "methods": [ + { + "name": "execute", + "returns": "unknown", + "params": { + "agent": "Agent", + "messages": "Message[]" + }, + "optional": false, + "sync": false, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": false + }, + { + "name": "executeStream", + "returns": "unknown", + "params": { + "agent": "Agent", + "messages": "Message[]" + }, + "optional": true, + "sync": false, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": false + }, + { + "name": "formatToolMessages", + "returns": "Message[]", + "params": { + "rawResponse": "unknown", + "textContent": "string?", + "toolCalls": "ToolCall[]", + "toolResults": "string[]" + }, + "optional": false, + "sync": true, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": false + } + ] + }, + { + "name": "HostToolExecutor", + "group": "", + "symbol": "HostToolExecutor", + "source": "Sources/PromptyModel/host_tool_executor.swift", + "methods": [ + { + "name": "execute", + "returns": "HostToolResult", + "params": { + "request": "HostToolRequest" + }, + "optional": false, + "sync": false, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": false + } + ] + }, + { + "name": "LoadConformance", + "group": "", + "symbol": "LoadConformance", + "source": "Sources/PromptyModel/load_conformance.swift", + "methods": [ + { + "name": "load", + "returns": "Agent", + "params": { + "source": "unknown" + }, + "optional": false, + "sync": false, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": false + } + ] + }, + { + "name": "ModelLister", + "group": "", + "symbol": "ModelLister", + "source": "Sources/PromptyModel/model_lister.swift", + "methods": [ + { + "name": "listModels", + "returns": "ModelInfo[]", + "params": { + "connection": "unknown" + }, + "optional": false, + "sync": false, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": false + } + ] + }, + { + "name": "Parser", + "group": "", + "symbol": "Parser", + "source": "Sources/PromptyModel/parser.swift", + "methods": [ + { + "name": "parse", + "returns": "Message[]", + "params": { + "agent": "Agent", + "context": "Record?", + "rendered": "string" + }, + "optional": false, + "sync": false, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": false + }, + { + "name": "preRender", + "returns": "unknown?", + "params": { + "template": "string" + }, + "optional": true, + "sync": true, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": false + } + ] + }, + { + "name": "PermissionResolver", + "group": "", + "symbol": "PermissionResolver", + "source": "Sources/PromptyModel/permission_resolver.swift", + "methods": [ + { + "name": "request", + "returns": "PermissionDecision", + "params": { + "request": "PermissionRequest" + }, + "optional": false, + "sync": false, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": false + } + ] + }, + { + "name": "Processor", + "group": "", + "symbol": "Processor", + "source": "Sources/PromptyModel/processor.swift", + "methods": [ + { + "name": "process", + "returns": "unknown", + "params": { + "agent": "Agent", + "response": "unknown" + }, + "optional": false, + "sync": false, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": false + }, + { + "name": "processStream", + "returns": "unknown", + "params": { + "stream": "unknown" + }, + "optional": true, + "sync": false, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": false + } + ] + }, + { + "name": "Renderer", + "group": "", + "symbol": "Renderer", + "source": "Sources/PromptyModel/renderer.swift", + "methods": [ + { + "name": "render", + "returns": "string", + "params": { + "agent": "Agent", + "inputs": "Record", + "template": "string" + }, + "optional": false, + "sync": false, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": false + } + ] + }, + { + "name": "TurnConformance", + "group": "", + "symbol": "TurnConformance", + "source": "Sources/PromptyModel/turn_conformance.swift", + "methods": [ + { + "name": "replay", + "returns": "unknown", + "params": { + "input": "unknown" + }, + "optional": false, + "sync": false, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": false + }, + { + "name": "run", + "returns": "unknown", + "params": { + "input": "unknown" + }, + "optional": false, + "sync": false, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": false + }, + { + "name": "runTurn", + "returns": "unknown", + "params": { + "input": "unknown" + }, + "optional": false, + "sync": false, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": false + } + ] + }, + { + "name": "WireConformance", + "group": "", + "symbol": "WireConformance", + "source": "Sources/PromptyModel/wire_conformance.swift", + "methods": [ + { + "name": "toRequest", + "returns": "unknown", + "params": { + "agent": "Agent", + "messages": "Message[]" + }, + "optional": false, + "sync": false, + "runtimeCancellable": false, + "atomic": false, + "nonFatal": false + } + ] + } + ], + "modules": [ + "Sources/PromptyModel/agent.swift", + "Sources/PromptyModel/checkpoint_store.swift", + "Sources/PromptyModel/contracts/ai_resource_info.swift", + "Sources/PromptyModel/contracts/authorization_code_flow.swift", + "Sources/PromptyModel/contracts/binding.swift", + "Sources/PromptyModel/contracts/checkpoint.swift", + "Sources/PromptyModel/contracts/compaction_complete_payload.swift", + "Sources/PromptyModel/contracts/compaction_config.swift", + "Sources/PromptyModel/contracts/compaction_failed_payload.swift", + "Sources/PromptyModel/contracts/compaction_start_payload.swift", + "Sources/PromptyModel/contracts/connection.swift", + "Sources/PromptyModel/contracts/content_part.swift", + "Sources/PromptyModel/contracts/context_candidate.swift", + "Sources/PromptyModel/contracts/context_request.swift", + "Sources/PromptyModel/contracts/delegated_state_reference.swift", + "Sources/PromptyModel/contracts/device_authorization.swift", + "Sources/PromptyModel/contracts/done_event_payload.swift", + "Sources/PromptyModel/contracts/engine_checkpoint.swift", + "Sources/PromptyModel/contracts/engine_event.swift", + "Sources/PromptyModel/contracts/engine_permission_decision.swift", + "Sources/PromptyModel/contracts/error_event_payload.swift", + "Sources/PromptyModel/contracts/file_not_found_error.swift", + "Sources/PromptyModel/contracts/final_output_policy_request.swift", + "Sources/PromptyModel/contracts/final_output_policy_result.swift", + "Sources/PromptyModel/contracts/format_config.swift", + "Sources/PromptyModel/contracts/guardrail_result.swift", + "Sources/PromptyModel/contracts/harness_context.swift", + "Sources/PromptyModel/contracts/hook_end_payload.swift", + "Sources/PromptyModel/contracts/hook_start_payload.swift", + "Sources/PromptyModel/contracts/host_policy_request.swift", + "Sources/PromptyModel/contracts/host_policy_result.swift", + "Sources/PromptyModel/contracts/host_tool_request.swift", + "Sources/PromptyModel/contracts/host_tool_result.swift", + "Sources/PromptyModel/contracts/invocation_context_decision.swift", + "Sources/PromptyModel/contracts/invocation_context_state.swift", + "Sources/PromptyModel/contracts/invocation_usage.swift", + "Sources/PromptyModel/contracts/invoker_error.swift", + "Sources/PromptyModel/contracts/llm_complete_payload.swift", + "Sources/PromptyModel/contracts/llm_start_payload.swift", + "Sources/PromptyModel/contracts/mcp_approval_mode.swift", + "Sources/PromptyModel/contracts/memory_entry.swift", + "Sources/PromptyModel/contracts/memory_store.swift", + "Sources/PromptyModel/contracts/message.swift", + "Sources/PromptyModel/contracts/messages_updated_payload.swift", + "Sources/PromptyModel/contracts/model.swift", + "Sources/PromptyModel/contracts/model_info.swift", + "Sources/PromptyModel/contracts/model_invocation_context_snapshot.swift", + "Sources/PromptyModel/contracts/model_invocation_request.swift", + "Sources/PromptyModel/contracts/model_invocation_response.swift", + "Sources/PromptyModel/contracts/model_options.swift", + "Sources/PromptyModel/contracts/model_reconciliation_state.swift", + "Sources/PromptyModel/contracts/model_tool_request.swift", + "Sources/PromptyModel/contracts/model_tool_result.swift", + "Sources/PromptyModel/contracts/o_auth_token.swift", + "Sources/PromptyModel/contracts/parser_config.swift", + "Sources/PromptyModel/contracts/permission_completed_payload.swift", + "Sources/PromptyModel/contracts/permission_decision.swift", + "Sources/PromptyModel/contracts/permission_request.swift", + "Sources/PromptyModel/contracts/permission_requested_payload.swift", + "Sources/PromptyModel/contracts/project_info.swift", + "Sources/PromptyModel/contracts/property.swift", + "Sources/PromptyModel/contracts/redacted_field.swift", + "Sources/PromptyModel/contracts/redaction_metadata.swift", + "Sources/PromptyModel/contracts/replay_journal_record.swift", + "Sources/PromptyModel/contracts/replay_mismatch.swift", + "Sources/PromptyModel/contracts/replay_verification_request.swift", + "Sources/PromptyModel/contracts/replay_verification_result.swift", + "Sources/PromptyModel/contracts/resume_context.swift", + "Sources/PromptyModel/contracts/retry_payload.swift", + "Sources/PromptyModel/contracts/retry_policy_request.swift", + "Sources/PromptyModel/contracts/run_turn_request.swift", + "Sources/PromptyModel/contracts/run_turn_result.swift", + "Sources/PromptyModel/contracts/session_end_payload.swift", + "Sources/PromptyModel/contracts/session_event.swift", + "Sources/PromptyModel/contracts/session_file_ref.swift", + "Sources/PromptyModel/contracts/session_ref.swift", + "Sources/PromptyModel/contracts/session_start_payload.swift", + "Sources/PromptyModel/contracts/session_summary.swift", + "Sources/PromptyModel/contracts/session_trace.swift", + "Sources/PromptyModel/contracts/session_warning_payload.swift", + "Sources/PromptyModel/contracts/status_event_payload.swift", + "Sources/PromptyModel/contracts/stream_chunk.swift", + "Sources/PromptyModel/contracts/stream_options.swift", + "Sources/PromptyModel/contracts/subscription_info.swift", + "Sources/PromptyModel/contracts/template.swift", + "Sources/PromptyModel/contracts/thinking_event_payload.swift", + "Sources/PromptyModel/contracts/thread_marker.swift", + "Sources/PromptyModel/contracts/token_event_payload.swift", + "Sources/PromptyModel/contracts/token_usage.swift", + "Sources/PromptyModel/contracts/tool.swift", + "Sources/PromptyModel/contracts/tool_call.swift", + "Sources/PromptyModel/contracts/tool_call_complete_payload.swift", + "Sources/PromptyModel/contracts/tool_call_start_payload.swift", + "Sources/PromptyModel/contracts/tool_context.swift", + "Sources/PromptyModel/contracts/tool_dispatch_result.swift", + "Sources/PromptyModel/contracts/tool_execution_complete_payload.swift", + "Sources/PromptyModel/contracts/tool_execution_start_payload.swift", + "Sources/PromptyModel/contracts/tool_result.swift", + "Sources/PromptyModel/contracts/tool_result_payload.swift", + "Sources/PromptyModel/contracts/trace_file.swift", + "Sources/PromptyModel/contracts/trace_span.swift", + "Sources/PromptyModel/contracts/trace_time.swift", + "Sources/PromptyModel/contracts/trajectory_event.swift", + "Sources/PromptyModel/contracts/turn_commit.swift", + "Sources/PromptyModel/contracts/turn_end_payload.swift", + "Sources/PromptyModel/contracts/turn_engine_result.swift", + "Sources/PromptyModel/contracts/turn_event.swift", + "Sources/PromptyModel/contracts/turn_model_request.swift", + "Sources/PromptyModel/contracts/turn_model_response.swift", + "Sources/PromptyModel/contracts/turn_options.swift", + "Sources/PromptyModel/contracts/turn_start_payload.swift", + "Sources/PromptyModel/contracts/turn_summary.swift", + "Sources/PromptyModel/contracts/turn_trace.swift", + "Sources/PromptyModel/contracts/validation_error.swift", + "Sources/PromptyModel/contracts/validation_result.swift", + "Sources/PromptyModel/discovery_conformance.swift", + "Sources/PromptyModel/engine_durability_port.swift", + "Sources/PromptyModel/engine_permission_port.swift", + "Sources/PromptyModel/engine_post_commit_port.swift", + "Sources/PromptyModel/engine_tool_port.swift", + "Sources/PromptyModel/event_journal_writer.swift", + "Sources/PromptyModel/event_sink.swift", + "Sources/PromptyModel/executor.swift", + "Sources/PromptyModel/host_tool_executor.swift", + "Sources/PromptyModel/load_conformance.swift", + "Sources/PromptyModel/model_lister.swift", + "Sources/PromptyModel/parser.swift", + "Sources/PromptyModel/permission_resolver.swift", + "Sources/PromptyModel/processor.swift", + "Sources/PromptyModel/renderer.swift", + "Sources/PromptyModel/turn_conformance.swift", + "Sources/PromptyModel/wire/anthropic_image_block.swift", + "Sources/PromptyModel/wire/anthropic_image_source.swift", + "Sources/PromptyModel/wire/anthropic_messages_request.swift", + "Sources/PromptyModel/wire/anthropic_messages_response.swift", + "Sources/PromptyModel/wire/anthropic_text_block.swift", + "Sources/PromptyModel/wire/anthropic_tool_definition.swift", + "Sources/PromptyModel/wire/anthropic_tool_result_block.swift", + "Sources/PromptyModel/wire/anthropic_tool_use_block.swift", + "Sources/PromptyModel/wire/anthropic_usage.swift", + "Sources/PromptyModel/wire/anthropic_wire_message.swift", + "Sources/PromptyModel/wire_conformance.swift" + ] + }, { "target": "typescript", "outputRoot": "../runtime/typescript/packages/core/src/model", diff --git a/schema/tsp-output/.typra-generated/hydration-seams.json b/schema/tsp-output/.typra-generated/hydration-seams.json index 5f84f2dfe..2d2d7d880 100644 --- a/schema/tsp-output/.typra-generated/hydration-seams.json +++ b/schema/tsp-output/.typra-generated/hydration-seams.json @@ -724,6 +724,150 @@ "generatedSource": "wire_conformance", "seamKind": "protocol-adapter" }, + { + "contract": "CheckpointStore", + "target": "swift", + "group": "", + "symbol": "CheckpointStore", + "generatedSource": "Sources/PromptyModel/checkpoint_store.swift", + "seamKind": "protocol-adapter" + }, + { + "contract": "DiscoveryConformance", + "target": "swift", + "group": "", + "symbol": "DiscoveryConformance", + "generatedSource": "Sources/PromptyModel/discovery_conformance.swift", + "seamKind": "protocol-adapter" + }, + { + "contract": "EngineDurabilityPort", + "target": "swift", + "group": "", + "symbol": "EngineDurabilityPort", + "generatedSource": "Sources/PromptyModel/engine_durability_port.swift", + "seamKind": "protocol-adapter" + }, + { + "contract": "EnginePermissionPort", + "target": "swift", + "group": "", + "symbol": "EnginePermissionPort", + "generatedSource": "Sources/PromptyModel/engine_permission_port.swift", + "seamKind": "protocol-adapter" + }, + { + "contract": "EnginePostCommitPort", + "target": "swift", + "group": "", + "symbol": "EnginePostCommitPort", + "generatedSource": "Sources/PromptyModel/engine_post_commit_port.swift", + "seamKind": "protocol-adapter" + }, + { + "contract": "EngineToolPort", + "target": "swift", + "group": "", + "symbol": "EngineToolPort", + "generatedSource": "Sources/PromptyModel/engine_tool_port.swift", + "seamKind": "protocol-adapter" + }, + { + "contract": "EventJournalWriter", + "target": "swift", + "group": "", + "symbol": "EventJournalWriter", + "generatedSource": "Sources/PromptyModel/event_journal_writer.swift", + "seamKind": "protocol-adapter" + }, + { + "contract": "EventSink", + "target": "swift", + "group": "", + "symbol": "EventSink", + "generatedSource": "Sources/PromptyModel/event_sink.swift", + "seamKind": "protocol-adapter" + }, + { + "contract": "Executor", + "target": "swift", + "group": "", + "symbol": "Executor", + "generatedSource": "Sources/PromptyModel/executor.swift", + "seamKind": "protocol-adapter" + }, + { + "contract": "HostToolExecutor", + "target": "swift", + "group": "", + "symbol": "HostToolExecutor", + "generatedSource": "Sources/PromptyModel/host_tool_executor.swift", + "seamKind": "protocol-adapter" + }, + { + "contract": "LoadConformance", + "target": "swift", + "group": "", + "symbol": "LoadConformance", + "generatedSource": "Sources/PromptyModel/load_conformance.swift", + "seamKind": "protocol-adapter" + }, + { + "contract": "ModelLister", + "target": "swift", + "group": "", + "symbol": "ModelLister", + "generatedSource": "Sources/PromptyModel/model_lister.swift", + "seamKind": "protocol-adapter" + }, + { + "contract": "Parser", + "target": "swift", + "group": "", + "symbol": "Parser", + "generatedSource": "Sources/PromptyModel/parser.swift", + "seamKind": "protocol-adapter" + }, + { + "contract": "PermissionResolver", + "target": "swift", + "group": "", + "symbol": "PermissionResolver", + "generatedSource": "Sources/PromptyModel/permission_resolver.swift", + "seamKind": "protocol-adapter" + }, + { + "contract": "Processor", + "target": "swift", + "group": "", + "symbol": "Processor", + "generatedSource": "Sources/PromptyModel/processor.swift", + "seamKind": "protocol-adapter" + }, + { + "contract": "Renderer", + "target": "swift", + "group": "", + "symbol": "Renderer", + "generatedSource": "Sources/PromptyModel/renderer.swift", + "seamKind": "protocol-adapter" + }, + { + "contract": "TurnConformance", + "target": "swift", + "group": "", + "symbol": "TurnConformance", + "generatedSource": "Sources/PromptyModel/turn_conformance.swift", + "seamKind": "protocol-adapter" + }, + { + "contract": "WireConformance", + "target": "swift", + "group": "", + "symbol": "WireConformance", + "generatedSource": "Sources/PromptyModel/wire_conformance.swift", + "seamKind": "protocol-adapter" + }, { "contract": "CheckpointStore", "target": "typescript", diff --git a/schema/tsp-output/.typra-generated/manifest.json b/schema/tsp-output/.typra-generated/manifest.json index 2eb90857b..888e61486 100644 --- a/schema/tsp-output/.typra-generated/manifest.json +++ b/schema/tsp-output/.typra-generated/manifest.json @@ -6173,6 +6173,1351 @@ "path": "../runtime/rust/prompty/tests/model/wire/mod.rs", "marker": true }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Package.swift", + "marker": false + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/agent.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/checkpoint_store.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Connectivity/authorization_code_flow.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Connectivity/connection.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Connectivity/device_authorization.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Connectivity/o_auth_token.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Conversation/content_part.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Conversation/message.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Conversation/thread_marker.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Conversation/tool_call.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Conversation/tool_result.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Core/file_not_found_error.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Core/invoker_error.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Core/property.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Core/validation_error.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Core/validation_result.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/checkpoint.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/compaction_complete_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/compaction_failed_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/compaction_start_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/done_event_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/error_event_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/harness_context.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/hook_end_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/hook_start_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/host_tool_request.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/host_tool_result.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/llm_complete_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/llm_start_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/messages_updated_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/permission_completed_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/permission_decision.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/permission_request.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/permission_requested_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/redacted_field.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/redaction_metadata.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/retry_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_end_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_event.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_file_ref.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_ref.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_start_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_summary.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_trace.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/session_warning_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/status_event_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/stream_chunk.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/thinking_event_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/token_event_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/tool_call_complete_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/tool_call_start_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/tool_execution_complete_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/tool_execution_start_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/tool_result_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/trajectory_event.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/turn_end_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/turn_event.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/turn_start_payload.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/turn_summary.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Events/turn_trace.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Guardrails/guardrail_result.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Memory/memory_entry.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Memory/memory_store.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/ai_resource_info.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/invocation_usage.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/model_info.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/model_options.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/model.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/project_info.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/subscription_info.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Models/token_usage.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/compaction_config.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/context_candidate.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/context_request.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/delegated_state_reference.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/engine_checkpoint.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/engine_event.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/engine_permission_decision.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/final_output_policy_request.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/final_output_policy_result.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/host_policy_request.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/host_policy_result.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/invocation_context_decision.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/invocation_context_state.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_invocation_context_snapshot.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_invocation_request.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_invocation_response.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_reconciliation_state.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_tool_request.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/model_tool_result.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/replay_journal_record.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/replay_mismatch.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/replay_verification_request.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/replay_verification_result.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/resume_context.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/retry_policy_request.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/run_turn_request.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/run_turn_result.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/turn_commit.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/turn_engine_result.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/turn_model_request.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/turn_model_response.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Pipeline/turn_options.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Streaming/stream_options.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Templates/format_config.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Templates/parser_config.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Templates/template.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tooling/binding.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tooling/mcp_approval_mode.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tooling/tool_context.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tooling/tool_dispatch_result.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tooling/tool.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tracing/trace_file.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tracing/trace_span.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Contracts/Tracing/trace_time.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/discovery_conformance.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/engine_durability_port.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/engine_permission_port.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/engine_post_commit_port.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/engine_tool_port.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/event_journal_writer.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/event_sink.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/executor.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/host_tool_executor.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/load_conformance.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/model_lister.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/parser.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/permission_resolver.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/processor.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/renderer.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/turn_conformance.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/TypraRuntime.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/wire_conformance.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_image_block.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_image_source.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_messages_request.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_messages_response.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_text_block.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_tool_definition.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_tool_result_block.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_tool_use_block.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_usage.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Sources/PromptyModel/Wire/Anthropic/anthropic_wire_message.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/AgentTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/ConformanceTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Connectivity/AuthorizationCodeFlowTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Connectivity/ConnectionTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Connectivity/DeviceAuthorizationTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Connectivity/OAuthTokenTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Conversation/ContentPartTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Conversation/MessageTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Conversation/ThreadMarkerTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Conversation/ToolCallTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Conversation/ToolResultTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Core/FileNotFoundErrorTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Core/InvokerErrorTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Core/PropertyTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Core/ValidationErrorTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Core/ValidationResultTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/CheckpointTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/CompactionCompletePayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/CompactionFailedPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/CompactionStartPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/DoneEventPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ErrorEventPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/HarnessContextTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/HookEndPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/HookStartPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/HostToolRequestTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/HostToolResultTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/LlmCompletePayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/LlmStartPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/MessagesUpdatedPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/PermissionCompletedPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/PermissionDecisionTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/PermissionRequestedPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/PermissionRequestTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/RedactedFieldTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/RedactionMetadataTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/RetryPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionEndPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionEventTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionFileRefTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionRefTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionStartPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionSummaryTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionTraceTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/SessionWarningPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/StatusEventPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/StreamChunkTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ThinkingEventPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TokenEventPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ToolCallCompletePayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ToolCallStartPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ToolExecutionCompletePayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ToolExecutionStartPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/ToolResultPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TrajectoryEventTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TurnEndPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TurnEventTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TurnStartPayloadTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TurnSummaryTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Events/TurnTraceTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Guardrails/GuardrailResultTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Memory/MemoryEntryTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Memory/MemoryStoreTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/AiResourceInfoTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/InvocationUsageTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/ModelInfoTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/ModelOptionsTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/ModelTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/ProjectInfoTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/SubscriptionInfoTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Models/TokenUsageTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/CompactionConfigTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ContextCandidateTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ContextRequestTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/DelegatedStateReferenceTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/EngineCheckpointTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/EngineEventTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/EnginePermissionDecisionTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/FinalOutputPolicyRequestTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/FinalOutputPolicyResultTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/HostPolicyRequestTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/HostPolicyResultTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/InvocationContextDecisionTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/InvocationContextStateTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelInvocationContextSnapshotTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelInvocationRequestTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelInvocationResponseTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelReconciliationStateTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelToolRequestTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ModelToolResultTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ReplayJournalRecordTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ReplayMismatchTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ReplayVerificationRequestTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ReplayVerificationResultTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/ResumeContextTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/RetryPolicyRequestTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/RunTurnRequestTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/RunTurnResultTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/TurnCommitTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/TurnEngineResultTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/TurnModelRequestTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/TurnModelResponseTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Pipeline/TurnOptionsTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Streaming/StreamOptionsTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Templates/FormatConfigTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Templates/ParserConfigTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Templates/TemplateTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tooling/BindingTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tooling/McpApprovalModeTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tooling/ToolContextTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tooling/ToolDispatchResultTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tooling/ToolTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tracing/TraceFileTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tracing/TraceSpanTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Contracts/Tracing/TraceTimeTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicImageBlockTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicImageSourceTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicMessagesRequestTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicMessagesResponseTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicTextBlockTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicToolDefinitionTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicToolResultBlockTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicToolUseBlockTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicUsageTests.swift", + "marker": true + }, + { + "outputRoot": "../runtime/swift/prompty-model", + "path": "../runtime/swift/prompty-model/Tests/PromptyModelTests/Wire/Anthropic/AnthropicWireMessageTests.swift", + "marker": true + }, { "outputRoot": "../runtime/typescript/packages/core/src", "path": "../runtime/typescript/packages/core/src/eslint.config.js", diff --git a/schema/tspconfig.yaml b/schema/tspconfig.yaml index 9ee7d7f79..af956bf3e 100644 --- a/schema/tspconfig.yaml +++ b/schema/tspconfig.yaml @@ -34,5 +34,18 @@ options: output-dir: "../runtime/rust/prompty/src/model" test-dir: "../runtime/rust/prompty/tests/model" import-path: "prompty::model" + - type: Swift + output-dir: "../runtime/swift/prompty-model" + test-dir: "../runtime/swift/prompty-model/Tests/PromptyModelTests" + package-name: "PromptyModel" + # swift-format is not installed on the schema-repro-check regen runner + # (it has no Swift toolchain), and the emitter's Swift driver silently + # falls back to its own deterministic formatting when swift-format is + # absent (the shell-out is wrapped in a catch that prints no warning). + # That makes a swift-format-formatted tree unreproducible: any machine + # without swift-format regenerates the compact fallback and the regen + # gate reports false drift. Disable the formatter so the emitter's + # deterministic output is the single source of truth on every platform. + format: false - type: markdown output-dir: "../web/src/content/docs/reference"