Skip to content

Fix C# type conflicts between hand-written and generated partial classes#346

Merged
sethjuarez merged 85 commits into
mainfrom
sethjuarez/spec-type-system-overhaul
Apr 17, 2026
Merged

Fix C# type conflicts between hand-written and generated partial classes#346
sethjuarez merged 85 commits into
mainfrom
sethjuarez/spec-type-system-overhaul

Conversation

@sethjuarez

Copy link
Copy Markdown
Member

Summary

Resolves type conflicts in the Prompty C# runtime where hand-written types in Types.cs, Pipeline.cs, and Guardrails.cs conflicted with generated partial class types in the Model/ directory (33 compilation errors).

Changes

Types.cs — removed duplicate type definitions

  • Removed ContentPart, TextPart, ImagePart, FilePart, AudioPart, Message (all duplicated by generated Model/*.cs)
  • Kept Roles, RichKinds, ThreadMarker (unique, not generated)

Pipeline.cs — removed duplicate ToolCall

  • Removed ToolCall class (duplicated by Model/ToolCall.cs)
  • Kept ToolCallResult and ExecuteError

Guardrails.cs — removed duplicate GuardrailResult

  • Removed record GuardrailResult(...) (conflicts with Model/GuardrailResult.cs)
  • Added Model/GuardrailResultExtensions.cs with a convenience constructor GuardrailResult(bool, string?, object?) to preserve existing call-site syntax

Metadata null safety

The generated Message.Metadata is IDictionary<string, object>? (nullable), while the old hand-written type used non-null Dictionary<string, object?>. Fixed all null-unsafe accesses:

  • ContextWindow.cs — 3 Metadata.TryGetValue() calls → Metadata is not null &&
  • Pipeline.cs — 2 new Dictionary<>(msg.Metadata) → null-checked
  • PromptyChatParser.csmessage.Metadata[...]Metadata ??= new Dictionary<>()
  • WireFormat.cs — 4 Metadata.TryGetValue() calls → null-checked
  • AnthropicExecutor.cs — 1 Metadata.TryGetValue() call → null-checked

IList vs List compatibility

  • WireFormat.BuildContentParts() parameter: List<ContentPart>IList<ContentPart> (matches generated Message.Parts type)

Test fixes

  • Metadata = new() { ... }Metadata = new Dictionary<string, object> { ... } (cannot target-type new() on IDictionary interface)
  • Rewrite: (PascalCase record param) → rewrite: (lowercase constructor param)
  • Metadata.ContainsKey()Metadata?.ContainsKey() ?? false

Verification

  • Full solution builds: 0 errors
  • All tests pass: 564 Core + 231 Provider (44 integration skipped)

Comment thread schema/emitter/src/languages/csharp/test-emitter.ts Fixed
Comment thread schema/emitter/src/languages/typescript/driver.ts Fixed
Comment thread schema/emitter/src/languages/typescript/driver.ts Fixed
@sethjuarez
sethjuarez force-pushed the sethjuarez/spec-type-system-overhaul branch 2 times, most recently from d550a5d to f07f00d Compare April 16, 2026 14:32
sethjuarez and others added 27 commits April 16, 2026 08:08
…e spec

Phase 1-3 of the spec & type system overhaul:

TypeSpec additions (schema/model/):
- message.tsp: Message, Role, ContentPart hierarchy, ToolCall, ToolResult
- events.tsp: AgentEventType, StreamChunk hierarchy
- guardrails.tsp: GuardrailResult, GuardrailPhase
- discovery.tsp: ModelInfo for provider model listing

TypeSpec changes:
- tools/prompty.tsp: Remove mode field and promptyToolMode alias
  (PromptyTool is always single-shot)

Emitter changes:
- Add additional-roots config option for standalone types not
  reachable from the root Prompty model
- Update filterNodes to include additional model trees

spec.md changes:
- §6.5: Reference TypeSpec for Message/ContentPart types, add ToolResult docs
- §9.2: Tool handlers return ToolResult (not string)
- §9.4: FormatToolMessages handles ContentPart[] in tool results
- §9.5: Rich content examples for OpenAI and Anthropic wire formats
- §9.7: PromptyTool is single-shot only, MUST NOT run agent loop
- §13.1: Reference TypeSpec AgentEventType, tool_result carries ToolResult
- §13.5: Add steering principle and normative requirements
- §15: New section for model discovery convention (SHOULD, not MUST)

Generated code regenerated for all runtimes (TS, Python, C#, Rust, Go).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ping, stale Rust tests

Three emitter bugs fixed:

1. Python save_parts serialization: Collections of types without a 'name'
   property (ContentPart[], StreamChunk[]) were incorrectly using object-format
   serialization, causing roundtrip failures. Added hasNameProperty guard to
   save methods in Python template (matching existing TS/C# behavior).

2. Test string escaping: Python and C# escapeString were no-ops, causing
   syntax errors in generated tests when @sample values contained double
   quotes (e.g., ToolCall.arguments with JSON). Now properly escape
   backslashes and quotes.

3. Stale Rust test files: Polymorphic child types (PromptyTool, FunctionTool,
   etc.) no longer get individual test files (tested via parent's enum), but
   old generated files persisted on disk. Deleted 13 stale files.

Also fixed load_vectors.rs to remove reference to deleted PromptyTool.mode field.

Test results: Python 254/254, TypeScript 334/334, Rust 549/549.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… generation

Add verb enforcement infrastructure to the TypeSpec emitter:

- @factory decorator: generates static factory methods (fully generated)
  - GuardrailResult: allow(), deny(reason), rewrite(value)
  - All 4 runtimes: Rust, TypeScript, Python, C#

- @Helper decorator: generates trait/interface stubs (hand-implemented)
  - Message: text() helper trait in Rust
  - Other runtimes: pending stub generation

- C# factory method name collision detection (Rewrite prop → CreateRewrite method)
- ToolResult.fromText deferred to extension files (too language-specific)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…across all runtimes

Remove duplicative GuardrailResult definitions from all 4 runtimes and replace
with the TypeSpec-emitted model type, using generated factory methods everywhere.

Rust:
- Remove struct+impl from guardrails.rs, re-export from crate::model
- All 538 tests passing

TypeScript:
- Remove interface from core/guardrails.ts, import generated class
- Update all callsites to use GuardrailResult.allow(), .deny(), .rewrite()
- Add type assertions for rewrite (unknown vs Message[]) in pipeline.ts
- All 371 tests passing

Python:
- Remove dataclass from core/guardrails.py, import from prompty.model
- Add __post_init__ to fix rewrite field/classmethod name collision
- Update all callsites to factory methods
- All 316 tests passing (1 pre-existing anthropic skip)

C#:
- Remove hand-written record from Guardrails.cs
- Update to Allow(), Deny(), CreateRewrite() factory methods
- Pre-existing duplicate type errors in Types.cs unchanged

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…s with create_

In Python, a @classmethod with the same name as a dataclass field shadows
the field's default value. The emitter now pre-computes safe factory method
names: if a factory name (in snake_case) matches any field name, it gets
prefixed with 'create_' (e.g., rewrite -> create_rewrite).

This mirrors the existing C# collision detection that prefixes with 'Create'.
Rust and TypeScript don't need this fix (methods/statics don't collide with
fields in those languages).

Changes:
- schema/emitter/src/ast.ts: Add factoryNameMap to PythonClassContext
- schema/emitter/src/python.ts: Compute safe names in buildClassContext()
- schema/emitter/src/templates/python/file.py.njk: Use factoryNameMap
- runtime/python: Update 3 callsites from .rewrite() to .create_rewrite()
- runtime/python: Remove __post_init__ workaround (no longer needed)

Verified: Rust 538, TypeScript 371, Python 414 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…TypeScript, Python

Remove duplicate ToolCall definitions from types.rs (Rust), types.ts (TypeScript),
and processor.py (Python). Each runtime now re-exports the generated ToolCall from
the model layer. C# was already using the generated type.

- Rust: pub use crate::model::ToolCall in types.rs (538 tests pass)
- TypeScript: re-export from model/tool-call.ts in core/types.ts (743 tests pass)
- Python: import from model in processor.py, re-export for backward compat (909 tests pass)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace camelCase attribute accesses with snake_case equivalents to
match the updated agentschema model types:
- agent.model.apiType -> agent.model.api_type
- conn.apiKey -> conn.api_key

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update test files to use snake_case field names matching the regenerated
agentschema model types:
- apiType -> api_type
- apiKey -> api_key
- maxOutputTokens -> max_output_tokens
- serverName -> server_name
- additionalProperties -> additional_properties

Dict key accesses (YAML/JSON keys) remain camelCase as intended.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix Python emitter to generate snake_case field names (was camelCase)
  - file.py.njk: snakeCase filter on field decls, collection methods, save access, factories
  - _macros.njk: snakeCase filter on setInstance/saveProperty field access
  - test-context.ts: Python renderKey now uses toSnakeCase
- Update all Python runtime code for snake_case model attributes:
  - providers/openai/executor.py (29 changes)
  - providers/anthropic/executor.py (11 changes)
  - providers/foundry/executor.py (6 changes)
  - 6 test files updated
- Add PromptyTool.mode field to TypeSpec (alias: 'single' | 'agentic')
  - Enables declarative sub-agent delegation via mode: agentic
  - Default: 'single' (single-shot invoke, no agent loop)
  - Regenerated across all 4 runtimes + schemas + docs
- Clean up test_spec_vectors.py: filter extension vectors at
  parametrization level instead of pytest.skip() inside test body
  - Eliminates 17 spurious SKIPPED results

Python: 989 passed, 0 failed, 0 skipped

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…erde

The ToolCall struct was swapped to the emitted model type which doesn't
derive serde::Serialize. Updated test to use the emitted to_value()
method with SaveContext instead of serde_json::to_value().

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
to_dict() used hasattr(obj, 'model_dump') to detect Pydantic models,
but MagicMock fakes every attribute. This caused recursive model_dump()
calls on mocks, each spawning new mocks — ~4s per test.

Fix: check hasattr(type(obj), 'model_dump') instead, which only matches
real Pydantic classes where model_dump is defined on the class.

Result: 989 tests in 10.73s (was ~10 minutes).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…odel

core/types.py no longer defines ContentPart, TextPart, ImagePart,
FilePart, AudioPart, or Message. Instead it re-exports the generated
types from prompty.model and attaches runtime helpers (.text property,
.to_text_content() method, metadata default) via monkey-patching.

Zero import changes needed at any consumer site — all code that does
'from prompty.core.types import Message' still works identically.

989 tests pass in 10.47s.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove hand-written ContentPart, TextPart, ImagePart, FilePart, AudioPart,
Message from Types.cs and ToolCall from Pipeline.cs — these are now provided
by the generated classes in Model/.

Key changes:
- Make generated Message partial; add MessageHelpers.cs with Text property
  and ToTextContent() convenience method
- Change Message.Metadata to IDictionary<string, object?> with non-null
  default (matching existing usage patterns)
- Change Message.Role default to "user" (was string.Empty)
- Fix WireFormat.cs BuildContentParts param: List -> IList
- Fix test files using Metadata = new() (interfaces need explicit ctor)
- Fix ToolCallConversionTests.cs string literal encoding (curly quotes)
- Fix GuardrailResult.cs null check on non-nullable bool (pre-existing)

All 836 tests pass (792 succeeded, 44 skipped — missing API keys).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Migrate all Message instantiations from positional args
(role, parts, metadata) to the new generated class constructor
taking a Partial<Message> init object.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Migrate all 6 Message constructor calls from positional args
(role, parts, metadata) to the new single options object pattern
({ role, parts, metadata }).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Migrate Message and ContentPart construction to the new Partial<T> init
object pattern from the generated model classes:
- new Message(role, parts, metadata) -> new Message({ role, parts, metadata })
- { kind: 'text', value } -> new TextPart({ value })
- { kind: 'image', source } -> new ImagePart({ source })

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ntentPart classes

- Change Message constructor from positional args to options object pattern
- Replace plain object content part literals with TextPart, ImagePart, AudioPart instances
- Add TextPart, ImagePart, AudioPart to imports from @prompty/core

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Migrate all Message instantiations from positional args to the new
Partial<Message> init object pattern:
- new Message(role, parts) → new Message({ role, parts })
- new Message(role, parts, metadata) → new Message({ role, parts, metadata })

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update compaction, resilience, registry, and anthropic e2e tests
to use the options-object Message constructor and proper ContentPart
class instances instead of plain object literals.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…types

Replace hand-written ContentPart/TextPart/ImagePart/FilePart/AudioPart
interfaces and Message class in core/types.ts with re-exports from the
auto-generated model/ directory. Add Message prototype augmentation for
text getter and toTextContent() method.

Key changes:
- types.ts: Re-export generated model types, add module augmentation
- types.ts helpers: text(), textMessage(), dictToMessage(),
  dictContentToPart() now return class instances instead of plain objects
- index.ts: Remove 'type' keyword from ContentPart exports (now classes)
- pipeline.ts, steering.ts, context.ts, parsers/prompty.ts: Update
  Message constructor from positional to Partial<Message> init object
- openai/executor.ts, anthropic/executor.ts: Same constructor updates
- openai/wire.ts, anthropic/wire.ts: Add explicit casts for ContentPart
  subclass properties (TextPart, ImagePart, etc.) since generated
  abstract class uses kind: string (no discriminated union narrowing)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The loadParts template unconditionally assumed all collection element
types have a 'name' field (dict-to-array pattern). This broke for
ContentPart collections in Message and ToolResult. Added the same
{% if collection.hasNameProperty %} guard that Python and C# already
use. Types without 'name' now only accept array format.

Regenerated all model files. TS core: 744 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…json::Value accessors

The emitter now generates typed fields (Vec<Property>, Vec<Tool>, Vec<Binding>)
on the Prompty struct instead of serde_json::Value with accessor methods. Updated
all call sites across the workspace:

- pipeline.rs: replace as_inputs/as_outputs/as_tools with direct field access
- renderers/common.rs: iterate &agent.inputs directly
- tool_dispatch.rs: change ToolHandlerTrait to accept &Tool, rewrite helpers
- prompty-openai wire.rs/processor.rs: typed tools, outputs, parameters
- prompty-anthropic wire.rs/processor.rs: same patterns
- loader_test.rs, load_vectors.rs: update test assertions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ull safety

- TypeSpec: add default 'user' for Message.role (propagates to all runtimes)
- C# emitter: use verbatim strings for sample values containing quotes
- C# runtime: null-safe Metadata access in Pipeline, PromptyChatParser,
  WireFormat, ContextWindow, AnthropicExecutor
- C# test: null-safe Metadata check in ParserTests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove PromptyToolMode and mode field from TypeSpec schema
- Regenerate all runtimes via emitter
- Wire ToolResult (with Vec<ContentPart>) through entire Rust pipeline:
  - ToolHandler/ToolFn/AsyncToolFn return ToolResult instead of String
  - dispatch_tool returns ToolResult
  - format_tool_messages takes &[ToolResult]
  - AgentEvent::ToolResult carries ToolResult
- Add From<String>, From<&str>, from_text(), text(), to_message_parts() on ToolResult
- Update prompty-openai wire format to handle rich content parts
- Update prompty-anthropic wire format for rich tool results
- Update prompty-foundry executor signature
- PromptyToolHandler always uses invoke() (single-shot), no mode branch
- Re-export ToolResult from lib.rs
- All 538 Rust tests pass

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…d type augmentation

- Replace module augmentation (declare module + Object.defineProperty) with
  standalone utility functions: messageText(), messageToTextContent(),
  toolResultText(), textToolResult()
- Update ToolHandler.executeTool() return type: string → ToolResult
- Update dispatchTool() and formatToolMessages() signatures across all packages
- Change ToolCall construction from object literals to new ToolCall({...})
- Fix metadata optional access patterns (msg.metadata → msg.metadata ?? {})
- Add default case to partToWire switch statements (openai, anthropic)
- Remove mode field from PromptyTool spec vectors (load_vectors.json)
- Update all test files to use standalone utility functions
- All 884 tests pass

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ptyToolHandler

- Changed all tool handler return types from str to ToolResult
- Added standalone helpers: text_tool_result(), tool_result_text(), to_tool_result()
- Removed mode branch from PromptyToolHandler (always single-shot invoke)
- Updated format_tool_messages in OpenAI/Anthropic to accept list[ToolResult]
- Updated pipeline dispatch functions to return list[ToolResult]
- Updated all test assertions for ToolResult-aware comparisons
- 989 tests pass, ruff clean

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…omptyToolHandler

- Add ToolResultHelpers.cs partial class with FromText(), Text property,
  and implicit string-to-ToolResult conversion
- Change ToolHandler delegate: Task<string> → Task<ToolResult>
- Update ToolDispatch registries and DispatchAsync return type
- Update Pipeline.cs tool dispatch loop (parallel + sequential)
- Update ToolAttribute DiscoverTools/BindTools handler return types
- Update OpenAI/Anthropic FormatToolMessages: List<string> → List<ToolResult>
- Update all test files: mock executors, tool handler dicts, assertions
- All 792 C# tests pass (561 core + 231 provider)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
sethjuarez and others added 9 commits April 16, 2026 08:08
Regenerated output from tsp compile after Go emitter changes.
No behavioral changes — existing tests pass (261 Python, 341 TypeScript).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add new TypeSpec models for types previously only in spec.md prose:
- pipeline.tsp: TurnOptions, CompactionConfig
- usage.tsp: TokenUsage
- events.tsp: 10 typed event payload models (Token, Thinking, ToolCallStart, ToolResult, Status, MessagesUpdated, Done, Error, CompactionComplete, CompactionFailed)
- tools.tsp: ToolContext, ToolDispatchResult

All types are generated into 5 runtimes (Python, TypeScript, Rust, C#, Go).
All 6 test suites pass (IR:122, Python:331, TS:457, C#:652, Go:pass, Rust:pass).

Intentionally excluded errors.tsp — error types need language-specific
exception base classes, making them a poor fit for code generation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add provider parameter to @knownas and @defaultFor decorators
- Create wire/openai.tsp with OpenAI Chat Completions API field mappings
- Create wire/anthropic.tsp with Anthropic Messages API field mappings
- Wire knownAs/defaultFor through PropertyNode in AST
- Add WireDecl/WireFieldMapping to Declaration IR
- Add lowerWire() pass to build wire declarations from knownAs data
- Generate toWire(provider) methods in all 5 language emitters:
  - TypeScript: toWire(provider: string): Record<string, unknown>
  - Python: to_wire(self, provider: str) -> dict[str, Any]
  - C#: ToWire(string provider): Dictionary<string, object?>
  - Go: ToWire(provider string) map[string]interface{}
  - Rust: to_wire(&self, provider: &str) -> serde_json::Value
- Add serde::Serialize derive for Rust structs with wire mappings
- Generated toWire() on ModelOptions and TokenUsage in all runtimes
- All 6 test suites pass (IR:122, Python:331, TS:457, Go, C#, Rust)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…cript

Replace manual ModelOptions field mapping in buildOptions/buildResponsesOptions with the generated ModelOptions.toWire(provider) method in both OpenAI and Anthropic wire.ts files. Update Anthropic buildChatArgs to use toWire result for max_tokens with DEFAULT_MAX_TOKENS fallback. Fix Anthropic e2e tests to use ModelOptions instances instead of plain objects.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…on executors

Replace manual ModelOptions field mapping in _build_options and
_build_responses_options with the generated to_wire() method across
OpenAI, Responses API, and Anthropic executors.

- OpenAI: _build_options uses to_wire('openai')
- Responses API: _build_responses_options uses to_wire('responses')
- Anthropic: _build_options uses to_wire('anthropic'), _build_chat_args
  simplified to use wire result for max_tokens default fallback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add protocol interface generation to 4 runtimes (TS, Python, Rust, Go):
- TypeScript: export interface with async methods
- Python: typing.Protocol class with sync + async variants
- Rust: #[async_trait] pub trait with Send + Sync bounds
- Go: interface type with method signatures

Infrastructure changes:
- @method decorator accepts optional params parameter for method signatures
- IR declarations.ts: isProtocol on TypeDecl, params on MethodStubDecl
- lower.ts: extractMethodTypeRefs() for protocol import resolution
- All 5 drivers skip test generation for protocol types
- C# driver skips protocol emission entirely (conflicts with hand-written Interfaces.cs)

Wire format fixes:
- Python toWire() now only maps fields with explicit provider mappings
- Added identity mappings in openai.tsp and anthropic.tsp for strict mode
- Added OpenAI Responses API wire mappings

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tions

to_wire() serializes None Option fields as JSON null. The old hand-written
code skipped None fields entirely. Without this fix, null values leak into
the request body causing vector test mismatches.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Rust/Go helpers

- Fix nullable protocol type imports in IR lowering (strip ? suffix before scalar check)
- Enable C# protocol generation (IRenderer, IParser, IExecutor, IProcessor)
- Migrate C# FormatToolMessages → FormatToolMessagesAsync across all implementations
- Extract IPreRenderable.cs from deleted Interfaces.cs
- Add Rust MessageHelpers + ToolResultHelpers trait implementations (model_ext.rs)
- Add Go MessageHelpers + ToolResultHelpers implementations (helpers.go)
- Add #![allow(unused_imports, dead_code)] to Rust generated headers
- Add nullable type support to all 5 protocol type mappers (C#, TS, Rust, Python, Go)

All 6 test suites pass: IR (122), TS (457), Python (331), Rust (587), C# (883), Go (235)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sethjuarez
sethjuarez force-pushed the sethjuarez/spec-type-system-overhaul branch from f07f00d to 9be0745 Compare April 16, 2026 15:09
sethjuarez and others added 11 commits April 16, 2026 08:14
- Run black on 90 generated Python model/test files
- Run dotnet format on generated C# model files
- Convert 3 sync test methods to async Task (xUnit1031 warning fix)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace regex-based MIME extraction with indexOf/slice to prevent ReDoS (high)
- Add backslash escaping in C# test string literals (medium)
- Replace all execSync shell commands with execFileSync across all drivers
  to prevent shell injection from paths (high + medium)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Python CI enforces 'ruff format --check', not 'black --check'.
Replace 'uv run black' with 'uv run ruff format' in the emitter's
post-compile formatting step so generated files pass CI automatically.

Also apply ruff format to the 18 files that were out of compliance.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…elds

The Python emitter now generates snake_case field names on dataclasses
(e.g. api_type, api_key, max_output_tokens, enum_values, server_name).
Update all hand-written runtime code and tests to use the new names.

Changes:
- providers/openai/executor.py: .apiType -> .api_type, .apiKey -> .api_key, .enumValues -> .enum_values
- providers/anthropic/executor.py: same pattern
- providers/foundry/executor.py: .apiType -> .api_type, .apiKey -> .api_key
- tests/test_loader.py: .apiType, .apiKey, .maxOutputTokens, .serverName
- tests/test_spec_vectors.py: .apiType, .apiKey (getattr), .maxOutputTokens, .serverName (getattr)
- tests/test_structured.py: .apiType -> .api_type
- tests/test_anthropic.py: .apiType -> .api_type

Dict key access (wire format) is intentionally unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add Dictionary<string, object?>? context parameter to all mock IParser
implementations in test files to match the updated IParser interface.
Also fix FormatToolMessages return type mismatches (return List<Message>
directly instead of wrapping in Task.FromResult).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…c support

- Extend @@method decorator with optional and sync flags
- Update all 5 language emitters for optional/sync protocol methods
- Add preRender (optional, sync) to Parser protocol
- Add context parameter to Parser.parse
- Mark formatToolMessages as sync (not async)
- Fix TS emitter: definite assignment (!) for required complex fields
- Update C# runtime: ParseAsync context param, FormatToolMessages sync
- All test suites green: Emitter 130, Python 1066, TS 60, Rust 587, C# 883

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ta fix

- Add executeStream (optional) to Executor protocol in TypeSpec
- Add processStream (optional) to Processor protocol in TypeSpec
- Add ThreadMarker model to message.tsp (replaces hand-written versions)
- Add toTextContent and text @@method stubs on Message
- Make Message.metadata non-optional with empty dict default
- Wire tsp format into schema build pipeline (format:tsp script)
- Fix Rust emitter: optional async defaults return Err not Ok(None)
- Fix C# ThreadMarker: use partial class to merge with generated
- Implement MessageHelpers.to_text_content() in Rust model_ext.rs
- All 6 runtimes + emitter tests passing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…urce

Replace hand-written Message, ContentPart, TextPart, ImagePart, FilePart,
AudioPart, and ThreadMarker classes in core/types.py with re-exports from
the generated model/ package. Add .text property and .to_text_content()
method via monkey-patching on the generated Message class.

This eliminates the duplicate type definitions while preserving the full
public API surface. All 1,071 Python tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e types

The Python emitter previously emitted a comment block for @method
declarations on concrete types. Now it emits a real <TypeName>Helpers
Protocol class with @runtime_checkable, proper signatures, and
docstrings from the @method decorator descriptions.

For Message this produces MessageHelpers(Protocol) with text() and
to_text_content(). Runtime code can be type-checked against this
contract. Matches what Rust and Go emitters already do.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… types

The TypeScript emitter previously emitted `// @method X(): T — desc''
comments inside each generated class for @method declarations. Now it
emits a real exported <TypeName>Helpers interface alongside the class,
with proper signatures and JSDoc comments from the @method decorator
descriptions.

For Message this produces `export interface MessageHelpers'' with
text() and toTextContent(). Runtime code annotated as MessageHelpers
will be structurally type-checked by the TS compiler.

Matches what Rust, Go, and now Python emitters emit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Emit a public partial interface IMessageHelpers / IToolResultHelpers
after each generated class with methods. The class declaration now
includes `: I<Name>Helpers` so the C# compiler enforces that a
hand-written partial class provides the declared members.

Zero-param methods are emitted as properties unless the name starts
with a verb prefix (to, get, fetch, compute, make, build, create,
load, save, convert, parse, format, render, serialize, deserialize,
find, calculate, invoke, execute, run). This matches existing runtime
conventions: Message.Text (property), Message.ToTextContent() (method).

Factory name collision detection was extended to also consider
@method names emitted as properties. For ToolResult, the factory
@factory(`text`, ...) collides with the property Text from
@method(`text`, ...), so the factory is renamed to CreateText.

- Replace comment-only #region Helpers body with a real interface
- Rename ToolResult.Text(string) factory to CreateText
- Rename TextContent helper to Text to satisfy IToolResultHelpers
- Update one test call site

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sethjuarez
sethjuarez force-pushed the sethjuarez/spec-type-system-overhaul branch from 4133952 to 18cf57d Compare April 17, 2026 03:11
Split the flat schema/model/ directory into semantically grouped subfolders
so contributors can navigate the TypeSpec sources by concept area:

  agent/       - Prompty agent + guardrails
  connection/  - Connection types
  core/        - base aliases + Property schema
  discovery/   - ModelInfo
  events/      - AgentEvent payloads + StreamChunk
  message/     - Message + ContentPart + ToolCall/Result
  model/       - Model + ModelOptions + TokenUsage
  pipeline/    - TurnOptions + CompactionConfig
  protocols/   - Renderer/Parser/Executor/Processor protocols
  template/    - Template + Format/Parser configs
  tools/       - base.tsp (Tool/FunctionTool/CustomTool/Binding/
                 ToolContext/ToolDispatchResult) + mcp/openapi/prompty
  wire/        - openai + anthropic knownAs mappings

Zero emitter changes: the emitter still writes flat output. The only
generated-code diffs are ordering changes in four barrel files (Python
__init__.py, Rust mod.rs + tests/model/main.rs, TypeScript index.ts)
caused by the new source-file iteration order. Functional contents are
identical.

Python tests: 1071 passed, 17 skipped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sethjuarez
sethjuarez force-pushed the sethjuarez/spec-type-system-overhaul branch from 99cc355 to 6534670 Compare April 17, 2026 04:25
sethjuarez and others added 4 commits April 16, 2026 21:44
- Rename message/ to conversation/ and split into 4 files (message, content, tool-invocation, thread)

- Merge protocols/ into pipeline/ as 4 per-protocol files (renderer, parser, executor, processor)

- Rename pipeline/pipeline.tsp to pipeline/turn.tsp

- Merge discovery/ into model/discovery.tsp

- Split tools/base.tsp into tools/tool.tsp + tools/dispatch.tsp; delete obsolete tools/main.tsp

- Split events/events.tsp into payloads.tsp + stream-chunks.tsp

- Split connection/connection.tsp: extract OAuthConnection and FoundryConnection into their own files

- Update main.tsp and all cross-file imports; regenerate runtimes (ordering-only diffs)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…istic

- Emitter: zero-param @method stubs with non-verb names now emit as`@property` in the generated `<Type>Helpers` Protocol (matches theC# emitter's `isMethodStyle` heuristic). Accessor-style helpers like`Message.text` stay idiomatic; verbs like `to_text_content` remainmethods.
- Emitter: `<Type>Helpers` Protocol classes are now exported from thegenerated `model/__init__.py` alongside their concrete types. This lets runtimes `isinstance(msg, MessageHelpers)` without reaching into private `_Message.py`.
- Runtime: `core/types.py` now imports `MessageHelpers` from the generated model, attaches `text` as a `property` (matching the Protocol), and adds a module-level `isinstance` assertion so the emitter contract is verified at import time.
- 1,071 Python tests still pass.
…lpers contract

Emitter: zero-param @method stubs with non-verb names now emit as`readonly <name>: <type>` in the generated `<Type>Helpers`interface (matches the C#/Python verb heuristic). Accessor-stylehelpers like `Message.text` stay idiomatic; verbs like`toTextContent` remain methods.

Runtime: hand-written `Message` class in `core/types.ts` now declares`implements MessageHelpers` so the TypeScript compiler enforces thegenerated contract. Full consolidation onto the generated class (deletinghand-written `class Message` and migrating positional `newMessage(role, parts)` call sites to named `newMessage({role, parts})`) is deferred to a follow-up — ~150 call sitesacross tests and providers.

All TS tests still pass.
…pages

The markdown emitter now renders the `@method` contract declarations and `@factory` helpers on each type's reference page. This makes the human-readable reference docs the canonical spec.md companion to the TypeSpec source, completing the Blank-Folder Test requirement that the implementation surface for each type is discoverable from the emitted output alone.

Reference pages for Message, ToolResult, Executor, Parser, Processor, Renderer, and GuardrailResult now include a Helper Methods table (name, signature, description, optional/sync flags) and a Factory Methods list.
@sethjuarez
sethjuarez merged commit 4e142da into main Apr 17, 2026
15 checks passed
@sethjuarez
sethjuarez deleted the sethjuarez/spec-type-system-overhaul branch April 17, 2026 06:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants