Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
28ef06b
Add turn harness contract and runtime events
sethjuarez Jun 10, 2026
f640f41
Add agent harness contracts
sethjuarez Jun 10, 2026
9060ceb
Tighten harness contract shapes
sethjuarez Jun 10, 2026
fd93931
Add runtime harness adapters
sethjuarez Jun 10, 2026
808c6c7
Rename harness trace writer to event journal
sethjuarez Jun 27, 2026
db429b3
Consume Typra emitter package
sethjuarez Jun 27, 2026
fca3f31
Bump Typra emitter to 0.2.5
sethjuarez Jun 27, 2026
fdbd3e3
Bump Typra emitter to 0.2.6
sethjuarez Jun 28, 2026
826fe3b
Bump Typra emitter to 0.2.7
sethjuarez Jun 28, 2026
08645e1
Bump Typra emitter to 0.3.0
sethjuarez Jun 28, 2026
949a4e1
Add OpenAI-compatible endpoint example
sethjuarez Jun 28, 2026
c3a8562
Add OpenAI-compatible endpoint docs example
sethjuarez Jun 28, 2026
2fd40b7
Fix generated reference docs frontmatter
sethjuarez Jun 28, 2026
283fd88
Bump Typra emitter to 0.3.1
sethjuarez Jun 28, 2026
341843b
Add reference turn runner
sethjuarez Jun 28, 2026
e420598
Add turn runner parity implementations
sethjuarez Jun 28, 2026
e7921b5
Move turn runner DTOs to TypeSpec
sethjuarez Jun 28, 2026
c34fb0a
Track Typra verifier metadata
sethjuarez Jun 28, 2026
fc90fea
Add replay verifier contracts
sethjuarez Jun 28, 2026
c52873c
Fix Typra verifier baseline inputs
sethjuarez Jun 28, 2026
0670050
Run Typra verifier on Windows
sethjuarez Jun 28, 2026
2648c46
Merge remote-tracking branch 'origin/main' into sejuare-microsoft-tur…
sethjuarez Jun 30, 2026
e797892
Address PR code quality findings
sethjuarez Jun 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,11 @@ __pycache__/

# Schema emitter build artifacts
schema/emitter/dist/
schema/tsp-output/
schema/tsp-output/*
!schema/tsp-output/.typra-generated/
!schema/tsp-output/.typra-generated/export-surfaces.json
!schema/tsp-output/.typra-generated/hydration-seams.json
!schema/tsp-output/.typra-generated/manifest.json
!schema/tsp-output/json-ast/
!schema/tsp-output/json-ast/model.json
.playwright-mcp/
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,46 @@ console.log(result);

**VS Code** — open the `.prompty` file and press **F5**.

### Use an OpenAI-compatible endpoint

Prompty's `openai` provider can also target OpenAI-compatible control planes,
gateways, or self-hosted model servers by setting `model.connection.endpoint`.
The prompt asset stays portable: switch the endpoint and key at runtime without
changing the prompt body.

```prompty
---
name: governed-greeting
model:
id: gpt-4o-mini
provider: openai
connection:
kind: key
endpoint: ${env:OPENAI_BASE_URL:https://api.openai.com/v1}
apiKey: ${env:OPENAI_API_KEY}
template:
format:
kind: jinja2
parser:
kind: prompty
---
system:
You are a careful assistant.

user:
Say hello to {{name}}.
```

For example, to route through Tuning Engines:

```bash
export OPENAI_BASE_URL=https://api.tuningengines.com/v1
export OPENAI_API_KEY=sk-te-your-inference-key
```

This keeps the `.prompty` file unchanged while the endpoint provides routing,
policy, usage tracking, or trace correlation around OpenAI-compatible calls.

## Contributor hygiene

Prompty normalizes text files to LF line endings via `.gitattributes`. Enable the
Expand Down
8 changes: 7 additions & 1 deletion runtime/csharp/Prompty.Core.Tests/AgentExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ public void EmitEvent_CallsCallback_WithCorrectArgs()
AgentEvents.EmitEvent(cb, AgentEventType.ToolCallStart, data);

Assert.Equal(AgentEventType.ToolCallStart, captured);
Assert.Same(data, capturedData);
Assert.NotNull(capturedData);
Assert.Equal("value", capturedData["key"]);
var turnEvent = Assert.IsType<Dictionary<string, object?>>(capturedData["turnEvent"]);
Assert.IsType<string>(turnEvent["id"]);
Assert.Equal("tool_call_start", turnEvent["type"]);
Assert.IsType<string>(turnEvent["timestamp"]);
Assert.Same(data, turnEvent["payload"]);
}

[Fact]
Expand Down
182 changes: 182 additions & 0 deletions runtime/csharp/Prompty.Core.Tests/HarnessAdaptersTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json;
using Prompty.Core;

namespace Prompty.Core.Tests;

public class HarnessAdaptersTests
{
[Fact]
public void CollectingEventSink_CapturesEvents()
{
var sink = new CollectingEventSink();

Assert.True(sink.EmitTurn(TurnEvent()));
Assert.True(sink.EmitSession(SessionEvent()));

Assert.Equal("turn-event", sink.TurnEvents.Single().Id);
Assert.Equal("session-event", sink.SessionEvents.Single().Id);
}

[Fact]
public void JsonlEventJournalWriter_WritesRecords()
{
var directory = Directory.CreateTempSubdirectory("prompty-trace-");
try
{
var path = Path.Combine(directory.FullName, "trace.jsonl");
var writer = new JsonlEventJournalWriter(path);

writer.AppendTurn(TurnEvent());
writer.AppendSession(SessionEvent());
writer.Close(new SessionSummary { SessionId = "session-1", Turns = 1 });

var lines = File.ReadAllLines(path)
.Select(line => JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(line)!)
.ToList();

Assert.Equal(new[] { "turn", "session", "summary" }, lines.Select(line => line["kind"].GetString()).ToArray());
Assert.Equal("turn-event", lines[0]["event"].GetProperty("id").GetString());
Assert.Equal("session-event", lines[1]["event"].GetProperty("id").GetString());
Assert.Equal("session-1", lines[2]["summary"].GetProperty("sessionId").GetString());
Assert.DoesNotContain("\r\n", File.ReadAllText(path));
}
finally
{
directory.Delete(recursive: true);
}
}

[Fact]
public void JsonlEventJournalWriter_ReturnsFalseAfterClose()
{
var directory = Directory.CreateTempSubdirectory("prompty-trace-");
try
{
var writer = new JsonlEventJournalWriter(Path.Combine(directory.FullName, "trace.jsonl"));

Assert.True(writer.Close(null));
Assert.False(writer.AppendTurn(TurnEvent()));
}
finally
{
directory.Delete(recursive: true);
}
}

[Fact]
public async Task InMemoryCheckpointStore_StoresCheckpoints()
{
var store = new InMemoryCheckpointStore();
var checkpoint = new Checkpoint { Id = "checkpoint-1", SessionId = "session-1", Title = "First" };

Assert.Same(checkpoint, await store.SaveAsync(checkpoint));
Assert.Same(checkpoint, await store.LoadAsync("session-1", "checkpoint-1"));
Assert.Null(await store.LoadAsync("session-1", "missing"));
Assert.Equal([checkpoint], await store.ListCheckpointsAsync("session-1"));
}

[Fact]
public async Task InMemoryCheckpointStore_RequiresKeys()
{
var store = new InMemoryCheckpointStore();

await Assert.ThrowsAsync<ArgumentException>(() =>
store.SaveAsync(new Checkpoint { Id = "checkpoint-1", Title = "Missing session" }));
await Assert.ThrowsAsync<ArgumentException>(() =>
store.SaveAsync(new Checkpoint { SessionId = "session-1", Title = "Missing id" }));
}

[Fact]
public async Task PermissionResolvers_ReturnDecisions()
{
var request = new PermissionRequest
{
RequestId = "permission-1",
ToolCallId = "tool-call-1",
Permission = "tool.execute"
};

var allow = await new AllowAllPermissionResolver().RequestAsync(request);
var deny = await new DenyAllPermissionResolver().RequestAsync(request);

Assert.True(allow.Approved);
Assert.Equal("allow_all", allow.Reason);
Assert.Equal("permission-1", allow.RequestId);
Assert.Equal("tool-call-1", allow.ToolCallId);
Assert.False(deny.Approved);
Assert.Equal("deny_all", deny.Reason);
}

[Fact]
public async Task FunctionHostToolExecutor_ExecutesRegisteredHandlers()
{
var executor = new FunctionHostToolExecutor(new Dictionary<string, HostToolHandler>
{
["add"] = (args, _) => Task.FromResult<object?>(Convert.ToInt32(args!["a"]) + Convert.ToInt32(args["b"]))
});

var result = await executor.ExecuteAsync(new HostToolRequest
{
RequestId = "exec-1",
ToolName = "add",
Arguments = new Dictionary<string, object> { ["a"] = 2, ["b"] = 3 }
});

Assert.True(result.Success);
Assert.Equal("exec-1", result.RequestId);
Assert.Equal("add", result.ToolName);
Assert.Equal(5, result.Result);
}

[Fact]
public async Task FunctionHostToolExecutor_PassesEmptyArguments()
{
var executor = new FunctionHostToolExecutor(new Dictionary<string, HostToolHandler>
{
["count"] = (args, _) => Task.FromResult<object?>(args.Count)
});

var result = await executor.ExecuteAsync(new HostToolRequest { ToolName = "count" });

Assert.True(result.Success);
Assert.Equal(0, result.Result);
}

[Fact]
public async Task FunctionHostToolExecutor_ReturnsFailureResults()
{
var executor = new FunctionHostToolExecutor(new Dictionary<string, HostToolHandler>
{
["fail"] = (_, _) => throw new InvalidOperationException("boom")
});

var missing = await executor.ExecuteAsync(new HostToolRequest { ToolName = "missing" });
var thrown = await executor.ExecuteAsync(new HostToolRequest { ToolName = "fail" });

Assert.False(missing.Success);
Assert.Equal("not_found", missing.ErrorKind);
Assert.False(thrown.Success);
Assert.Equal("exception", thrown.ErrorKind);
var result = Assert.IsType<Dictionary<string, object?>>(thrown.Result);
Assert.Equal("boom", result["message"]);
}

private static TurnEvent TurnEvent() => new()
{
Id = "turn-event",
Type = TurnEventType.TurnStart,
Timestamp = "2026-06-10T00:00:00Z",
Payload = new Dictionary<string, object> { ["phase"] = "start" }
};

private static SessionEvent SessionEvent() => new()
{
Id = "session-event",
Type = SessionEventType.SessionStart,
Timestamp = "2026-06-10T00:00:00Z",
SessionId = "session-1",
Payload = new Dictionary<string, object> { ["phase"] = "start" }
};
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// <auto-generated by typra-emitter>
using Xunit;

#pragma warning disable IDE0130
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// <auto-generated by typra-emitter>
using Xunit;

#pragma warning disable IDE0130
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// <auto-generated by typra-emitter>
using Xunit;

#pragma warning disable IDE0130
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// <auto-generated by typra-emitter>
using Xunit;

#pragma warning disable IDE0130
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// <auto-generated by typra-emitter>
using Xunit;

#pragma warning disable IDE0130
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// <auto-generated by typra-emitter>
using Xunit;

#pragma warning disable IDE0130
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// <auto-generated by typra-emitter>
using Xunit;

#pragma warning disable IDE0130
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// <auto-generated by typra-emitter>
using Xunit;

#pragma warning disable IDE0130
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// <auto-generated by typra-emitter>
using Xunit;

#pragma warning disable IDE0130
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// <auto-generated by typra-emitter>
using Xunit;

#pragma warning disable IDE0130
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// <auto-generated by typra-emitter>
using Xunit;

#pragma warning disable IDE0130
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// <auto-generated by typra-emitter>
using Xunit;

#pragma warning disable IDE0130
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// <auto-generated by typra-emitter>
using Xunit;

#pragma warning disable IDE0130
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// <auto-generated by typra-emitter>
using Xunit;

#pragma warning disable IDE0130
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// <auto-generated by typra-emitter>
using Xunit;

#pragma warning disable IDE0130
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// <auto-generated by typra-emitter>
using Xunit;

#pragma warning disable IDE0130
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// <auto-generated by typra-emitter>
using Xunit;

#pragma warning disable IDE0130
Expand Down
Loading
Loading