Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ namespace Microsoft.AgentHostProtocol;
[JsonSerializable(typeof(AnnotationsState))]
[JsonSerializable(typeof(AnnotationsSummary))]
[JsonSerializable(typeof(AnnotationsUpdatedAction))]
[JsonSerializable(typeof(AttributionPageSourceLocation))]
[JsonSerializable(typeof(AttributionResponsePart))]
[JsonSerializable(typeof(AttributionSource))]
[JsonSerializable(typeof(AttributionSourceLocation))]
[JsonSerializable(typeof(AttributionSourceLocationKind))]
[JsonSerializable(typeof(AttributionSpan))]
[JsonSerializable(typeof(AttributionTextSourceLocation))]
[JsonSerializable(typeof(AuthenticateParams))]
[JsonSerializable(typeof(AuthenticateResult))]
[JsonSerializable(typeof(AuthRequiredErrorData))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,8 @@ public enum ResponsePartKind
InputRequest,
[WireValue("error")]
Error,
[WireValue("attribution")]
Attribution,
}

/// <summary>Status of a tool call in the lifecycle state machine.</summary>
Expand All @@ -297,6 +299,16 @@ public enum ToolCallStatus
Cancelled,
}

/// <summary>The kind of location within an attribution source.</summary>
[JsonConverter(typeof(WireEnumConverter<AttributionSourceLocationKind>))]
public enum AttributionSourceLocationKind
{
[WireValue("text")]
Text,
[WireValue("page")]
Page,
}

/// <summary>How a tool call was confirmed for execution.
///
/// - `NotNeeded` — No confirmation required (auto-approved)
Expand Down Expand Up @@ -2631,6 +2643,111 @@ public sealed record ErrorResponsePart
public bool? Resumable { get; init; }
}

/// <summary>Sources that support an earlier markdown or reasoning part in the same turn.
///
/// The host appends this part with `chat/responsePart` after the target's last
/// text delta and before the turn ends. At most one attribution part may target
/// a given part. Neither the target text nor its attribution changes afterward,
/// including when the turn resumes; further output uses new part identifiers.
///
/// Clients MAY show inline citations or a source list. Clients that do not
/// support attribution can ignore this part and still render the original text.</summary>
public sealed record AttributionResponsePart
{
/// <summary>Discriminant</summary>
public ResponsePartKind Kind { get; init; }

/// <summary>Non-empty identifier, unique among response parts in this turn.</summary>
public required string Id { get; init; }

/// <summary>Identifier of the earlier MarkdownResponsePart or ReasoningResponsePart.</summary>
public required string TargetPartId { get; init; }

/// <summary>Supporting sources. MUST contain at least one entry, with distinct IDs.</summary>
public required List<AttributionSource> Sources { get; init; }

/// <summary>Ranges of target text linked to sources. An empty list supplies sources for
/// the target as a whole without claiming a more precise text-to-source mapping.</summary>
public required List<AttributionSpan> Spans { get; init; }

/// <summary>Optional implementation-specific details; not needed to display attribution.</summary>
[JsonPropertyName("_meta")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary<string, JsonElement>? Meta { get; init; }
}

/// <summary>A source, or a passage within a source, supporting an attributed response.
///
/// A source need not be a public URL or a local file. Hosts SHOULD provide a
/// title when no URI is available and prefer versioned URIs when possible.
/// Two entries may share a URI when they describe different passages.</summary>
public sealed record AttributionSource
{
/// <summary>Non-empty identifier, unique within the containing attribution part.</summary>
public required string Id { get; init; }

/// <summary>Human-readable source title, rendered as plain text.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Title { get; init; }

/// <summary>Source URI. Its presence does not authorize opening or fetching the resource.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Uri { get; init; }

/// <summary>MIME type of the source, when known.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ContentType { get; init; }

/// <summary>Optional short quotation from the source, not a generated answer summary.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Excerpt { get; init; }

/// <summary>Location within the source, not within the generated response.</summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public AttributionSourceLocation? Location { get; init; }

/// <summary>Optional implementation-specific details; not needed to display the source.</summary>
[JsonPropertyName("_meta")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary<string, JsonElement>? Meta { get; init; }
}

/// <summary>A non-empty range of generated text linked to supporting sources.
///
/// Ranges address the target part's raw `content`, before Markdown rendering:
/// zero-based lines and UTF-16 code-unit character offsets, start inclusive and
/// end exclusive. CRLF, LF, and lone CR each count as one line break. Positions
/// MUST lie within the text and MUST NOT split a surrogate pair.</summary>
public sealed record AttributionSpan
{
/// <summary>Range within the target part, not within any source or the combined turn.</summary>
public required TextRange Range { get; init; }

/// <summary>Non-empty, distinct IDs from the containing AttributionResponsePart.sources.</summary>
public required List<string> SourceIds { get; init; }
}

/// <summary>A range within a textual source, using the same position rules as AttributionSpan.</summary>
public sealed record AttributionTextSourceLocation
{
public AttributionSourceLocationKind Kind { get; init; }

/// <summary>Non-empty, start-inclusive, end-exclusive range within the source text.</summary>
public required TextRange Range { get; init; }
}

/// <summary>An inclusive range of pages within a document.</summary>
public sealed record AttributionPageSourceLocation
{
public AttributionSourceLocationKind Kind { get; init; }

/// <summary>First page, numbered from one.</summary>
public long StartPage { get; init; }

/// <summary>Last page, inclusive. MUST be greater than or equal to startPage.</summary>
public long EndPage { get; init; }
}

/// <summary>Tool execution result details, available after execution completes.</summary>
public sealed record ToolCallResult
{
Expand Down Expand Up @@ -5528,6 +5645,34 @@ public ResponsePartConverter()
["systemNotification"] = typeof(SystemNotificationResponsePart),
["inputRequest"] = typeof(InputRequestResponsePart),
["error"] = typeof(ErrorResponsePart),
["attribution"] = typeof(AttributionResponsePart),
},
allowUnknown: true)
{
}
}

/// <summary>A location within a source supporting an attributed response.</summary>
[JsonConverter(typeof(AttributionSourceLocationConverter))]
public sealed class AttributionSourceLocation : AhpUnion
{
/// <summary>Creates an empty AttributionSourceLocation (no active variant).</summary>
public AttributionSourceLocation() { }

/// <summary>Creates a AttributionSourceLocation wrapping the given variant value.</summary>
public AttributionSourceLocation(object? value) : base(value) { }
}

/// <summary>System.Text.Json converter for the AttributionSourceLocation discriminated union.</summary>
internal sealed class AttributionSourceLocationConverter : UnionConverter<AttributionSourceLocation>
{
public AttributionSourceLocationConverter()
: base(
discriminator: "kind",
variants: new Dictionary<string, Type>
{
["text"] = typeof(AttributionTextSourceLocation),
["page"] = typeof(AttributionPageSourceLocation),
},
allowUnknown: true)
{
Expand Down
1 change: 1 addition & 0 deletions clients/dotnet/src/AgentHostProtocol/Reducers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ private static ReduceOutcome UpdateResponsePart(
ToolCallResponsePart v => ToolCallId(v.ToolCall),
MarkdownResponsePart v => v.Id,
ReasoningResponsePart v => v.Id,
AttributionResponsePart v => v.Id,
_ => string.Empty,
};

Expand Down
2 changes: 2 additions & 0 deletions clients/go/ahp/reducers.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,8 @@ func updateResponsePart(state *ahptypes.ChatState, turnID, partID string, update
id = v.Id
case *ahptypes.ReasoningResponsePart:
id = v.Id
case *ahptypes.AttributionResponsePart:
id = v.Id
}
if id != "" && id == partID {
updater(part)
Expand Down
152 changes: 152 additions & 0 deletions clients/go/ahptypes/state.generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ const (
ResponsePartKindSystemNotification ResponsePartKind = "systemNotification"
ResponsePartKindInputRequest ResponsePartKind = "inputRequest"
ResponsePartKindError ResponsePartKind = "error"
ResponsePartKindAttribution ResponsePartKind = "attribution"
)

// Status of a tool call in the lifecycle state machine.
Expand All @@ -237,6 +238,14 @@ const (
ToolCallStatusCancelled ToolCallStatus = "cancelled"
)

// The kind of location within an attribution source.
type AttributionSourceLocationKind string

const (
AttributionSourceLocationKindText AttributionSourceLocationKind = "text"
AttributionSourceLocationKindPage AttributionSourceLocationKind = "page"
)

// How a tool call was confirmed for execution.
//
// - `NotNeeded` — No confirmation required (auto-approved)
Expand Down Expand Up @@ -1994,6 +2003,82 @@ type ErrorResponsePart struct {
Resumable *bool `json:"resumable,omitempty"`
}

// Sources that support an earlier markdown or reasoning part in the same turn.
//
// The host appends this part with `chat/responsePart` after the target's last
// text delta and before the turn ends. At most one attribution part may target
// a given part. Neither the target text nor its attribution changes afterward,
// including when the turn resumes; further output uses new part identifiers.
//
// Clients MAY show inline citations or a source list. Clients that do not
// support attribution can ignore this part and still render the original text.
type AttributionResponsePart struct {
// Discriminant
Kind ResponsePartKind `json:"kind"`
// Non-empty identifier, unique among response parts in this turn.
Id string `json:"id"`
// Identifier of the earlier MarkdownResponsePart or ReasoningResponsePart.
TargetPartId string `json:"targetPartId"`
// Supporting sources. MUST contain at least one entry, with distinct IDs.
Sources []AttributionSource `json:"sources"`
// Ranges of target text linked to sources. An empty list supplies sources for
// the target as a whole without claiming a more precise text-to-source mapping.
Spans []AttributionSpan `json:"spans"`
// Optional implementation-specific details; not needed to display attribution.
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
}

// A source, or a passage within a source, supporting an attributed response.
//
// A source need not be a public URL or a local file. Hosts SHOULD provide a
// title when no URI is available and prefer versioned URIs when possible.
// Two entries may share a URI when they describe different passages.
type AttributionSource struct {
// Non-empty identifier, unique within the containing attribution part.
Id string `json:"id"`
// Human-readable source title, rendered as plain text.
Title *string `json:"title,omitempty"`
// Source URI. Its presence does not authorize opening or fetching the resource.
Uri *URI `json:"uri,omitempty"`
// MIME type of the source, when known.
ContentType *string `json:"contentType,omitempty"`
// Optional short quotation from the source, not a generated answer summary.
Excerpt *string `json:"excerpt,omitempty"`
// Location within the source, not within the generated response.
Location *AttributionSourceLocation `json:"location,omitempty"`
// Optional implementation-specific details; not needed to display the source.
Meta map[string]json.RawMessage `json:"_meta,omitempty"`
}

// A non-empty range of generated text linked to supporting sources.
//
// Ranges address the target part's raw `content`, before Markdown rendering:
// zero-based lines and UTF-16 code-unit character offsets, start inclusive and
// end exclusive. CRLF, LF, and lone CR each count as one line break. Positions
// MUST lie within the text and MUST NOT split a surrogate pair.
type AttributionSpan struct {
// Range within the target part, not within any source or the combined turn.
Range TextRange `json:"range"`
// Non-empty, distinct IDs from the containing AttributionResponsePart.sources.
SourceIds []string `json:"sourceIds"`
}

// A range within a textual source, using the same position rules as AttributionSpan.
type AttributionTextSourceLocation struct {
Kind AttributionSourceLocationKind `json:"kind"`
// Non-empty, start-inclusive, end-exclusive range within the source text.
Range TextRange `json:"range"`
}

// An inclusive range of pages within a document.
type AttributionPageSourceLocation struct {
Kind AttributionSourceLocationKind `json:"kind"`
// First page, numbered from one.
StartPage int64 `json:"startPage"`
// Last page, inclusive. MUST be greater than or equal to startPage.
EndPage int64 `json:"endPage"`
}

// Tool execution result details, available after execution completes.
type ToolCallResult struct {
// Whether the tool succeeded
Expand Down Expand Up @@ -4216,6 +4301,7 @@ func (*ReasoningResponsePart) isResponsePart() {}
func (*SystemNotificationResponsePart) isResponsePart() {}
func (*InputRequestResponsePart) isResponsePart() {}
func (*ErrorResponsePart) isResponsePart() {}
func (*AttributionResponsePart) isResponsePart() {}

// ResponsePartUnknown carries an unrecognized ResponsePart variant — typically a discriminator value introduced by a newer protocol version. The original JSON object is preserved verbatim so that re-encoding round-trips faithfully.
type ResponsePartUnknown struct {
Expand Down Expand Up @@ -4273,6 +4359,12 @@ func (u *ResponsePart) UnmarshalJSON(data []byte) error {
return err
}
u.Value = &value
case "attribution":
var value AttributionResponsePart
if err := json.Unmarshal(data, &value); err != nil {
return err
}
u.Value = &value
default:
raw := make(json.RawMessage, len(data))
copy(raw, data)
Expand All @@ -4295,6 +4387,66 @@ func (u ResponsePart) MarshalJSON() ([]byte, error) {
return json.Marshal(u.Value)
}

// AttributionSourceLocation identifies a passage within a supporting source.
type AttributionSourceLocation struct {
Value isAttributionSourceLocation
}

// isAttributionSourceLocation is the marker interface implemented by every
// concrete variant of AttributionSourceLocation.
type isAttributionSourceLocation interface{ isAttributionSourceLocation() }

func (*AttributionTextSourceLocation) isAttributionSourceLocation() {}
func (*AttributionPageSourceLocation) isAttributionSourceLocation() {}

// AttributionSourceLocationUnknown carries an unrecognized AttributionSourceLocation variant — typically a discriminator value introduced by a newer protocol version. The original JSON object is preserved verbatim so that re-encoding round-trips faithfully.
type AttributionSourceLocationUnknown struct {
Raw json.RawMessage
}

func (*AttributionSourceLocationUnknown) isAttributionSourceLocation() {}

// UnmarshalJSON decodes the variant indicated by the "kind" discriminator.
func (u *AttributionSourceLocation) UnmarshalJSON(data []byte) error {
disc, _, err := readDiscriminator(data, "kind")
if err != nil {
return err
}
switch disc {
case "text":
var value AttributionTextSourceLocation
if err := json.Unmarshal(data, &value); err != nil {
return err
}
u.Value = &value
case "page":
var value AttributionPageSourceLocation
if err := json.Unmarshal(data, &value); err != nil {
return err
}
u.Value = &value
default:
raw := make(json.RawMessage, len(data))
copy(raw, data)
u.Value = &AttributionSourceLocationUnknown{Raw: raw}
}
return nil
}

// MarshalJSON encodes the active variant back to JSON.
func (u AttributionSourceLocation) MarshalJSON() ([]byte, error) {
if unk, ok := u.Value.(*AttributionSourceLocationUnknown); ok {
if len(unk.Raw) == 0 {
return []byte("null"), nil
}
return unk.Raw, nil
}
if u.Value == nil {
return []byte("null"), nil
}
return json.Marshal(u.Value)
}

// ToolCallState is the full tool call lifecycle state.
type ToolCallState struct {
Value isToolCallState
Expand Down
Loading