diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs
index 394577de9..c9939544f 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Actions.generated.cs
@@ -784,19 +784,20 @@ public sealed record SessionActiveClientRemovedAction
public required string ClientId { get; init; }
}
-/// A working directory was added to the session's
+/// A working directory was added or updated in the session's
/// {@link SessionState.workingDirectories} set.
///
-/// Membership semantics keyed by the directory URI: the reducer appends
-/// `directory` when the set does not already contain it (creating the set if
-/// absent) and is a no-op when it is already present. Only valid when the agent
-/// advertises {@link AgentCapabilities.multipleWorkingDirectories}.
+/// Upsert keyed by URI. A rich record replaces the existing entry in place;
+/// a URI-only duplicate preserves known metadata. Only valid when the agent
+/// advertises {@link AgentCapabilities.multipleWorkingDirectories}. Clients
+/// MUST omit `repo` and use only local or absent `origin`; the host validates
+/// and enriches the entry before accepting it.
public sealed record SessionWorkingDirectorySetAction
{
public ActionType Type { get; init; } = ActionType.SessionWorkingDirectorySet;
/// The working directory to grant the session's agent tool access to.
- public required string Directory { get; init; }
+ public required WorkingDirectoryEntry Directory { get; init; }
}
/// A working directory was removed from the session's
@@ -829,13 +830,16 @@ public sealed record SessionWorkingDirectoryRemovedAction
/// for example, `[A, B, C]` with `B → C` becomes `[A, C]`. When it occurs
/// before the target, it retains its earlier position and the target is removed;
/// `[A, B, C]` with `C → A` becomes `[A, B]`.
+/// A rich replacement supplies the complete entry; a URI-only replacement
+/// preserves metadata already known for that URI.
///
/// Only valid when the agent advertises
/// {@link AgentCapabilities.multipleWorkingDirectories}. Replacing index `0`
/// additionally requires
/// {@link MultipleWorkingDirectoriesCapability.primaryReplacement}; clients
/// MUST NOT target an immutable primary. The host MUST validate and apply its
-/// backend side effect before broadcasting an accepted action, or reject it.
+/// backend side effect before broadcasting an accepted action, or reject it.
+/// Clients MUST omit `repo` and use only local or absent `origin`.
public sealed record SessionWorkingDirectoryReplacedAction
{
public ActionType Type { get; init; } = ActionType.SessionWorkingDirectoryReplaced;
@@ -843,8 +847,8 @@ public sealed record SessionWorkingDirectoryReplacedAction
/// URI of the existing entry to replace.
public required string Directory { get; init; }
- /// URI to place in the replaced entry's position.
- public required string Replacement { get; init; }
+ /// Complete entry to place in the replaced entry's position.
+ public required WorkingDirectoryEntry Replacement { get; init; }
}
/// A session-level input request was added or updated.
@@ -1869,8 +1873,9 @@ public sealed record CanvasStateChangedAction
/// Membership semantics keyed by the directory URI: the reducer appends
/// `directory` when the chat's subset does not already contain it (creating the
/// subset if absent) and is a no-op when it is already present. `directory` MUST
-/// be one of the owning session's {@link SessionState.workingDirectories}; a host
-/// MUST reject a directory that is not. Only valid when the agent advertises
+/// match a URI string or record's `uri` in the owning session's
+/// {@link SessionState.workingDirectories}; a host MUST reject a directory that
+/// does not. Only valid when the agent advertises
/// {@link AgentCapabilities.multipleWorkingDirectories}.
public sealed record ChatWorkingDirectorySetAction
{
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
index 13801df80..71b085d37 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Commands.generated.cs
@@ -331,6 +331,21 @@ public override void Write(Utf8JsonWriter writer, AuthenticateTargetKind value,
// ─── Command Payloads ─────────────────────────────────────────────────
+/// Requested repository source, not a resolved working directory.
+public sealed record RepositorySource
+{
+ /// Credential-free repository source URI.
+ public required string Source { get; init; }
+
+ /// Requested branch, tag, or commit. Omit to use the host's default revision.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Revision { get; init; }
+
+ /// Repository-relative selected folder; omit for the root. Hosts reject empty, absolute, or escaping paths.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Subdirectory { get; init; }
+}
+
/// Opens a TCP connection from the host to a destination on the host's network.
///
/// The client chooses the channel URI. This command does not subscribe: after it
@@ -495,6 +510,10 @@ public sealed record InitializeResult
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? DefaultDirectory { get; init; }
+ /// Host repository preparation support; absent when unsupported.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public RepositoryPreparationCapability? RepositoryPreparation { get; init; }
+
/// Characters that, when typed in a {@link Message} input, SHOULD cause
/// the client to issue a `completions` request with
/// {@link CompletionItemKind.UserMessage}. Typically includes characters like
@@ -576,6 +595,11 @@ public sealed record Implementation
/// are reserved for future per-capability options.
public sealed record ClientCapabilities
{
+ /// Client accepts rich {@link WorkingDirectory} records as well as URI strings.
+ /// Hosts project records to URIs when absent and retain this choice on reconnect.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public WorkingDirectoryInfoCapability? WorkingDirectoryInfo { get; init; }
+
/// Client can render
/// [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) — i.e.
/// it can host the View sandbox, run the `ui/*` protocol against it,
@@ -591,6 +615,24 @@ public sealed record ClientCapabilities
public Dictionary? McpApps { get; init; }
}
+/// Host support for repository preparation at session creation. Presence
+/// alone supports one repository at its default revision.
+public sealed record RepositoryPreparationCapability
+{
+ /// When true, clients may supply {@link RepositorySource.revision}.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? Revision { get; init; }
+
+ /// When true, clients may supply more than one repository.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? MultipleRepositories { get; init; }
+}
+
+/// Presence capability for {@link ClientCapabilities.workingDirectoryInfo}.
+public sealed record WorkingDirectoryInfoCapability
+{
+}
+
/// Automation features supported by this host authority.
///
/// The presence of this object advertises the baseline `ahp-automations://`
@@ -947,6 +989,11 @@ public sealed record CreateSessionParams
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List? WorkingDirectories { get; init; }
+ /// Repositories to prepare instead of an explicit `workingDirectories` list.
+ /// An empty list is equivalent to omission.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? Repositories { get; init; }
+
/// Agent-specific configuration values collected via `resolveSessionConfig`.
/// Keys and values correspond to the schema returned by the server.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
@@ -1070,9 +1117,9 @@ public sealed record CreateChatParams
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public ChatSource? Source { get; init; }
- /// Initial working-directory subset for this chat. Every entry MUST be
- /// present in the owning session's `workingDirectories`; the server MUST
- /// reject any entry that is not. When absent, the chat inherits the full
+ /// Initial working-directory URI subset for this chat. Every URI MUST match
+ /// a URI string or record's `uri` in the owning session's `workingDirectories`;
+ /// the server MUST reject any entry that does not. When absent, the chat inherits the full
/// session set. Forked chats (those whose `source.kind` is `"fork"`) inherit
/// the source chat's `workingDirectories`; this field is ignored for forks.
///
@@ -2002,6 +2049,10 @@ public sealed record ResolveSessionConfigParams
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? WorkingDirectory { get; init; }
+ /// Repositories used as configuration context; an empty list is equivalent to omission.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? Repositories { get; init; }
+
/// Current user-filled configuration values
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary? Config { get; init; }
@@ -2040,6 +2091,10 @@ public sealed record SessionConfigCompletionsParams
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? WorkingDirectory { get; init; }
+ /// Repositories used as configuration context; an empty list is equivalent to omission.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List? Repositories { get; init; }
+
/// Current user-filled configuration values (provides context for the query)
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary? Config { get; init; }
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
index 581549391..25588f646 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/JsonSerializerContext.generated.cs
@@ -266,6 +266,7 @@ namespace Microsoft.AgentHostProtocol;
[JsonSerializable(typeof(ListAutomationTriggerDefinitionsResult))]
[JsonSerializable(typeof(ListSessionsParams))]
[JsonSerializable(typeof(ListSessionsResult))]
+[JsonSerializable(typeof(LocalWorkingDirectoryOrigin))]
[JsonSerializable(typeof(MarkdownResponsePart))]
[JsonSerializable(typeof(McpAuthRequiredReason))]
[JsonSerializable(typeof(McpAuthRequirement))]
@@ -316,6 +317,9 @@ namespace Microsoft.AgentHostProtocol;
[JsonSerializable(typeof(ReconnectResult))]
[JsonSerializable(typeof(ReconnectResultType))]
[JsonSerializable(typeof(ReconnectSnapshotResult))]
+[JsonSerializable(typeof(RepositoryPreparationCapability))]
+[JsonSerializable(typeof(RepositorySource))]
+[JsonSerializable(typeof(RepoWorkingDirectoryOrigin))]
[JsonSerializable(typeof(ResolveSessionConfigParams))]
[JsonSerializable(typeof(ResolveSessionConfigResult))]
[JsonSerializable(typeof(ResourceChange))]
@@ -514,6 +518,12 @@ namespace Microsoft.AgentHostProtocol;
[JsonSerializable(typeof(UnsubscribeParams))]
[JsonSerializable(typeof(UnsupportedProtocolVersionErrorData))]
[JsonSerializable(typeof(UsageInfo))]
+[JsonSerializable(typeof(WorkingDirectory))]
+[JsonSerializable(typeof(WorkingDirectoryEntry))]
+[JsonSerializable(typeof(WorkingDirectoryInfoCapability))]
+[JsonSerializable(typeof(WorkingDirectoryOrigin))]
+[JsonSerializable(typeof(WorkingDirectoryOriginKind))]
+[JsonSerializable(typeof(WorktreeWorkingDirectoryOrigin))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs
index 0b4da6a7d..85ddab128 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/Notifications.generated.cs
@@ -443,9 +443,11 @@ public sealed record PartialSessionSummary
/// (the first entry is a protected, replaceable primary slot). Individual chats
/// MAY restrict to a subset via
/// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
- /// chat that sets none operates against this full set.
+ /// chat that sets none operates against this full set.
+ /// Entries are uniquely keyed by URI. Rich records require
+ /// {@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public List? WorkingDirectories { get; init; }
+ public List? WorkingDirectories { get; init; }
/// Lightweight summary of this session's inline annotations channel
/// (`ahp-annotations:/<uuid>`). Surfaced so badge UI can render
diff --git a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
index 641370767..2d19212c9 100644
--- a/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
+++ b/clients/dotnet/src/AgentHostProtocol.Abstractions/Generated/State.generated.cs
@@ -176,6 +176,59 @@ public override void Write(Utf8JsonWriter writer, SessionOriginKind value, JsonS
=> writer.WriteStringValue(value.Value);
}
+/// How a working directory was prepared.
+[JsonConverter(typeof(WorkingDirectoryOriginKindConverter))]
+public readonly struct WorkingDirectoryOriginKind : IEquatable
+{
+ private readonly string? _value;
+
+ /// Wraps a raw wire value — including one this build does not recognize.
+ /// The raw wire string.
+ public WorkingDirectoryOriginKind(string value)
+ {
+ _value = value;
+ }
+
+ /// The raw wire value.
+ public string Value => _value ?? string.Empty;
+
+ public static readonly WorkingDirectoryOriginKind Local = new WorkingDirectoryOriginKind("local");
+
+ public static readonly WorkingDirectoryOriginKind Repo = new WorkingDirectoryOriginKind("repo");
+
+ public static readonly WorkingDirectoryOriginKind Worktree = new WorkingDirectoryOriginKind("worktree");
+
+ ///
+ public bool Equals(WorkingDirectoryOriginKind other) => string.Equals(Value, other.Value, StringComparison.Ordinal);
+
+ ///
+ public override bool Equals(object? obj) => obj is WorkingDirectoryOriginKind other && Equals(other);
+
+ ///
+ public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Value);
+
+ ///
+ public override string ToString() => Value;
+
+ /// Ordinal equality over the raw wire value.
+ public static bool operator ==(WorkingDirectoryOriginKind left, WorkingDirectoryOriginKind right) => left.Equals(right);
+
+ /// Ordinal inequality over the raw wire value.
+ public static bool operator !=(WorkingDirectoryOriginKind left, WorkingDirectoryOriginKind right) => !left.Equals(right);
+}
+
+/// Reads and writes as its raw wire string, preserving unrecognized values.
+internal sealed class WorkingDirectoryOriginKindConverter : JsonConverter
+{
+ ///
+ public override WorkingDirectoryOriginKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ => new WorkingDirectoryOriginKind(reader.GetString() ?? throw new JsonException("WorkingDirectoryOriginKind expects a JSON string."));
+
+ ///
+ public override void Write(Utf8JsonWriter writer, WorkingDirectoryOriginKind value, JsonSerializerOptions options)
+ => writer.WriteStringValue(value.Value);
+}
+
/// Discriminant for {@link ChatOrigin} — how a chat came into existence.
[JsonConverter(typeof(ChatOriginKindConverter))]
public readonly struct ChatOriginKind : IEquatable
@@ -2152,6 +2205,39 @@ public sealed record MultipleWorkingDirectoriesCapability
public bool? PrimaryReplacement { get; init; }
}
+/// An actual working directory, uniquely keyed by `uri` within the session.
+public sealed record WorkingDirectory
+{
+ /// Actual selected directory, which may be a repository subdirectory.
+ public required string Uri { get; init; }
+
+ /// Credential-free repository source association, not a checkout identity.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Repo { get; init; }
+
+ /// Host-reported provenance; omission means unspecified.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public WorkingDirectoryOrigin? Origin { get; init; }
+}
+
+public sealed record LocalWorkingDirectoryOrigin
+{
+ public WorkingDirectoryOriginKind Kind { get; init; } = WorkingDirectoryOriginKind.Local;
+}
+
+public sealed record RepoWorkingDirectoryOrigin
+{
+ public WorkingDirectoryOriginKind Kind { get; init; } = WorkingDirectoryOriginKind.Repo;
+}
+
+public sealed record WorktreeWorkingDirectoryOrigin
+{
+ public WorkingDirectoryOriginKind Kind { get; init; } = WorkingDirectoryOriginKind.Worktree;
+
+ /// Main worktree associated with the host-prepared worktree.
+ public required string MainWorktree { get; init; }
+}
+
public sealed record SessionModelInfo
{
/// Model identifier
@@ -2538,13 +2624,14 @@ public sealed class ChatState
/// The subset of the session's
/// {@link SessionState.workingDirectories | `workingDirectories`} that this
- /// chat's agent has tool access to. Every entry MUST be present in the owning
- /// session's `workingDirectories`; servers MUST reject a
+ /// chat's agent has tool access to. Every URI MUST match a URI string or a
+ /// record's `uri` in the owning session's set; servers MUST reject a
/// `chat/workingDirectorySet` action that violates this constraint.
///
/// When absent, the chat inherits the full session set. When present but empty
/// (not recommended), the chat has no working-directory tool access at all.
///
+ /// Directory metadata belongs to the session, not the chat.
/// Dispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to
/// update the subset on a running chat.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
@@ -2991,9 +3078,11 @@ public sealed class SessionState
/// (the first entry is a protected, replaceable primary slot). Individual chats
/// MAY restrict to a subset via
/// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
- /// chat that sets none operates against this full set.
+ /// chat that sets none operates against this full set.
+ /// Entries are uniquely keyed by URI. Rich records require
+ /// {@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public List? WorkingDirectories { get; set; }
+ public List? WorkingDirectories { get; set; }
/// Lightweight summary of this session's inline annotations channel
/// (`ahp-annotations:/<uuid>`). Surfaced so badge UI can render
@@ -3330,9 +3419,11 @@ public sealed class SessionSummary
/// (the first entry is a protected, replaceable primary slot). Individual chats
/// MAY restrict to a subset via
/// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
- /// chat that sets none operates against this full set.
+ /// chat that sets none operates against this full set.
+ /// Entries are uniquely keyed by URI. Rich records require
+ /// {@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public List? WorkingDirectories { get; set; }
+ public List? WorkingDirectories { get; set; }
/// Lightweight summary of this session's inline annotations channel
/// (`ahp-annotations:/<uuid>`). Surfaced so badge UI can render
@@ -8035,6 +8126,34 @@ public SessionOriginConverter()
}
}
+/// Host-reported working-directory provenance.
+[JsonConverter(typeof(WorkingDirectoryOriginConverter))]
+public sealed class WorkingDirectoryOrigin : AhpUnion
+{
+ /// Creates an empty WorkingDirectoryOrigin (no active variant).
+ public WorkingDirectoryOrigin() { }
+
+ /// Creates a WorkingDirectoryOrigin wrapping the given variant value.
+ public WorkingDirectoryOrigin(object? value) : base(value) { }
+}
+
+/// System.Text.Json converter for the WorkingDirectoryOrigin discriminated union.
+internal sealed class WorkingDirectoryOriginConverter : UnionConverter
+{
+ public WorkingDirectoryOriginConverter()
+ : base(
+ discriminator: "kind",
+ variants: new Dictionary
+ {
+ ["local"] = typeof(LocalWorkingDirectoryOrigin),
+ ["repo"] = typeof(RepoWorkingDirectoryOrigin),
+ ["worktree"] = typeof(WorktreeWorkingDirectoryOrigin),
+ },
+ allowUnknown: true)
+ {
+ }
+}
+
/// AutomationTrigger is an automatic trigger for an automation.
[JsonConverter(typeof(AutomationTriggerConverter))]
public sealed class AutomationTrigger : AhpUnion
@@ -8262,6 +8381,37 @@ public override void Write(Utf8JsonWriter writer, ToolInput value, JsonSerialize
}
}
+/// A legacy URI or a complete working-directory record.
+[JsonConverter(typeof(WorkingDirectoryEntryConverter))]
+public sealed class WorkingDirectoryEntry
+{
+ public WorkingDirectoryEntry(string uri) { Uri = uri; }
+ public WorkingDirectoryEntry(WorkingDirectory directory) { Uri = directory.Uri; Directory = directory; }
+
+ public string Uri { get; }
+ public WorkingDirectory? Directory { get; }
+}
+
+internal sealed class WorkingDirectoryEntryConverter : JsonConverter
+{
+ public override WorkingDirectoryEntry Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ return new WorkingDirectoryEntry(reader.GetString()!);
+ }
+ return new WorkingDirectoryEntry(
+ JsonSerializer.Deserialize(ref reader, AhpJsonTypeInfo.Get(options))
+ ?? throw new JsonException("WorkingDirectoryEntry requires a URI or directory"));
+ }
+
+ public override void Write(Utf8JsonWriter writer, WorkingDirectoryEntry value, JsonSerializerOptions options)
+ {
+ if (value.Directory is null) { writer.WriteStringValue(value.Uri); return; }
+ JsonSerializer.Serialize(writer, value.Directory, AhpJsonTypeInfo.Get(options));
+ }
+}
+
///
/// SnapshotState is the state payload of a snapshot — root, session,
/// chat, canvas, terminal, changeset, resource-watch, annotations, automation catalogue,
diff --git a/clients/dotnet/src/AgentHostProtocol/Reducers.cs b/clients/dotnet/src/AgentHostProtocol/Reducers.cs
index dadc1c50f..664d6b29b 100644
--- a/clients/dotnet/src/AgentHostProtocol/Reducers.cs
+++ b/clients/dotnet/src/AgentHostProtocol/Reducers.cs
@@ -784,16 +784,20 @@ public static ReduceOutcome ApplyToSession(SessionState state, StateAction actio
}
case SessionWorkingDirectorySetAction a:
{
- // Membership keyed by the directory URI: append when the set does
- // not already contain it (creating the set if absent), no-op when
- // it is already present. Mirrors the TS reducer.
- if (state.WorkingDirectories is not null && state.WorkingDirectories.Contains(a.Directory))
+ state.WorkingDirectories ??= new List();
+ int index = state.WorkingDirectories.FindIndex(d => d.Uri == a.Directory.Uri);
+ if (index < 0)
+ {
+ state.WorkingDirectories.Add(a.Directory);
+ }
+ else if (a.Directory.Directory is null)
{
return ReduceOutcome.NoOp;
}
-
- state.WorkingDirectories ??= new List();
- state.WorkingDirectories.Add(a.Directory);
+ else
+ {
+ state.WorkingDirectories[index] = a.Directory;
+ }
return ReduceOutcome.Applied;
}
case SessionWorkingDirectoryRemovedAction a:
@@ -806,7 +810,7 @@ public static ReduceOutcome ApplyToSession(SessionState state, StateAction actio
return ReduceOutcome.NoOp;
}
- int wdIdx = state.WorkingDirectories.IndexOf(a.Directory);
+ int wdIdx = state.WorkingDirectories.FindIndex(d => d.Uri == a.Directory);
if (wdIdx < 0)
{
return ReduceOutcome.NoOp;
@@ -822,26 +826,27 @@ public static ReduceOutcome ApplyToSession(SessionState state, StateAction actio
return ReduceOutcome.NoOp;
}
- int index = state.WorkingDirectories.IndexOf(a.Directory);
+ int index = state.WorkingDirectories.FindIndex(d => d.Uri == a.Directory);
if (index < 0)
{
return ReduceOutcome.NoOp;
}
- int replacementIndex = state.WorkingDirectories.IndexOf(a.Replacement);
+ int replacementIndex = state.WorkingDirectories.FindIndex(d => d.Uri == a.Replacement.Uri);
+ var replacement = a.Replacement.Directory is null && replacementIndex >= 0
+ ? state.WorkingDirectories[replacementIndex]
+ : a.Replacement;
if (replacementIndex >= 0 && replacementIndex < index)
{
+ state.WorkingDirectories[replacementIndex] = replacement;
state.WorkingDirectories.RemoveAt(index);
return ReduceOutcome.Applied;
}
- state.WorkingDirectories[index] = a.Replacement;
- for (int i = state.WorkingDirectories.Count - 1; i >= 0; i--)
+ state.WorkingDirectories[index] = replacement;
+ if (replacementIndex > index)
{
- if (i != index && state.WorkingDirectories[i] == a.Replacement)
- {
- state.WorkingDirectories.RemoveAt(i);
- }
+ state.WorkingDirectories.RemoveAt(replacementIndex);
}
return ReduceOutcome.Applied;
diff --git a/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs b/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs
index 447313763..363c02d4d 100644
--- a/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs
+++ b/clients/dotnet/tests/AgentHostProtocol.Tests/TypesRoundTripFixtures.cs
@@ -164,6 +164,18 @@ private static (object decoded, string reencoded) DecodeAndReencode(string type,
return Wrap(Ser.Deserialize(inputJson));
case "InitializeResult":
return Wrap(Ser.Deserialize(inputJson));
+ case "InitializeParams":
+ return Wrap(Ser.Deserialize(inputJson));
+ case "ListSessionsResult":
+ return Wrap(Ser.Deserialize(inputJson));
+ case "SessionSummaryChangedParams":
+ return Wrap(Ser.Deserialize(inputJson));
+ case "CreateSessionParams":
+ return Wrap(Ser.Deserialize(inputJson));
+ case "ResolveSessionConfigParams":
+ return Wrap(Ser.Deserialize(inputJson));
+ case "SessionConfigCompletionsParams":
+ return Wrap(Ser.Deserialize(inputJson));
case "CreateChatParams":
return Wrap(Ser.Deserialize(inputJson));
case "Snapshot":
diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go
index 22fe67c01..404ebe39b 100644
--- a/clients/go/ahp/reducers.go
+++ b/clients/go/ahp/reducers.go
@@ -1058,16 +1058,20 @@ func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAct
}
return ReduceOutcomeNoOp
case *ahptypes.SessionWorkingDirectorySetAction:
- for _, d := range state.WorkingDirectories {
- if d == a.Directory {
- return ReduceOutcomeNoOp
+ for i, d := range state.WorkingDirectories {
+ if d.GetURI() == a.Directory.GetURI() {
+ if a.Directory.Directory == nil {
+ return ReduceOutcomeNoOp
+ }
+ state.WorkingDirectories[i] = a.Directory
+ return ReduceOutcomeApplied
}
}
state.WorkingDirectories = append(state.WorkingDirectories, a.Directory)
return ReduceOutcomeApplied
case *ahptypes.SessionWorkingDirectoryRemovedAction:
for i := range state.WorkingDirectories {
- if state.WorkingDirectories[i] == a.Directory {
+ if state.WorkingDirectories[i].GetURI() == a.Directory {
state.WorkingDirectories = append(state.WorkingDirectories[:i], state.WorkingDirectories[i+1:]...)
return ReduceOutcomeApplied
}
@@ -1076,7 +1080,7 @@ func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAct
case *ahptypes.SessionWorkingDirectoryReplacedAction:
idx := -1
for i, directory := range state.WorkingDirectories {
- if directory == a.Directory {
+ if directory.GetURI() == a.Directory {
idx = i
break
}
@@ -1084,21 +1088,26 @@ func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAct
if idx < 0 {
return ReduceOutcomeNoOp
}
- for i := 0; i < idx; i++ {
- if state.WorkingDirectories[i] == a.Replacement {
- state.WorkingDirectories = append(state.WorkingDirectories[:idx], state.WorkingDirectories[idx+1:]...)
- return ReduceOutcomeApplied
+ replacementIdx := -1
+ for i, directory := range state.WorkingDirectories {
+ if directory.GetURI() == a.Replacement.GetURI() {
+ replacementIdx = i
+ break
}
}
- updated := make([]ahptypes.URI, 0, len(state.WorkingDirectories))
- for i, directory := range state.WorkingDirectories {
- if i == idx {
- updated = append(updated, a.Replacement)
- } else if directory != a.Replacement {
- updated = append(updated, directory)
+ replacement := a.Replacement
+ if replacement.Directory == nil && replacementIdx >= 0 {
+ replacement = state.WorkingDirectories[replacementIdx]
+ }
+ if replacementIdx >= 0 && replacementIdx < idx {
+ state.WorkingDirectories[replacementIdx] = replacement
+ state.WorkingDirectories = append(state.WorkingDirectories[:idx], state.WorkingDirectories[idx+1:]...)
+ } else {
+ state.WorkingDirectories[idx] = replacement
+ if replacementIdx > idx {
+ state.WorkingDirectories = append(state.WorkingDirectories[:replacementIdx], state.WorkingDirectories[replacementIdx+1:]...)
}
}
- state.WorkingDirectories = updated
return ReduceOutcomeApplied
case *ahptypes.SessionInputNeededSetAction:
id, ok := sessionInputRequestID(a.Request)
diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go
index cfb71a816..e0cc3dfc5 100644
--- a/clients/go/ahptypes/actions.generated.go
+++ b/clients/go/ahptypes/actions.generated.go
@@ -1079,17 +1079,18 @@ type SessionActiveClientRemovedAction struct {
ClientId string `json:"clientId"`
}
-// A working directory was added to the session's
+// A working directory was added or updated in the session's
// {@link SessionState.workingDirectories} set.
//
-// Membership semantics keyed by the directory URI: the reducer appends
-// `directory` when the set does not already contain it (creating the set if
-// absent) and is a no-op when it is already present. Only valid when the agent
-// advertises {@link AgentCapabilities.multipleWorkingDirectories}.
+// Upsert keyed by URI. A rich record replaces the existing entry in place;
+// a URI-only duplicate preserves known metadata. Only valid when the agent
+// advertises {@link AgentCapabilities.multipleWorkingDirectories}. Clients
+// MUST omit `repo` and use only local or absent `origin`; the host validates
+// and enriches the entry before accepting it.
type SessionWorkingDirectorySetAction struct {
Type ActionType `json:"type"`
// The working directory to grant the session's agent tool access to.
- Directory URI `json:"directory"`
+ Directory WorkingDirectoryEntry `json:"directory"`
}
// A working directory was removed from the session's
@@ -1120,6 +1121,8 @@ type SessionWorkingDirectoryRemovedAction struct {
// for example, `[A, B, C]` with `B → C` becomes `[A, C]`. When it occurs
// before the target, it retains its earlier position and the target is removed;
// `[A, B, C]` with `C → A` becomes `[A, B]`.
+// A rich replacement supplies the complete entry; a URI-only replacement
+// preserves metadata already known for that URI.
//
// Only valid when the agent advertises
// {@link AgentCapabilities.multipleWorkingDirectories}. Replacing index `0`
@@ -1127,12 +1130,13 @@ type SessionWorkingDirectoryRemovedAction struct {
// {@link MultipleWorkingDirectoriesCapability.primaryReplacement}; clients
// MUST NOT target an immutable primary. The host MUST validate and apply its
// backend side effect before broadcasting an accepted action, or reject it.
+// Clients MUST omit `repo` and use only local or absent `origin`.
type SessionWorkingDirectoryReplacedAction struct {
Type ActionType `json:"type"`
// URI of the existing entry to replace.
Directory URI `json:"directory"`
- // URI to place in the replaced entry's position.
- Replacement URI `json:"replacement"`
+ // Complete entry to place in the replaced entry's position.
+ Replacement WorkingDirectoryEntry `json:"replacement"`
}
// A working directory was added to this chat's
@@ -1141,8 +1145,9 @@ type SessionWorkingDirectoryReplacedAction struct {
// Membership semantics keyed by the directory URI: the reducer appends
// `directory` when the chat's subset does not already contain it (creating the
// subset if absent) and is a no-op when it is already present. `directory` MUST
-// be one of the owning session's {@link SessionState.workingDirectories}; a host
-// MUST reject a directory that is not. Only valid when the agent advertises
+// match a URI string or record's `uri` in the owning session's
+// {@link SessionState.workingDirectories}; a host MUST reject a directory that
+// does not. Only valid when the agent advertises
// {@link AgentCapabilities.multipleWorkingDirectories}.
type ChatWorkingDirectorySetAction struct {
Type ActionType `json:"type"`
diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go
index 77443e810..78a5914c8 100644
--- a/clients/go/ahptypes/commands.generated.go
+++ b/clients/go/ahptypes/commands.generated.go
@@ -114,6 +114,16 @@ const (
// ─── Command Payloads ─────────────────────────────────────────────────
+// Requested repository source, not a resolved working directory.
+type RepositorySource struct {
+ // Credential-free repository source URI.
+ Source URI `json:"source"`
+ // Requested branch, tag, or commit. Omit to use the host's default revision.
+ Revision *string `json:"revision,omitempty"`
+ // Repository-relative selected folder; omit for the root. Hosts reject empty, absolute, or escaping paths.
+ Subdirectory *string `json:"subdirectory,omitempty"`
+}
+
// Opens a TCP connection from the host to a destination on the host's network.
//
// The client chooses the channel URI. This command does not subscribe: after it
@@ -243,6 +253,8 @@ type InitializeResult struct {
Environment *HostEnvironment `json:"environment,omitempty"`
// Suggested default directory for remote filesystem browsing
DefaultDirectory *URI `json:"defaultDirectory,omitempty"`
+ // Host repository preparation support; absent when unsupported.
+ RepositoryPreparation *RepositoryPreparationCapability `json:"repositoryPreparation,omitempty"`
// Characters that, when typed in a {@link Message} input, SHOULD cause
// the client to issue a `completions` request with
// {@link CompletionItemKind.UserMessage}. Typically includes characters like
@@ -277,12 +289,28 @@ type InitializeResult struct {
MultipleChatProviders *MultipleChatProvidersCapability `json:"multipleChatProviders,omitempty"`
}
+// Host support for repository preparation at session creation. Presence
+// alone supports one repository at its default revision.
+type RepositoryPreparationCapability struct {
+ // When true, clients may supply {@link RepositorySource.revision}.
+ Revision *bool `json:"revision,omitempty"`
+ // When true, clients may supply more than one repository.
+ MultipleRepositories *bool `json:"multipleRepositories,omitempty"`
+}
+
+// Presence capability for {@link ClientCapabilities.workingDirectoryInfo}.
+type WorkingDirectoryInfoCapability struct {
+}
+
// Optional capabilities a client declares during `initialize`.
//
// Each field is a presence flag: an empty object `{}` means "supported",
// absence means "not supported". Sub-fields on individual capabilities
// are reserved for future per-capability options.
type ClientCapabilities struct {
+ // Client accepts rich {@link WorkingDirectory} records as well as URI strings.
+ // Hosts project records to URIs when absent and retain this choice on reconnect.
+ WorkingDirectoryInfo *WorkingDirectoryInfoCapability `json:"workingDirectoryInfo,omitempty"`
// Client can render
// [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) — i.e.
// it can host the View sandbox, run the `ui/*` protocol against it,
@@ -601,6 +629,9 @@ type CreateSessionParams struct {
// and ignores the rest. Dispatch working-directory actions to change the set
// after the session has started.
WorkingDirectories []URI `json:"workingDirectories,omitempty"`
+ // Repositories to prepare instead of an explicit `workingDirectories` list.
+ // An empty list is equivalent to omission.
+ Repositories []RepositorySource `json:"repositories,omitempty"`
// Agent-specific configuration values collected via `resolveSessionConfig`.
// Keys and values correspond to the schema returned by the server.
Config map[string]json.RawMessage `json:"config,omitempty"`
@@ -697,9 +728,9 @@ type CreateChatParams struct {
// also snapshots and preserves that exact selected text in the created chat's
// origin; any `responsePartId` there is provenance only, not a live range.
Source *ChatSource `json:"source,omitempty"`
- // Initial working-directory subset for this chat. Every entry MUST be
- // present in the owning session's `workingDirectories`; the server MUST
- // reject any entry that is not. When absent, the chat inherits the full
+ // Initial working-directory URI subset for this chat. Every URI MUST match
+ // a URI string or record's `uri` in the owning session's `workingDirectories`;
+ // the server MUST reject any entry that does not. When absent, the chat inherits the full
// session set. Forked chats (those whose `source.kind` is `"fork"`) inherit
// the source chat's `workingDirectories`; this field is ignored for forks.
//
@@ -1440,6 +1471,8 @@ type ResolveSessionConfigParams struct {
Provider *string `json:"provider,omitempty"`
// Working directory for the session
WorkingDirectory *URI `json:"workingDirectory,omitempty"`
+ // Repositories used as configuration context; an empty list is equivalent to omission.
+ Repositories []RepositorySource `json:"repositories,omitempty"`
// Current user-filled configuration values
Config map[string]json.RawMessage `json:"config,omitempty"`
}
@@ -1467,6 +1500,8 @@ type SessionConfigCompletionsParams struct {
Provider *string `json:"provider,omitempty"`
// Working directory for the session
WorkingDirectory *URI `json:"workingDirectory,omitempty"`
+ // Repositories used as configuration context; an empty list is equivalent to omission.
+ Repositories []RepositorySource `json:"repositories,omitempty"`
// Current user-filled configuration values (provides context for the query)
Config map[string]json.RawMessage `json:"config,omitempty"`
// Property id from the schema to query values for
diff --git a/clients/go/ahptypes/notifications.generated.go b/clients/go/ahptypes/notifications.generated.go
index 5a5e955ad..5f0d42b3f 100644
--- a/clients/go/ahptypes/notifications.generated.go
+++ b/clients/go/ahptypes/notifications.generated.go
@@ -358,7 +358,9 @@ type PartialSessionSummary struct {
// MAY restrict to a subset via
// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
// chat that sets none operates against this full set.
- WorkingDirectories []URI `json:"workingDirectories,omitempty"`
+ // Entries are uniquely keyed by URI. Rich records require
+ // {@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs.
+ WorkingDirectories []WorkingDirectoryEntry `json:"workingDirectories,omitempty"`
// Lightweight summary of this session's inline annotations channel
// (`ahp-annotations:/`). Surfaced so badge UI can render
// annotation / entry counts without subscribing. Absent when the session
diff --git a/clients/go/ahptypes/roundtrip_fixture_test.go b/clients/go/ahptypes/roundtrip_fixture_test.go
index c8442d822..57e448a69 100644
--- a/clients/go/ahptypes/roundtrip_fixture_test.go
+++ b/clients/go/ahptypes/roundtrip_fixture_test.go
@@ -258,6 +258,38 @@ func decodeAndReencode(t *testing.T, name, typ, inputJSON string) string {
var v InitializeResult
dec(&v)
return enc(&v)
+ case "InitializeParams":
+ var v InitializeParams
+ dec(&v)
+ return enc(&v)
+ case "SubscribeResult":
+ var v SubscribeResult
+ dec(&v)
+ return enc(&v)
+ case "ReconnectResult":
+ var v ReconnectResult
+ dec(&v)
+ return enc(&v)
+ case "ListSessionsResult":
+ var v ListSessionsResult
+ dec(&v)
+ return enc(&v)
+ case "SessionSummaryChangedParams":
+ var v SessionSummaryChangedParams
+ dec(&v)
+ return enc(&v)
+ case "CreateSessionParams":
+ var v CreateSessionParams
+ dec(&v)
+ return enc(&v)
+ case "ResolveSessionConfigParams":
+ var v ResolveSessionConfigParams
+ dec(&v)
+ return enc(&v)
+ case "SessionConfigCompletionsParams":
+ var v SessionConfigCompletionsParams
+ dec(&v)
+ return enc(&v)
case "CreateChatParams":
var v CreateChatParams
dec(&v)
@@ -274,14 +306,6 @@ func decodeAndReencode(t *testing.T, name, typ, inputJSON string) string {
var v SubscribeParams
dec(&v)
return enc(&v)
- case "SubscribeResult":
- var v SubscribeResult
- dec(&v)
- return enc(&v)
- case "ReconnectResult":
- var v ReconnectResult
- dec(&v)
- return enc(&v)
case "ChannelFrameParams":
var v ChannelFrameParams
dec(&v)
diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go
index 545273128..464424970 100644
--- a/clients/go/ahptypes/state.generated.go
+++ b/clients/go/ahptypes/state.generated.go
@@ -6,13 +6,9 @@ package ahptypes
import (
"encoding/json"
+ "fmt"
)
-// Reference the encoding/json import to keep gofmt -d from
-// stripping it when a generated file has no struct that mentions
-// json.RawMessage directly (rare but possible). Compiled out.
-var _ = json.RawMessage(nil)
-
// ─── Enums ────────────────────────────────────────────────────────────
// How the server resumes sending data on a windowed subscription after
@@ -517,6 +513,15 @@ const (
SessionOriginKindAutomation SessionOriginKind = "automation"
)
+// How a working directory was prepared.
+type WorkingDirectoryOriginKind string
+
+const (
+ WorkingDirectoryOriginKindLocal WorkingDirectoryOriginKind = "local"
+ WorkingDirectoryOriginKindRepo WorkingDirectoryOriginKind = "repo"
+ WorkingDirectoryOriginKindWorktree WorkingDirectoryOriginKind = "worktree"
+)
+
// Operations the host currently permits for an automation.
//
// The list on {@link AutomationEntry.operations} is authoritative and may
@@ -895,6 +900,30 @@ type MultipleWorkingDirectoriesCapability struct {
PrimaryReplacement *bool `json:"primaryReplacement,omitempty"`
}
+// An actual working directory, uniquely keyed by `uri` within the session.
+type WorkingDirectory struct {
+ // Actual selected directory, which may be a repository subdirectory.
+ Uri URI `json:"uri"`
+ // Credential-free repository source association, not a checkout identity.
+ Repo *URI `json:"repo,omitempty"`
+ // Host-reported provenance; omission means unspecified.
+ Origin *WorkingDirectoryOrigin `json:"origin,omitempty"`
+}
+
+type LocalWorkingDirectoryOrigin struct {
+ Kind WorkingDirectoryOriginKind `json:"kind"`
+}
+
+type RepoWorkingDirectoryOrigin struct {
+ Kind WorkingDirectoryOriginKind `json:"kind"`
+}
+
+type WorktreeWorkingDirectoryOrigin struct {
+ Kind WorkingDirectoryOriginKind `json:"kind"`
+ // Main worktree associated with the host-prepared worktree.
+ MainWorktree URI `json:"mainWorktree"`
+}
+
type SessionModelInfo struct {
// Model identifier
Id string `json:"id"`
@@ -1063,7 +1092,9 @@ type SessionState struct {
// MAY restrict to a subset via
// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
// chat that sets none operates against this full set.
- WorkingDirectories []URI `json:"workingDirectories,omitempty"`
+ // Entries are uniquely keyed by URI. Rich records require
+ // {@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs.
+ WorkingDirectories []WorkingDirectoryEntry `json:"workingDirectories,omitempty"`
// Lightweight summary of this session's inline annotations channel
// (`ahp-annotations:/`). Surfaced so badge UI can render
// annotation / entry counts without subscribing. Absent when the session
@@ -1343,7 +1374,9 @@ type SessionSummary struct {
// MAY restrict to a subset via
// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
// chat that sets none operates against this full set.
- WorkingDirectories []URI `json:"workingDirectories,omitempty"`
+ // Entries are uniquely keyed by URI. Rich records require
+ // {@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs.
+ WorkingDirectories []WorkingDirectoryEntry `json:"workingDirectories,omitempty"`
// Lightweight summary of this session's inline annotations channel
// (`ahp-annotations:/`). Surfaced so badge UI can render
// annotation / entry counts without subscribing. Absent when the session
@@ -1472,13 +1505,14 @@ type ChatState struct {
Provider *string `json:"provider,omitempty"`
// The subset of the session's
// {@link SessionState.workingDirectories | `workingDirectories`} that this
- // chat's agent has tool access to. Every entry MUST be present in the owning
- // session's `workingDirectories`; servers MUST reject a
+ // chat's agent has tool access to. Every URI MUST match a URI string or a
+ // record's `uri` in the owning session's set; servers MUST reject a
// `chat/workingDirectorySet` action that violates this constraint.
//
// When absent, the chat inherits the full session set. When present but empty
// (not recommended), the chat has no working-directory tool access at all.
//
+ // Directory metadata belongs to the session, not the chat.
// Dispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to
// update the subset on a running chat.
WorkingDirectories []URI `json:"workingDirectories,omitempty"`
@@ -5340,6 +5374,50 @@ func (t *ToolInput) UnmarshalJSON(data []byte) error {
return nil
}
+// WorkingDirectoryEntry is a legacy URI or a complete working-directory record.
+type WorkingDirectoryEntry struct {
+ URI *URI
+ Directory *WorkingDirectory
+}
+
+func (d WorkingDirectoryEntry) GetURI() URI {
+ if d.Directory != nil {
+ return d.Directory.Uri
+ }
+ if d.URI != nil {
+ return *d.URI
+ }
+ return ""
+}
+
+func (d WorkingDirectoryEntry) MarshalJSON() ([]byte, error) {
+ if d.Directory != nil {
+ return json.Marshal(d.Directory)
+ }
+ if d.URI != nil {
+ return json.Marshal(*d.URI)
+ }
+ return nil, fmt.Errorf("WorkingDirectoryEntry requires a URI or directory")
+}
+
+func (d *WorkingDirectoryEntry) UnmarshalJSON(data []byte) error {
+ *d = WorkingDirectoryEntry{}
+ var uri *URI
+ if err := json.Unmarshal(data, &uri); err == nil && uri != nil {
+ d.URI = uri
+ return nil
+ }
+ var directory *WorkingDirectory
+ if err := json.Unmarshal(data, &directory); err != nil {
+ return err
+ }
+ if directory == nil {
+ return fmt.Errorf("WorkingDirectoryEntry requires a URI or directory")
+ }
+ d.Directory = directory
+ return nil
+}
+
// ─── Discriminated Unions ─────────────────────────────────────────────
// ResponsePart is a single part of a response stream (text, tool call, reasoning, content reference).
@@ -6759,6 +6837,92 @@ func (u SessionOrigin) MarshalJSON() ([]byte, error) {
return json.Marshal(object)
}
+// WorkingDirectoryOrigin is the host-reported working-directory provenance.
+type WorkingDirectoryOrigin struct {
+ Value isWorkingDirectoryOrigin
+}
+
+// isWorkingDirectoryOrigin is the marker interface implemented by every
+// concrete variant of WorkingDirectoryOrigin.
+type isWorkingDirectoryOrigin interface{ isWorkingDirectoryOrigin() }
+
+func (*LocalWorkingDirectoryOrigin) isWorkingDirectoryOrigin() {}
+func (*RepoWorkingDirectoryOrigin) isWorkingDirectoryOrigin() {}
+func (*WorktreeWorkingDirectoryOrigin) isWorkingDirectoryOrigin() {}
+
+// WorkingDirectoryOriginUnknown carries an unrecognized WorkingDirectoryOrigin 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 WorkingDirectoryOriginUnknown struct {
+ Raw json.RawMessage
+}
+
+func (*WorkingDirectoryOriginUnknown) isWorkingDirectoryOrigin() {}
+
+// UnmarshalJSON decodes the variant indicated by the "kind" discriminator.
+func (u *WorkingDirectoryOrigin) UnmarshalJSON(data []byte) error {
+ disc, _, err := readDiscriminator(data, "kind")
+ if err != nil {
+ return err
+ }
+ switch disc {
+ case "local":
+ var value LocalWorkingDirectoryOrigin
+ if err := json.Unmarshal(data, &value); err != nil {
+ return err
+ }
+ u.Value = &value
+ case "repo":
+ var value RepoWorkingDirectoryOrigin
+ if err := json.Unmarshal(data, &value); err != nil {
+ return err
+ }
+ u.Value = &value
+ case "worktree":
+ var value WorktreeWorkingDirectoryOrigin
+ 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 = &WorkingDirectoryOriginUnknown{Raw: raw}
+ }
+ return nil
+}
+
+// MarshalJSON encodes the active variant back to JSON.
+func (u WorkingDirectoryOrigin) MarshalJSON() ([]byte, error) {
+ if unk, ok := u.Value.(*WorkingDirectoryOriginUnknown); ok {
+ if len(unk.Raw) == 0 {
+ return []byte("null"), nil
+ }
+ return unk.Raw, nil
+ }
+ if u.Value == nil {
+ return []byte("null"), nil
+ }
+ data, err := json.Marshal(u.Value)
+ if err != nil {
+ return nil, err
+ }
+ var object map[string]json.RawMessage
+ if err := json.Unmarshal(data, &object); err != nil {
+ return nil, err
+ }
+ if object == nil {
+ return data, nil
+ }
+ switch u.Value.(type) {
+ case *LocalWorkingDirectoryOrigin:
+ object["kind"] = json.RawMessage("\"local\"")
+ case *RepoWorkingDirectoryOrigin:
+ object["kind"] = json.RawMessage("\"repo\"")
+ case *WorktreeWorkingDirectoryOrigin:
+ object["kind"] = json.RawMessage("\"worktree\"")
+ }
+ return json.Marshal(object)
+}
+
// AutomationTrigger is an automatic trigger for an automation.
type AutomationTrigger struct {
Value isAutomationTrigger
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt
index dd291e656..b2799ed38 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt
@@ -708,16 +708,21 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat
is StateActionSessionWorkingDirectorySet -> {
val list = state.workingDirectories ?: emptyList()
- if (list.contains(action.value.directory)) {
+ val idx = list.indexOfFirst { it.uri == action.value.directory.uri }
+ if (idx < 0) {
+ state.copy(workingDirectories = list + action.value.directory)
+ } else if (action.value.directory is WorkingDirectoryEntry.Uri) {
state
} else {
- state.copy(workingDirectories = list + action.value.directory)
+ val updated = list.toMutableList()
+ updated[idx] = action.value.directory
+ state.copy(workingDirectories = updated)
}
}
is StateActionSessionWorkingDirectoryRemoved -> {
val list = state.workingDirectories
- val idx = list?.indexOf(action.value.directory) ?: -1
+ val idx = list?.indexOfFirst { it.uri == action.value.directory } ?: -1
if (list == null || idx < 0) {
state
} else {
@@ -729,24 +734,27 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat
is StateActionSessionWorkingDirectoryReplaced -> {
val list = state.workingDirectories
- val idx = list?.indexOf(action.value.directory) ?: -1
+ val idx = list?.indexOfFirst { it.uri == action.value.directory } ?: -1
if (list == null || idx < 0) {
state
} else {
- val replacementIdx = list.indexOf(action.value.replacement)
+ val replacementIdx = list.indexOfFirst { it.uri == action.value.replacement.uri }
+ val replacement = if (action.value.replacement is WorkingDirectoryEntry.Uri && replacementIdx >= 0) {
+ list[replacementIdx]
+ } else {
+ action.value.replacement
+ }
+ val updated = list.toMutableList()
if (replacementIdx in 0 until idx) {
- state.copy(workingDirectories = list.filterIndexed { index, _ -> index != idx })
+ updated[replacementIdx] = replacement
+ updated.removeAt(idx)
} else {
- state.copy(
- workingDirectories = list.mapIndexedNotNull { index, directory ->
- when {
- index == idx -> action.value.replacement
- directory == action.value.replacement -> null
- else -> directory
- }
- },
- )
+ updated[idx] = replacement
+ if (replacementIdx > idx) {
+ updated.removeAt(replacementIdx)
+ }
}
+ state.copy(workingDirectories = updated)
}
}
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt
index 70d6a8322..eb743eb26 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt
@@ -943,7 +943,7 @@ data class SessionWorkingDirectorySetAction(
/**
* The working directory to grant the session's agent tool access to.
*/
- val directory: String
+ val directory: WorkingDirectoryEntry
)
@Serializable
@@ -963,9 +963,9 @@ data class SessionWorkingDirectoryReplacedAction(
*/
val directory: String,
/**
- * URI to place in the replaced entry's position.
+ * Complete entry to place in the replaced entry's position.
*/
- val replacement: String
+ val replacement: WorkingDirectoryEntry
)
@Serializable
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
index 12b5b8017..1528406fd 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt
@@ -330,6 +330,22 @@ internal object SideChatSourceSerializer : KSerializer {
}
}
+@Serializable
+data class RepositorySource(
+ /**
+ * Credential-free repository source URI.
+ */
+ val source: String,
+ /**
+ * Requested branch, tag, or commit. Omit to use the host's default revision.
+ */
+ val revision: String? = null,
+ /**
+ * Repository-relative selected folder; omit for the root. Hosts reject empty, absolute, or escaping paths.
+ */
+ val subdirectory: String? = null
+)
+
/**
* Opens a TCP connection from the host to a destination on the host's network.
*
@@ -491,6 +507,10 @@ data class InitializeResult(
* Suggested default directory for remote filesystem browsing
*/
val defaultDirectory: String? = null,
+ /**
+ * Host repository preparation support; absent when unsupported.
+ */
+ val repositoryPreparation: RepositoryPreparationCapability? = null,
/**
* Characters that, when typed in a {@link Message} input, SHOULD cause
* the client to issue a `completions` request with
@@ -537,8 +557,28 @@ data class InitializeResult(
val multipleChatProviders: MultipleChatProvidersCapability? = null
)
+@Serializable
+data class RepositoryPreparationCapability(
+ /**
+ * When true, clients may supply {@link RepositorySource.revision}.
+ */
+ val revision: Boolean? = null,
+ /**
+ * When true, clients may supply more than one repository.
+ */
+ val multipleRepositories: Boolean? = null
+)
+
+@Serializable
+class WorkingDirectoryInfoCapability
+
@Serializable
data class ClientCapabilities(
+ /**
+ * Client accepts rich {@link WorkingDirectory} records as well as URI strings.
+ * Hosts project records to URIs when absent and retain this choice on reconnect.
+ */
+ val workingDirectoryInfo: WorkingDirectoryInfoCapability? = null,
/**
* Client can render
* [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) — i.e.
@@ -893,6 +933,11 @@ data class CreateSessionParams(
* after the session has started.
*/
val workingDirectories: List? = null,
+ /**
+ * Repositories to prepare instead of an explicit `workingDirectories` list.
+ * An empty list is equivalent to omission.
+ */
+ val repositories: List? = null,
/**
* Agent-specific configuration values collected via `resolveSessionConfig`.
* Keys and values correspond to the schema returned by the server.
@@ -974,9 +1019,9 @@ data class CreateChatParams(
*/
val source: ChatSource? = null,
/**
- * Initial working-directory subset for this chat. Every entry MUST be
- * present in the owning session's `workingDirectories`; the server MUST
- * reject any entry that is not. When absent, the chat inherits the full
+ * Initial working-directory URI subset for this chat. Every URI MUST match
+ * a URI string or record's `uri` in the owning session's `workingDirectories`;
+ * the server MUST reject any entry that does not. When absent, the chat inherits the full
* session set. Forked chats (those whose `source.kind` is `"fork"`) inherit
* the source chat's `workingDirectories`; this field is ignored for forks.
*
@@ -1719,6 +1764,10 @@ data class ResolveSessionConfigParams(
* Working directory for the session
*/
val workingDirectory: String? = null,
+ /**
+ * Repositories used as configuration context; an empty list is equivalent to omission.
+ */
+ val repositories: List? = null,
/**
* Current user-filled configuration values
*/
@@ -1849,6 +1898,10 @@ data class SessionConfigCompletionsParams(
* Working directory for the session
*/
val workingDirectory: String? = null,
+ /**
+ * Repositories used as configuration context; an empty list is equivalent to omission.
+ */
+ val repositories: List? = null,
/**
* Current user-filled configuration values (provides context for the query)
*/
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt
index 02a8aefe9..9a09ee108 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Notifications.generated.kt
@@ -390,8 +390,10 @@ data class PartialSessionSummary(
* MAY restrict to a subset via
* {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
* chat that sets none operates against this full set.
+ * Entries are uniquely keyed by URI. Rich records require
+ * {@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs.
*/
- val workingDirectories: List? = null,
+ val workingDirectories: List? = null,
/**
* Lightweight summary of this session's inline annotations channel
* (`ahp-annotations:/`). Surfaced so badge UI can render
diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
index 21449a12f..af84dc502 100644
--- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
+++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
@@ -1102,6 +1102,29 @@ internal object SessionOriginKindSerializer : KSerializer {
SessionOriginKind(decoder.decodeString())
}
+/**
+ * How a working directory was prepared.
+ */
+@Serializable(with = WorkingDirectoryOriginKindSerializer::class)
+@JvmInline
+value class WorkingDirectoryOriginKind(val rawValue: String) {
+ companion object {
+ val LOCAL: WorkingDirectoryOriginKind = WorkingDirectoryOriginKind("local")
+ val REPO: WorkingDirectoryOriginKind = WorkingDirectoryOriginKind("repo")
+ val WORKTREE: WorkingDirectoryOriginKind = WorkingDirectoryOriginKind("worktree")
+ }
+}
+
+internal object WorkingDirectoryOriginKindSerializer : KSerializer {
+ override val descriptor: SerialDescriptor =
+ PrimitiveSerialDescriptor("WorkingDirectoryOriginKind", PrimitiveKind.STRING)
+ override fun serialize(encoder: Encoder, value: WorkingDirectoryOriginKind) {
+ encoder.encodeString(value.rawValue)
+ }
+ override fun deserialize(decoder: Decoder): WorkingDirectoryOriginKind =
+ WorkingDirectoryOriginKind(decoder.decodeString())
+}
+
/**
* Operations the host currently permits for an automation.
*
@@ -1647,6 +1670,41 @@ data class MultipleWorkingDirectoriesCapability(
val primaryReplacement: Boolean? = null
)
+@Serializable
+data class WorkingDirectory(
+ /**
+ * Actual selected directory, which may be a repository subdirectory.
+ */
+ val uri: String,
+ /**
+ * Credential-free repository source association, not a checkout identity.
+ */
+ val repo: String? = null,
+ /**
+ * Host-reported provenance; omission means unspecified.
+ */
+ val origin: WorkingDirectoryOrigin? = null
+)
+
+@Serializable
+data class LocalWorkingDirectoryOrigin(
+ val kind: WorkingDirectoryOriginKind
+)
+
+@Serializable
+data class RepoWorkingDirectoryOrigin(
+ val kind: WorkingDirectoryOriginKind
+)
+
+@Serializable
+data class WorktreeWorkingDirectoryOrigin(
+ val kind: WorkingDirectoryOriginKind,
+ /**
+ * Main worktree associated with the host-prepared worktree.
+ */
+ val mainWorktree: String
+)
+
@Serializable
data class SessionModelInfo(
/**
@@ -1902,13 +1960,14 @@ data class ChatState(
/**
* The subset of the session's
* {@link SessionState.workingDirectories | `workingDirectories`} that this
- * chat's agent has tool access to. Every entry MUST be present in the owning
- * session's `workingDirectories`; servers MUST reject a
+ * chat's agent has tool access to. Every URI MUST match a URI string or a
+ * record's `uri` in the owning session's set; servers MUST reject a
* `chat/workingDirectorySet` action that violates this constraint.
*
* When absent, the chat inherits the full session set. When present but empty
* (not recommended), the chat has no working-directory tool access at all.
*
+ * Directory metadata belongs to the session, not the chat.
* Dispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to
* update the subset on a running chat.
*/
@@ -2113,8 +2172,10 @@ data class SessionState(
* MAY restrict to a subset via
* {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
* chat that sets none operates against this full set.
+ * Entries are uniquely keyed by URI. Rich records require
+ * {@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs.
*/
- val workingDirectories: List? = null,
+ val workingDirectories: List? = null,
/**
* Lightweight summary of this session's inline annotations channel
* (`ahp-annotations:/`). Surfaced so badge UI can render
@@ -2518,8 +2579,10 @@ data class SessionSummary(
* MAY restrict to a subset via
* {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
* chat that sets none operates against this full set.
+ * Entries are uniquely keyed by URI. Rich records require
+ * {@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs.
*/
- val workingDirectories: List? = null,
+ val workingDirectories: List? = null,
/**
* Lightweight summary of this session's inline annotations channel
* (`ahp-annotations:/`). Surfaced so badge UI can render
@@ -6901,6 +6964,50 @@ internal object ToolInputSerializer : KSerializer {
}
}
+/** A legacy URI or a complete working-directory record. */
+@Serializable(with = WorkingDirectoryEntrySerializer::class)
+sealed interface WorkingDirectoryEntry {
+ val uri: String
+
+ data class Uri(val value: String) : WorkingDirectoryEntry {
+ override val uri: String get() = value
+ }
+ data class Directory(val value: WorkingDirectory) : WorkingDirectoryEntry {
+ override val uri: String get() = value.uri
+ }
+}
+
+internal object WorkingDirectoryEntrySerializer : KSerializer {
+ override val descriptor: SerialDescriptor =
+ buildClassSerialDescriptor("WorkingDirectoryEntry")
+
+ override fun deserialize(decoder: Decoder): WorkingDirectoryEntry {
+ val input = decoder as? JsonDecoder
+ ?: error("WorkingDirectoryEntry requires JSON")
+ return when (val element = input.decodeJsonElement()) {
+ is JsonPrimitive -> {
+ require(element.isString) { "WorkingDirectoryEntry URI must be a string" }
+ WorkingDirectoryEntry.Uri(element.content)
+ }
+ is JsonObject -> WorkingDirectoryEntry.Directory(
+ input.json.decodeFromJsonElement(WorkingDirectory.serializer(), element),
+ )
+ else -> error("WorkingDirectoryEntry requires a URI or directory")
+ }
+ }
+
+ override fun serialize(encoder: Encoder, value: WorkingDirectoryEntry) {
+ val output = encoder as? JsonEncoder
+ ?: error("WorkingDirectoryEntry requires JSON")
+ val element = when (value) {
+ is WorkingDirectoryEntry.Uri -> JsonPrimitive(value.value)
+ is WorkingDirectoryEntry.Directory ->
+ output.json.encodeToJsonElement(WorkingDirectory.serializer(), value.value)
+ }
+ output.encodeJsonElement(element)
+ }
+}
+
// ─── Discriminated Unions ───────────────────────────────────────────────────
@Serializable(with = ChatOriginSerializer::class)
@@ -7982,6 +8089,67 @@ internal object SessionOriginSerializer : KSerializer {
}
}
+@Serializable(with = WorkingDirectoryOriginSerializer::class)
+sealed interface WorkingDirectoryOrigin
+
+@JvmInline
+value class WorkingDirectoryOriginLocal(val value: LocalWorkingDirectoryOrigin) : WorkingDirectoryOrigin
+@JvmInline
+value class WorkingDirectoryOriginRepo(val value: RepoWorkingDirectoryOrigin) : WorkingDirectoryOrigin
+@JvmInline
+value class WorkingDirectoryOriginWorktree(val value: WorktreeWorkingDirectoryOrigin) : WorkingDirectoryOrigin
+/**
+ * Forward-compat catch-all for unknown WorkingDirectoryOrigin discriminators.
+ *
+ * Older clients may receive newer wire variants they don't recognise; capturing
+ * the raw `JsonObject` lets such payloads round-trip through the client unchanged.
+ * Reducers handle this variant conservatively on a per-union basis (typically
+ * as a no-op, but see `Reducers.kt` for the exact treatment).
+ */
+@JvmInline
+value class WorkingDirectoryOriginUnknown(val raw: JsonObject) : WorkingDirectoryOrigin
+
+internal object WorkingDirectoryOriginSerializer : KSerializer {
+ override val descriptor: SerialDescriptor =
+ buildClassSerialDescriptor("WorkingDirectoryOrigin")
+
+ override fun deserialize(decoder: Decoder): WorkingDirectoryOrigin {
+ val input = decoder as? JsonDecoder
+ ?: error("WorkingDirectoryOrigin can only be deserialized from JSON")
+ val element = input.decodeJsonElement()
+ val obj = element as? JsonObject
+ ?: error("Expected JsonObject for WorkingDirectoryOrigin")
+ val discriminant = (obj["kind"] as? JsonPrimitive)?.content
+ ?: return WorkingDirectoryOriginUnknown(obj)
+ return when (discriminant) {
+ "local" -> WorkingDirectoryOriginLocal(input.json.decodeFromJsonElement(LocalWorkingDirectoryOrigin.serializer(), element))
+ "repo" -> WorkingDirectoryOriginRepo(input.json.decodeFromJsonElement(RepoWorkingDirectoryOrigin.serializer(), element))
+ "worktree" -> WorkingDirectoryOriginWorktree(input.json.decodeFromJsonElement(WorktreeWorkingDirectoryOrigin.serializer(), element))
+ else -> WorkingDirectoryOriginUnknown(obj)
+ }
+ }
+
+ override fun serialize(encoder: Encoder, value: WorkingDirectoryOrigin) {
+ val output = encoder as? JsonEncoder
+ ?: error("WorkingDirectoryOrigin can only be serialized to JSON")
+ val element: JsonElement = when (value) {
+ is WorkingDirectoryOriginLocal -> output.json.encodeToJsonElement(LocalWorkingDirectoryOrigin.serializer(), value.value)
+ is WorkingDirectoryOriginRepo -> output.json.encodeToJsonElement(RepoWorkingDirectoryOrigin.serializer(), value.value)
+ is WorkingDirectoryOriginWorktree -> output.json.encodeToJsonElement(WorktreeWorkingDirectoryOrigin.serializer(), value.value)
+ is WorkingDirectoryOriginUnknown -> value.raw
+ }
+ val encodedObject = element.jsonObject.toMutableMap()
+ val discriminant = when (value) {
+ is WorkingDirectoryOriginLocal -> "local"
+ is WorkingDirectoryOriginRepo -> "repo"
+ is WorkingDirectoryOriginWorktree -> "worktree"
+ is WorkingDirectoryOriginUnknown -> null
+ }
+ if (discriminant != null) encodedObject["kind"] = JsonPrimitive(discriminant)
+ output.encodeJsonElement(JsonObject(encodedObject))
+ }
+}
+
@Serializable(with = AutomationTriggerSerializer::class)
sealed interface AutomationTrigger
diff --git a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt
index 38b1b7f3c..31db5c2ff 100644
--- a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt
+++ b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt
@@ -34,11 +34,15 @@ import com.microsoft.agenthostprotocol.generated.ChangesetOperationTarget
import com.microsoft.agenthostprotocol.generated.ChatSource
import com.microsoft.agenthostprotocol.generated.CreateChatParams
import com.microsoft.agenthostprotocol.generated.Customization
+import com.microsoft.agenthostprotocol.generated.CreateSessionParams
import com.microsoft.agenthostprotocol.generated.Implementation
import com.microsoft.agenthostprotocol.generated.InitializeResult
-import com.microsoft.agenthostprotocol.generated.SubscribeParams
+import com.microsoft.agenthostprotocol.generated.InitializeParams
import com.microsoft.agenthostprotocol.generated.SubscribeResult
import com.microsoft.agenthostprotocol.generated.ReconnectResult
+import com.microsoft.agenthostprotocol.generated.ListSessionsResult
+import com.microsoft.agenthostprotocol.generated.SessionSummaryChangedParams
+import com.microsoft.agenthostprotocol.generated.SubscribeParams
import com.microsoft.agenthostprotocol.generated.ChannelFrameParams
import com.microsoft.agenthostprotocol.generated.ChannelCreditParams
import com.microsoft.agenthostprotocol.generated.ReconnectParams
@@ -53,7 +57,9 @@ import com.microsoft.agenthostprotocol.generated.JsonRpcNotification
import com.microsoft.agenthostprotocol.generated.JsonRpcRequest
import com.microsoft.agenthostprotocol.generated.JsonRpcSuccessResponse
import com.microsoft.agenthostprotocol.generated.PartialSessionSummary
+import com.microsoft.agenthostprotocol.generated.ResolveSessionConfigParams
import com.microsoft.agenthostprotocol.generated.SessionAddedParams
+import com.microsoft.agenthostprotocol.generated.SessionConfigCompletionsParams
import com.microsoft.agenthostprotocol.generated.ChatInputQuestion
import com.microsoft.agenthostprotocol.generated.SessionStatus
import com.microsoft.agenthostprotocol.generated.SessionSummary
@@ -267,6 +273,12 @@ class RoundTripCorpusTest {
"PartialSessionSummary" -> rt(PartialSessionSummary.serializer())
"Implementation" -> rt(Implementation.serializer())
"InitializeResult" -> rt(InitializeResult.serializer())
+ "InitializeParams" -> rt(InitializeParams.serializer())
+ "ListSessionsResult" -> rt(ListSessionsResult.serializer())
+ "SessionSummaryChangedParams" -> rt(SessionSummaryChangedParams.serializer())
+ "CreateSessionParams" -> rt(CreateSessionParams.serializer())
+ "ResolveSessionConfigParams" -> rt(ResolveSessionConfigParams.serializer())
+ "SessionConfigCompletionsParams" -> rt(SessionConfigCompletionsParams.serializer())
"CreateChatParams" -> rt(CreateChatParams.serializer())
"ChatSource" -> rt(ChatSource.serializer())
"Snapshot" -> rt(Snapshot.serializer())
diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs
index dc6fe0f7c..9274b1d80 100644
--- a/clients/rust/crates/ahp-types/src/actions.rs
+++ b/clients/rust/crates/ahp-types/src/actions.rs
@@ -24,6 +24,7 @@ use crate::state::{
SessionInputRequest, SideChatSelection, TerminalClaim, TerminalInfo, TextRange,
ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallContributor, ToolCallResult,
ToolCallRiskAssessment, ToolDefinition, ToolInput, ToolResultContent, Turn, UsageInfo,
+ WorkingDirectoryEntry,
};
// ─── ActionType ──────────────────────────────────────────────────────
@@ -1339,18 +1340,19 @@ pub struct SessionActiveClientRemovedAction {
pub client_id: String,
}
-/// A working directory was added to the session's
+/// A working directory was added or updated in the session's
/// {@link SessionState.workingDirectories} set.
///
-/// Membership semantics keyed by the directory URI: the reducer appends
-/// `directory` when the set does not already contain it (creating the set if
-/// absent) and is a no-op when it is already present. Only valid when the agent
-/// advertises {@link AgentCapabilities.multipleWorkingDirectories}.
+/// Upsert keyed by URI. A rich record replaces the existing entry in place;
+/// a URI-only duplicate preserves known metadata. Only valid when the agent
+/// advertises {@link AgentCapabilities.multipleWorkingDirectories}. Clients
+/// MUST omit `repo` and use only local or absent `origin`; the host validates
+/// and enriches the entry before accepting it.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionWorkingDirectorySetAction {
/// The working directory to grant the session's agent tool access to.
- pub directory: Uri,
+ pub directory: WorkingDirectoryEntry,
}
/// A working directory was removed from the session's
@@ -1382,6 +1384,8 @@ pub struct SessionWorkingDirectoryRemovedAction {
/// for example, `[A, B, C]` with `B → C` becomes `[A, C]`. When it occurs
/// before the target, it retains its earlier position and the target is removed;
/// `[A, B, C]` with `C → A` becomes `[A, B]`.
+/// A rich replacement supplies the complete entry; a URI-only replacement
+/// preserves metadata already known for that URI.
///
/// Only valid when the agent advertises
/// {@link AgentCapabilities.multipleWorkingDirectories}. Replacing index `0`
@@ -1389,13 +1393,14 @@ pub struct SessionWorkingDirectoryRemovedAction {
/// {@link MultipleWorkingDirectoriesCapability.primaryReplacement}; clients
/// MUST NOT target an immutable primary. The host MUST validate and apply its
/// backend side effect before broadcasting an accepted action, or reject it.
+/// Clients MUST omit `repo` and use only local or absent `origin`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionWorkingDirectoryReplacedAction {
/// URI of the existing entry to replace.
pub directory: Uri,
- /// URI to place in the replaced entry's position.
- pub replacement: Uri,
+ /// Complete entry to place in the replaced entry's position.
+ pub replacement: WorkingDirectoryEntry,
}
/// A working directory was added to this chat's
@@ -1404,8 +1409,9 @@ pub struct SessionWorkingDirectoryReplacedAction {
/// Membership semantics keyed by the directory URI: the reducer appends
/// `directory` when the chat's subset does not already contain it (creating the
/// subset if absent) and is a no-op when it is already present. `directory` MUST
-/// be one of the owning session's {@link SessionState.workingDirectories}; a host
-/// MUST reject a directory that is not. Only valid when the agent advertises
+/// match a URI string or record's `uri` in the owning session's
+/// {@link SessionState.workingDirectories}; a host MUST reject a directory that
+/// does not. Only valid when the agent advertises
/// {@link AgentCapabilities.multipleWorkingDirectories}.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs
index 6a5b53628..fe45126dc 100644
--- a/clients/rust/crates/ahp-types/src/commands.rs
+++ b/clients/rust/crates/ahp-types/src/commands.rs
@@ -266,6 +266,20 @@ impl<'de> serde::Deserialize<'de> for AuthenticateTargetKind {
// ─── Command Payloads ─────────────────────────────────────────────────
+/// Requested repository source, not a resolved working directory.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct RepositorySource {
+ /// Credential-free repository source URI.
+ pub source: Uri,
+ /// Requested branch, tag, or commit. Omit to use the host's default revision.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub revision: Option,
+ /// Repository-relative selected folder; omit for the root. Hosts reject empty, absolute, or escaping paths.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub subdirectory: Option,
+}
+
/// Opens a TCP connection from the host to a destination on the host's network.
///
/// The client chooses the channel URI. This command does not subscribe: after it
@@ -415,6 +429,9 @@ pub struct InitializeResult {
/// Suggested default directory for remote filesystem browsing
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_directory: Option,
+ /// Host repository preparation support; absent when unsupported.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub repository_preparation: Option,
/// Characters that, when typed in a {@link Message} input, SHOULD cause
/// the client to issue a `completions` request with
/// {@link CompletionItemKind.UserMessage}. Typically includes characters like
@@ -455,6 +472,24 @@ pub struct InitializeResult {
pub multiple_chat_providers: Option,
}
+/// Host support for repository preparation at session creation. Presence
+/// alone supports one repository at its default revision.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
+#[serde(rename_all = "camelCase")]
+pub struct RepositoryPreparationCapability {
+ /// When true, clients may supply {@link RepositorySource.revision}.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub revision: Option,
+ /// When true, clients may supply more than one repository.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub multiple_repositories: Option,
+}
+
+/// Presence capability for {@link ClientCapabilities.workingDirectoryInfo}.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct WorkingDirectoryInfoCapability {}
+
/// Optional capabilities a client declares during `initialize`.
///
/// Each field is a presence flag: an empty object `{}` means "supported",
@@ -463,6 +498,10 @@ pub struct InitializeResult {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ClientCapabilities {
+ /// Client accepts rich {@link WorkingDirectory} records as well as URI strings.
+ /// Hosts project records to URIs when absent and retain this choice on reconnect.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub working_directory_info: Option,
/// Client can render
/// [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) — i.e.
/// it can host the View sandbox, run the `ui/*` protocol against it,
@@ -877,6 +916,10 @@ pub struct CreateSessionParams {
/// after the session has started.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub working_directories: Option>,
+ /// Repositories to prepare instead of an explicit `workingDirectories` list.
+ /// An empty list is equivalent to omission.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub repositories: Option>,
/// Agent-specific configuration values collected via `resolveSessionConfig`.
/// Keys and values correspond to the schema returned by the server.
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -985,9 +1028,9 @@ pub struct CreateChatParams {
/// origin; any `responsePartId` there is provenance only, not a live range.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option,
- /// Initial working-directory subset for this chat. Every entry MUST be
- /// present in the owning session's `workingDirectories`; the server MUST
- /// reject any entry that is not. When absent, the chat inherits the full
+ /// Initial working-directory URI subset for this chat. Every URI MUST match
+ /// a URI string or record's `uri` in the owning session's `workingDirectories`;
+ /// the server MUST reject any entry that does not. When absent, the chat inherits the full
/// session set. Forked chats (those whose `source.kind` is `"fork"`) inherit
/// the source chat's `workingDirectories`; this field is ignored for forks.
///
@@ -1850,6 +1893,9 @@ pub struct ResolveSessionConfigParams {
/// Working directory for the session
#[serde(default, skip_serializing_if = "Option::is_none")]
pub working_directory: Option,
+ /// Repositories used as configuration context; an empty list is equivalent to omission.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub repositories: Option>,
/// Current user-filled configuration values
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option,
@@ -1885,6 +1931,9 @@ pub struct SessionConfigCompletionsParams {
/// Working directory for the session
#[serde(default, skip_serializing_if = "Option::is_none")]
pub working_directory: Option,
+ /// Repositories used as configuration context; an empty list is equivalent to omission.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub repositories: Option>,
/// Current user-filled configuration values (provides context for the query)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option,
diff --git a/clients/rust/crates/ahp-types/src/notifications.rs b/clients/rust/crates/ahp-types/src/notifications.rs
index 60e47bca1..71b362d5e 100644
--- a/clients/rust/crates/ahp-types/src/notifications.rs
+++ b/clients/rust/crates/ahp-types/src/notifications.rs
@@ -15,7 +15,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr};
use crate::state::{
AgentSelection, AnnotationsSummary, ChangesSummary, Changeset, FileEdit, ModelSelection,
ProjectInfo, ProtectedResourceMetadata, SessionChatSummary, SessionOrigin, SessionStatus,
- SessionSummary, Snapshot,
+ SessionSummary, Snapshot, WorkingDirectoryEntry,
};
// ─── Enums ────────────────────────────────────────────────────────────
@@ -431,8 +431,10 @@ pub struct PartialSessionSummary {
/// MAY restrict to a subset via
/// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
/// chat that sets none operates against this full set.
+ /// Entries are uniquely keyed by URI. Rich records require
+ /// {@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs.
#[serde(default, skip_serializing_if = "Option::is_none")]
- pub working_directories: Option>,
+ pub working_directories: Option>,
/// Lightweight summary of this session's inline annotations channel
/// (`ahp-annotations:/`). Surfaced so badge UI can render
/// annotation / entry counts without subscribing. Absent when the session
diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs
index dfbb59ec3..dbd74ee35 100644
--- a/clients/rust/crates/ahp-types/src/state.rs
+++ b/clients/rust/crates/ahp-types/src/state.rs
@@ -1431,6 +1431,45 @@ impl<'de> serde::Deserialize<'de> for SessionOriginKind {
}
}
+/// How a working directory was prepared.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum WorkingDirectoryOriginKind {
+ Local,
+ Repo,
+ Worktree,
+ /// Unknown raw value from a newer protocol version, preserved verbatim.
+ Unknown(String),
+}
+
+impl serde::Serialize for WorkingDirectoryOriginKind {
+ fn serialize(&self, serializer: S) -> Result
+ where
+ S: serde::Serializer,
+ {
+ match self {
+ Self::Local => serializer.serialize_str("local"),
+ Self::Repo => serializer.serialize_str("repo"),
+ Self::Worktree => serializer.serialize_str("worktree"),
+ Self::Unknown(value) => serializer.serialize_str(value),
+ }
+ }
+}
+
+impl<'de> serde::Deserialize<'de> for WorkingDirectoryOriginKind {
+ fn deserialize(deserializer: D) -> Result
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let raw = ::deserialize(deserializer)?;
+ Ok(match raw.as_str() {
+ "local" => Self::Local,
+ "repo" => Self::Repo,
+ "worktree" => Self::Worktree,
+ _ => Self::Unknown(raw),
+ })
+ }
+}
+
/// Operations the host currently permits for an automation.
///
/// The list on {@link AutomationEntry.operations} is authoritative and may
@@ -1960,6 +1999,35 @@ pub struct MultipleWorkingDirectoriesCapability {
pub primary_replacement: Option,
}
+/// An actual working directory, uniquely keyed by `uri` within the session.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct WorkingDirectory {
+ /// Actual selected directory, which may be a repository subdirectory.
+ pub uri: Uri,
+ /// Credential-free repository source association, not a checkout identity.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub repo: Option,
+ /// Host-reported provenance; omission means unspecified.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub origin: Option,
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct LocalWorkingDirectoryOrigin {}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct RepoWorkingDirectoryOrigin {}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct WorktreeWorkingDirectoryOrigin {
+ /// Main worktree associated with the host-prepared worktree.
+ pub main_worktree: Uri,
+}
+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionModelInfo {
@@ -2189,13 +2257,14 @@ pub struct ChatState {
pub provider: Option,
/// The subset of the session's
/// {@link SessionState.workingDirectories | `workingDirectories`} that this
- /// chat's agent has tool access to. Every entry MUST be present in the owning
- /// session's `workingDirectories`; servers MUST reject a
+ /// chat's agent has tool access to. Every URI MUST match a URI string or a
+ /// record's `uri` in the owning session's set; servers MUST reject a
/// `chat/workingDirectorySet` action that violates this constraint.
///
/// When absent, the chat inherits the full session set. When present but empty
/// (not recommended), the chat has no working-directory tool access at all.
///
+ /// Directory metadata belongs to the session, not the chat.
/// Dispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to
/// update the subset on a running chat.
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -2481,8 +2550,10 @@ pub struct SessionState {
/// MAY restrict to a subset via
/// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
/// chat that sets none operates against this full set.
+ /// Entries are uniquely keyed by URI. Rich records require
+ /// {@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs.
#[serde(default, skip_serializing_if = "Option::is_none")]
- pub working_directories: Option>,
+ pub working_directories: Option>,
/// Lightweight summary of this session's inline annotations channel
/// (`ahp-annotations:/`). Surfaced so badge UI can render
/// annotation / entry counts without subscribing. Absent when the session
@@ -2821,8 +2892,10 @@ pub struct SessionSummary {
/// MAY restrict to a subset via
/// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
/// chat that sets none operates against this full set.
+ /// Entries are uniquely keyed by URI. Rich records require
+ /// {@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs.
#[serde(default, skip_serializing_if = "Option::is_none")]
- pub working_directories: Option>,
+ pub working_directories: Option>,
/// Lightweight summary of this session's inline annotations channel
/// (`ahp-annotations:/`). Surfaced so badge UI can render
/// annotation / entry counts without subscribing. Absent when the session
@@ -6697,6 +6770,35 @@ pub enum ToolInput {
ContentRef(ContentRef),
}
+/// A legacy URI or a complete working-directory record.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum WorkingDirectoryEntry {
+ Uri(Uri),
+ Directory(WorkingDirectory),
+}
+
+impl WorkingDirectoryEntry {
+ pub fn uri(&self) -> &Uri {
+ match self {
+ Self::Uri(uri) => uri,
+ Self::Directory(directory) => &directory.uri,
+ }
+ }
+}
+
+impl From for WorkingDirectoryEntry {
+ fn from(uri: Uri) -> Self {
+ Self::Uri(uri)
+ }
+}
+
+impl From for WorkingDirectoryEntry {
+ fn from(directory: WorkingDirectory) -> Self {
+ Self::Directory(directory)
+ }
+}
+
// ─── Discriminated Unions ─────────────────────────────────────────────
/// How a chat came into existence.
@@ -7077,6 +7179,22 @@ pub enum SessionOrigin {
Unknown(serde_json::Value),
}
+/// Host-reported working-directory provenance.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(tag = "kind")]
+pub enum WorkingDirectoryOrigin {
+ #[serde(rename = "local")]
+ Local(LocalWorkingDirectoryOrigin),
+ #[serde(rename = "repo")]
+ Repo(RepoWorkingDirectoryOrigin),
+ #[serde(rename = "worktree")]
+ Worktree(WorktreeWorkingDirectoryOrigin),
+ /// Unknown or future variant — preserved as raw JSON for round-trip fidelity.
+ /// Reducers treat this as a no-op.
+ #[serde(untagged)]
+ Unknown(serde_json::Value),
+}
+
/// Automatic trigger for an automation.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind")]
diff --git a/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs b/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs
index 330f28625..0f5ea8906 100644
--- a/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs
+++ b/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs
@@ -30,14 +30,16 @@ use ahp_types::{
actions::{ActionEnvelope, StateAction},
commands::{
AuthenticateParams, ChangesetOperationTarget, ChatSource, CreateChatParams,
- CreateTcpConnectionParams, Implementation, InitializeResult, ReconnectParams,
- ReconnectResult, ResourceReadParams, ResourceReadResult, SubscribeParams, SubscribeResult,
+ CreateSessionParams, CreateTcpConnectionParams, Implementation, InitializeParams,
+ InitializeResult, ListSessionsResult, ReconnectParams, ReconnectResult,
+ ResolveSessionConfigParams, ResourceReadParams, ResourceReadResult,
+ SessionConfigCompletionsParams, SubscribeParams, SubscribeResult,
},
common::StringOrMarkdown,
messages::JsonRpcMessage,
notifications::{
ChannelCreditParams, ChannelFrameParams, PartialSessionSummary, SessionAddedParams,
- TcpDataParams, TcpEofParams, TcpResetParams,
+ SessionSummaryChangedParams, TcpDataParams, TcpEofParams, TcpResetParams,
},
state::{ChatInputQuestion, Customization, SessionStatus, SessionSummary, Snapshot},
version::{PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS},
@@ -230,6 +232,12 @@ fn decode_and_reencode(file: &str, type_name: &str, input_json: &str) -> Result<
"PartialSessionSummary" => round_trip!(PartialSessionSummary),
"Implementation" => round_trip!(Implementation),
"InitializeResult" => round_trip!(InitializeResult),
+ "InitializeParams" => round_trip!(InitializeParams),
+ "ListSessionsResult" => round_trip!(ListSessionsResult),
+ "SessionSummaryChangedParams" => round_trip!(SessionSummaryChangedParams),
+ "CreateSessionParams" => round_trip!(CreateSessionParams),
+ "ResolveSessionConfigParams" => round_trip!(ResolveSessionConfigParams),
+ "SessionConfigCompletionsParams" => round_trip!(SessionConfigCompletionsParams),
"CreateChatParams" => round_trip!(CreateChatParams),
"ChatSource" => round_trip!(ChatSource),
"Snapshot" => round_trip!(Snapshot),
diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs
index 702224823..7536a982a 100644
--- a/clients/rust/crates/ahp/src/reducers.rs
+++ b/clients/rust/crates/ahp/src/reducers.rs
@@ -68,7 +68,7 @@ use ahp_types::state::{
ToolCallCancelledState, ToolCallCompletedState, ToolCallConfirmationReason,
ToolCallContributor, ToolCallPendingConfirmationState, ToolCallPendingResultConfirmationState,
ToolCallResponsePart, ToolCallRunningState, ToolCallState, ToolCallStatus,
- ToolCallStreamingState, ToolInput, Turn, TurnState,
+ ToolCallStreamingState, ToolInput, Turn, TurnState, WorkingDirectoryEntry,
};
use jiff::{SignedDuration, Timestamp};
@@ -881,17 +881,21 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) -
}
StateAction::SessionWorkingDirectorySet(a) => {
let list = state.working_directories.get_or_insert_with(Vec::new);
- if list.contains(&a.directory) {
- return ReduceOutcome::NoOp;
+ if let Some(idx) = list.iter().position(|d| d.uri() == a.directory.uri()) {
+ if matches!(a.directory, WorkingDirectoryEntry::Uri(_)) {
+ return ReduceOutcome::NoOp;
+ }
+ list[idx] = a.directory.clone();
+ } else {
+ list.push(a.directory.clone());
}
- list.push(a.directory.clone());
ReduceOutcome::Applied
}
StateAction::SessionWorkingDirectoryRemoved(a) => {
let Some(list) = state.working_directories.as_mut() else {
return ReduceOutcome::NoOp;
};
- let Some(idx) = list.iter().position(|d| *d == a.directory) else {
+ let Some(idx) = list.iter().position(|d| d.uri() == &a.directory) else {
return ReduceOutcome::NoOp;
};
list.remove(idx);
@@ -901,25 +905,26 @@ pub fn apply_action_to_session(state: &mut SessionState, action: &StateAction) -
let Some(list) = state.working_directories.as_mut() else {
return ReduceOutcome::NoOp;
};
- let Some(idx) = list.iter().position(|directory| directory == &a.directory) else {
+ let Some(idx) = list
+ .iter()
+ .position(|directory| directory.uri() == &a.directory)
+ else {
return ReduceOutcome::NoOp;
};
- if list[..idx]
- .iter()
- .any(|directory| directory == &a.replacement)
- {
+ let replacement_idx = list.iter().position(|d| d.uri() == a.replacement.uri());
+ let replacement = match (&a.replacement, replacement_idx) {
+ (WorkingDirectoryEntry::Uri(_), Some(i)) => list[i].clone(),
+ _ => a.replacement.clone(),
+ };
+ if let Some(i) = replacement_idx.filter(|&i| i < idx) {
+ list[i] = replacement;
list.remove(idx);
- return ReduceOutcome::Applied;
- }
- let mut updated = Vec::with_capacity(list.len());
- for (i, directory) in list.iter().enumerate() {
- if i == idx {
- updated.push(a.replacement.clone());
- } else if directory != &a.replacement {
- updated.push(directory.clone());
+ } else {
+ list[idx] = replacement;
+ if let Some(i) = replacement_idx.filter(|&i| i > idx) {
+ list.remove(i);
}
}
- *list = updated;
ReduceOutcome::Applied
}
StateAction::SessionInputNeededSet(a) => {
diff --git a/clients/rust/crates/ahp/tests/client_roundtrip.rs b/clients/rust/crates/ahp/tests/client_roundtrip.rs
index 58482f447..ded101462 100644
--- a/clients/rust/crates/ahp/tests/client_roundtrip.rs
+++ b/clients/rust/crates/ahp/tests/client_roundtrip.rs
@@ -385,6 +385,7 @@ async fn session_config_completions_send_wrapper_targets_root_channel() {
meta: None,
provider: None,
working_directory: None,
+ repositories: None,
config: None,
property: "baseBranch".into(),
query: Some("ma".into()),
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift
index e97981c39..54d84cd8a 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift
@@ -1514,11 +1514,11 @@ public struct SessionActiveClientRemovedAction: Codable, Sendable, Equatable {
public struct SessionWorkingDirectorySetAction: Codable, Sendable, Equatable {
public var type: ActionType
/// The working directory to grant the session's agent tool access to.
- public var directory: String
+ public var directory: WorkingDirectoryEntry
public init(
type: ActionType,
- directory: String
+ directory: WorkingDirectoryEntry
) {
self.type = type
self.directory = directory
@@ -1543,13 +1543,13 @@ public struct SessionWorkingDirectoryReplacedAction: Codable, Sendable, Equatabl
public var type: ActionType
/// URI of the existing entry to replace.
public var directory: String
- /// URI to place in the replaced entry's position.
- public var replacement: String
+ /// Complete entry to place in the replaced entry's position.
+ public var replacement: WorkingDirectoryEntry
public init(
type: ActionType,
directory: String,
- replacement: String
+ replacement: WorkingDirectoryEntry
) {
self.type = type
self.directory = directory
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
index 16dbcf143..14ab8149a 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift
@@ -288,6 +288,25 @@ public struct SideChatSource: Codable, Sendable, Equatable {
}
}
+public struct RepositorySource: Codable, Sendable, Equatable {
+ /// Credential-free repository source URI.
+ public var source: String
+ /// Requested branch, tag, or commit. Omit to use the host's default revision.
+ public var revision: String?
+ /// Repository-relative selected folder; omit for the root. Hosts reject empty, absolute, or escaping paths.
+ public var subdirectory: String?
+
+ public init(
+ source: String,
+ revision: String? = nil,
+ subdirectory: String? = nil
+ ) {
+ self.source = source
+ self.revision = revision
+ self.subdirectory = subdirectory
+ }
+}
+
/// Opens a TCP connection from the host to a destination on the host's network.
///
/// The client chooses the channel URI. This command does not subscribe: after it
@@ -459,6 +478,8 @@ public struct InitializeResult: Codable, Sendable, Equatable {
public var environment: HostEnvironment?
/// Suggested default directory for remote filesystem browsing
public var defaultDirectory: String?
+ /// Host repository preparation support; absent when unsupported.
+ public var repositoryPreparation: RepositoryPreparationCapability?
/// Characters that, when typed in a {@link Message} input, SHOULD cause
/// the client to issue a `completions` request with
/// {@link CompletionItemKind.UserMessage}. Typically includes characters like
@@ -500,6 +521,7 @@ public struct InitializeResult: Codable, Sendable, Equatable {
case snapshots
case environment
case defaultDirectory
+ case repositoryPreparation
case completionTriggerCharacters
case terminalCommandPrefix
case telemetry
@@ -516,6 +538,7 @@ public struct InitializeResult: Codable, Sendable, Equatable {
snapshots: [Snapshot],
environment: HostEnvironment? = nil,
defaultDirectory: String? = nil,
+ repositoryPreparation: RepositoryPreparationCapability? = nil,
completionTriggerCharacters: [String]? = nil,
terminalCommandPrefix: String? = nil,
telemetry: TelemetryCapabilities? = nil,
@@ -530,6 +553,7 @@ public struct InitializeResult: Codable, Sendable, Equatable {
self.snapshots = snapshots
self.environment = environment
self.defaultDirectory = defaultDirectory
+ self.repositoryPreparation = repositoryPreparation
self.completionTriggerCharacters = completionTriggerCharacters
self.terminalCommandPrefix = terminalCommandPrefix
self.telemetry = telemetry
@@ -561,6 +585,9 @@ public struct PingParams: Codable, Sendable, Equatable {
}
public struct ClientCapabilities: Codable, Sendable, Equatable {
+ /// Client accepts rich {@link WorkingDirectory} records as well as URI strings.
+ /// Hosts project records to URIs when absent and retain this choice on reconnect.
+ public var workingDirectoryInfo: WorkingDirectoryInfoCapability?
/// Client can render
/// [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) — i.e.
/// it can host the View sandbox, run the `ui/*` protocol against it,
@@ -575,8 +602,10 @@ public struct ClientCapabilities: Codable, Sendable, Equatable {
public var mcpApps: [String: AnyCodable]?
public init(
+ workingDirectoryInfo: WorkingDirectoryInfoCapability? = nil,
mcpApps: [String: AnyCodable]? = nil
) {
+ self.workingDirectoryInfo = workingDirectoryInfo
self.mcpApps = mcpApps
}
}
@@ -616,6 +645,29 @@ public struct AutomationCapabilities: Codable, Sendable, Equatable {
}
}
+public struct RepositoryPreparationCapability: Codable, Sendable, Equatable {
+ /// When true, clients may supply {@link RepositorySource.revision}.
+ public var revision: Bool?
+ /// When true, clients may supply more than one repository.
+ public var multipleRepositories: Bool?
+
+ public init(
+ revision: Bool? = nil,
+ multipleRepositories: Bool? = nil
+ ) {
+ self.revision = revision
+ self.multipleRepositories = multipleRepositories
+ }
+}
+
+public struct WorkingDirectoryInfoCapability: Codable, Sendable, Equatable {
+
+ public init(
+
+ ) {
+ }
+}
+
public struct AutomationCreateCapability: Codable, Sendable, Equatable {
public init(
@@ -985,6 +1037,9 @@ public struct CreateSessionParams: Codable, Sendable, Equatable {
/// and ignores the rest. Dispatch working-directory actions to change the set
/// after the session has started.
public var workingDirectories: [String]?
+ /// Repositories to prepare instead of an explicit `workingDirectories` list.
+ /// An empty list is equivalent to omission.
+ public var repositories: [RepositorySource]?
/// Agent-specific configuration values collected via `resolveSessionConfig`.
/// Keys and values correspond to the schema returned by the server.
public var config: [String: AnyCodable]?
@@ -1012,6 +1067,7 @@ public struct CreateSessionParams: Codable, Sendable, Equatable {
case meta = "_meta"
case provider
case workingDirectories
+ case repositories
case config
case activeClient
case progressToken
@@ -1022,6 +1078,7 @@ public struct CreateSessionParams: Codable, Sendable, Equatable {
meta: [String: AnyCodable]? = nil,
provider: String? = nil,
workingDirectories: [String]? = nil,
+ repositories: [RepositorySource]? = nil,
config: [String: AnyCodable]? = nil,
activeClient: SessionActiveClient? = nil,
progressToken: String? = nil
@@ -1030,6 +1087,7 @@ public struct CreateSessionParams: Codable, Sendable, Equatable {
self.meta = meta
self.provider = provider
self.workingDirectories = workingDirectories
+ self.repositories = repositories
self.config = config
self.activeClient = activeClient
self.progressToken = progressToken
@@ -1082,9 +1140,9 @@ public struct CreateChatParams: Codable, Sendable, Equatable {
/// also snapshots and preserves that exact selected text in the created chat's
/// origin; any `responsePartId` there is provenance only, not a live range.
public var source: ChatSource?
- /// Initial working-directory subset for this chat. Every entry MUST be
- /// present in the owning session's `workingDirectories`; the server MUST
- /// reject any entry that is not. When absent, the chat inherits the full
+ /// Initial working-directory URI subset for this chat. Every URI MUST match
+ /// a URI string or record's `uri` in the owning session's `workingDirectories`;
+ /// the server MUST reject any entry that does not. When absent, the chat inherits the full
/// session set. Forked chats (those whose `source.kind` is `"fork"`) inherit
/// the source chat's `workingDirectories`; this field is ignored for forks.
///
@@ -2086,6 +2144,8 @@ public struct ResolveSessionConfigParams: Codable, Sendable, Equatable {
public var provider: String?
/// Working directory for the session
public var workingDirectory: String?
+ /// Repositories used as configuration context; an empty list is equivalent to omission.
+ public var repositories: [RepositorySource]?
/// Current user-filled configuration values
public var config: [String: AnyCodable]?
@@ -2094,6 +2154,7 @@ public struct ResolveSessionConfigParams: Codable, Sendable, Equatable {
case meta = "_meta"
case provider
case workingDirectory
+ case repositories
case config
}
@@ -2102,12 +2163,14 @@ public struct ResolveSessionConfigParams: Codable, Sendable, Equatable {
meta: [String: AnyCodable]? = nil,
provider: String? = nil,
workingDirectory: String? = nil,
+ repositories: [RepositorySource]? = nil,
config: [String: AnyCodable]? = nil
) {
self.channel = channel
self.meta = meta
self.provider = provider
self.workingDirectory = workingDirectory
+ self.repositories = repositories
self.config = config
}
}
@@ -2255,6 +2318,8 @@ public struct SessionConfigCompletionsParams: Codable, Sendable, Equatable {
public var provider: String?
/// Working directory for the session
public var workingDirectory: String?
+ /// Repositories used as configuration context; an empty list is equivalent to omission.
+ public var repositories: [RepositorySource]?
/// Current user-filled configuration values (provides context for the query)
public var config: [String: AnyCodable]?
/// Property id from the schema to query values for
@@ -2267,6 +2332,7 @@ public struct SessionConfigCompletionsParams: Codable, Sendable, Equatable {
case meta = "_meta"
case provider
case workingDirectory
+ case repositories
case config
case property
case query
@@ -2277,6 +2343,7 @@ public struct SessionConfigCompletionsParams: Codable, Sendable, Equatable {
meta: [String: AnyCodable]? = nil,
provider: String? = nil,
workingDirectory: String? = nil,
+ repositories: [RepositorySource]? = nil,
config: [String: AnyCodable]? = nil,
property: String,
query: String? = nil
@@ -2285,6 +2352,7 @@ public struct SessionConfigCompletionsParams: Codable, Sendable, Equatable {
self.meta = meta
self.provider = provider
self.workingDirectory = workingDirectory
+ self.repositories = repositories
self.config = config
self.property = property
self.query = query
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift
index 1fc3b90f6..f82c4117a 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Notifications.generated.swift
@@ -389,7 +389,9 @@ public struct PartialSessionSummary: Codable, Sendable, Equatable {
/// MAY restrict to a subset via
/// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
/// chat that sets none operates against this full set.
- public var workingDirectories: [String]?
+ /// Entries are uniquely keyed by URI. Rich records require
+ /// {@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs.
+ public var workingDirectories: [WorkingDirectoryEntry]?
/// Lightweight summary of this session's inline annotations channel
/// (`ahp-annotations:/`). Surfaced so badge UI can render
/// annotation / entry counts without subscribing. Absent when the session
@@ -441,7 +443,7 @@ public struct PartialSessionSummary: Codable, Sendable, Equatable {
activity: String? = nil,
origin: SessionOrigin? = nil,
project: ProjectInfo? = nil,
- workingDirectories: [String]? = nil,
+ workingDirectories: [WorkingDirectoryEntry]? = nil,
annotations: AnnotationsSummary? = nil,
resource: String? = nil,
createdAt: String? = nil,
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
index a59c63da5..4d0e29874 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
@@ -1058,6 +1058,36 @@ public enum SessionOriginKind: Codable, Sendable, Equatable {
}
}
+/// How a working directory was prepared.
+public enum WorkingDirectoryOriginKind: Codable, Sendable, Equatable {
+ case local
+ case repo
+ case worktree
+ /// Unknown raw value from a newer protocol version, preserved verbatim.
+ case unknown(String)
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.singleValueContainer()
+ let raw = try container.decode(String.self)
+ switch raw {
+ case "local": self = .local
+ case "repo": self = .repo
+ case "worktree": self = .worktree
+ default: self = .unknown(raw)
+ }
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.singleValueContainer()
+ switch self {
+ case .local: try container.encode("local")
+ case .repo: try container.encode("repo")
+ case .worktree: try container.encode("worktree")
+ case .unknown(let raw): try container.encode(raw)
+ }
+ }
+}
+
/// Operations the host currently permits for an automation.
///
/// The list on {@link AutomationEntry.operations} is authoritative and may
@@ -1646,6 +1676,59 @@ public struct MultipleWorkingDirectoriesCapability: Codable, Sendable, Equatable
}
}
+public struct WorkingDirectory: Codable, Sendable, Equatable {
+ /// Actual selected directory, which may be a repository subdirectory.
+ public var uri: String
+ /// Credential-free repository source association, not a checkout identity.
+ public var repo: String?
+ /// Host-reported provenance; omission means unspecified.
+ public var origin: WorkingDirectoryOrigin?
+
+ public init(
+ uri: String,
+ repo: String? = nil,
+ origin: WorkingDirectoryOrigin? = nil
+ ) {
+ self.uri = uri
+ self.repo = repo
+ self.origin = origin
+ }
+}
+
+public struct LocalWorkingDirectoryOrigin: Codable, Sendable, Equatable {
+ public var kind: WorkingDirectoryOriginKind
+
+ public init(
+ kind: WorkingDirectoryOriginKind
+ ) {
+ self.kind = kind
+ }
+}
+
+public struct RepoWorkingDirectoryOrigin: Codable, Sendable, Equatable {
+ public var kind: WorkingDirectoryOriginKind
+
+ public init(
+ kind: WorkingDirectoryOriginKind
+ ) {
+ self.kind = kind
+ }
+}
+
+public struct WorktreeWorkingDirectoryOrigin: Codable, Sendable, Equatable {
+ public var kind: WorkingDirectoryOriginKind
+ /// Main worktree associated with the host-prepared worktree.
+ public var mainWorktree: String
+
+ public init(
+ kind: WorkingDirectoryOriginKind,
+ mainWorktree: String
+ ) {
+ self.kind = kind
+ self.mainWorktree = mainWorktree
+ }
+}
+
public struct SessionModelInfo: Codable, Sendable, Equatable {
/// Model identifier
public var id: String
@@ -1969,13 +2052,14 @@ public struct ChatState: Codable, Sendable, Equatable {
public var provider: String?
/// The subset of the session's
/// {@link SessionState.workingDirectories | `workingDirectories`} that this
- /// chat's agent has tool access to. Every entry MUST be present in the owning
- /// session's `workingDirectories`; servers MUST reject a
+ /// chat's agent has tool access to. Every URI MUST match a URI string or a
+ /// record's `uri` in the owning session's set; servers MUST reject a
/// `chat/workingDirectorySet` action that violates this constraint.
///
/// When absent, the chat inherits the full session set. When present but empty
/// (not recommended), the chat has no working-directory tool access at all.
///
+ /// Directory metadata belongs to the session, not the chat.
/// Dispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to
/// update the subset on a running chat.
public var workingDirectories: [String]?
@@ -2221,7 +2305,9 @@ public struct SessionState: Codable, Sendable, Equatable {
/// MAY restrict to a subset via
/// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
/// chat that sets none operates against this full set.
- public var workingDirectories: [String]?
+ /// Entries are uniquely keyed by URI. Rich records require
+ /// {@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs.
+ public var workingDirectories: [WorkingDirectoryEntry]?
/// Lightweight summary of this session's inline annotations channel
/// (`ahp-annotations:/`). Surfaced so badge UI can render
/// annotation / entry counts without subscribing. Absent when the session
@@ -2335,7 +2421,7 @@ public struct SessionState: Codable, Sendable, Equatable {
activity: String? = nil,
origin: SessionOrigin? = nil,
project: ProjectInfo? = nil,
- workingDirectories: [String]? = nil,
+ workingDirectories: [WorkingDirectoryEntry]? = nil,
annotations: AnnotationsSummary? = nil,
lifecycle: SessionLifecycle,
creationError: ErrorInfo? = nil,
@@ -2647,7 +2733,9 @@ public struct SessionSummary: Codable, Sendable, Equatable {
/// MAY restrict to a subset via
/// {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
/// chat that sets none operates against this full set.
- public var workingDirectories: [String]?
+ /// Entries are uniquely keyed by URI. Rich records require
+ /// {@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs.
+ public var workingDirectories: [WorkingDirectoryEntry]?
/// Lightweight summary of this session's inline annotations channel
/// (`ahp-annotations:/`). Surfaced so badge UI can render
/// annotation / entry counts without subscribing. Absent when the session
@@ -2699,7 +2787,7 @@ public struct SessionSummary: Codable, Sendable, Equatable {
activity: String? = nil,
origin: SessionOrigin? = nil,
project: ProjectInfo? = nil,
- workingDirectories: [String]? = nil,
+ workingDirectories: [WorkingDirectoryEntry]? = nil,
annotations: AnnotationsSummary? = nil,
resource: String,
createdAt: String,
@@ -8876,6 +8964,36 @@ public enum ToolInput: Codable, Sendable, Equatable {
}
}
+/// A legacy URI or a complete working-directory record.
+public enum WorkingDirectoryEntry: Codable, Sendable {
+ case uri(String)
+ case directory(WorkingDirectory)
+
+ public var uri: String {
+ switch self {
+ case .uri(let value): return value
+ case .directory(let value): return value.uri
+ }
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.singleValueContainer()
+ if let uri = try? container.decode(String.self) {
+ self = .uri(uri)
+ } else {
+ self = .directory(try container.decode(WorkingDirectory.self))
+ }
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.singleValueContainer()
+ switch self {
+ case .uri(let value): try container.encode(value)
+ case .directory(let value): try container.encode(value)
+ }
+ }
+}
+
// MARK: - Discriminated Unions
public struct ChatOriginUser: Codable, Sendable, Equatable {
@@ -9740,6 +9858,52 @@ public enum SessionOrigin: Codable, Sendable, Equatable {
}
}
+public enum WorkingDirectoryOrigin: Codable, Sendable, Equatable {
+ case local(LocalWorkingDirectoryOrigin)
+ case repo(RepoWorkingDirectoryOrigin)
+ case worktree(WorktreeWorkingDirectoryOrigin)
+ /// Unknown or future discriminant; the raw payload is preserved
+ /// and re-encoded verbatim for forward-compatibility.
+ case unknown(AnyCodable)
+
+ private enum DiscriminantKey: String, CodingKey {
+ case discriminant = "kind"
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: DiscriminantKey.self)
+ guard let discriminant = try container.decodeIfPresent(String.self, forKey: .discriminant) else {
+ self = .unknown(try AnyCodable(from: decoder))
+ return
+ }
+ switch discriminant {
+ case "local":
+ self = .local(try LocalWorkingDirectoryOrigin(from: decoder))
+ case "repo":
+ self = .repo(try RepoWorkingDirectoryOrigin(from: decoder))
+ case "worktree":
+ self = .worktree(try WorktreeWorkingDirectoryOrigin(from: decoder))
+ default:
+ self = .unknown(try AnyCodable(from: decoder))
+ }
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ switch self {
+ case .local(var value):
+ value.kind = .local
+ try value.encode(to: encoder)
+ case .repo(var value):
+ value.kind = .repo
+ try value.encode(to: encoder)
+ case .worktree(var value):
+ value.kind = .worktree
+ try value.encode(to: encoder)
+ case .unknown(let value): try value.encode(to: encoder)
+ }
+ }
+}
+
public enum AutomationTrigger: Codable, Sendable, Equatable {
case schedule(AutomationScheduleTrigger)
case event(AutomationEventTrigger)
diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift
index dca8a0725..f22a83fa0 100644
--- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift
+++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift
@@ -881,31 +881,43 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS
return next
case .sessionWorkingDirectorySet(let a):
- if (state.workingDirectories ?? []).contains(a.directory) { return state }
var next = state
- next.workingDirectories = (next.workingDirectories ?? []) + [a.directory]
+ var directories = state.workingDirectories ?? []
+ if let idx = directories.firstIndex(where: { $0.uri == a.directory.uri }) {
+ guard case .directory = a.directory else { return state }
+ directories[idx] = a.directory
+ } else {
+ directories.append(a.directory)
+ }
+ next.workingDirectories = directories
return next
case .sessionWorkingDirectoryRemoved(let a):
- guard let idx = state.workingDirectories?.firstIndex(of: a.directory) else { return state }
+ guard let idx = state.workingDirectories?.firstIndex(where: { $0.uri == a.directory }) else { return state }
var next = state
next.workingDirectories?.remove(at: idx)
return next
case .sessionWorkingDirectoryReplaced(let a):
guard let directories = state.workingDirectories,
- let idx = directories.firstIndex(of: a.directory) else { return state }
+ let idx = directories.firstIndex(where: { $0.uri == a.directory }) else { return state }
var next = state
- if let replacementIdx = directories.firstIndex(of: a.replacement), replacementIdx < idx {
- next.workingDirectories = directories.enumerated().compactMap { index, directory in
- index == idx ? nil : directory
- }
+ let replacementIdx = directories.firstIndex(where: { $0.uri == a.replacement.uri })
+ var replacement = a.replacement
+ if case .uri = replacement, let replacementIdx {
+ replacement = directories[replacementIdx]
+ }
+ var updated = directories
+ if let replacementIdx, replacementIdx < idx {
+ updated[replacementIdx] = replacement
+ updated.remove(at: idx)
} else {
- next.workingDirectories = directories.enumerated().compactMap { index, directory in
- if index == idx { return a.replacement }
- return directory == a.replacement ? nil : directory
+ updated[idx] = replacement
+ if let replacementIdx, replacementIdx > idx {
+ updated.remove(at: replacementIdx)
}
}
+ next.workingDirectories = updated
return next
case .sessionInputNeededSet(let a):
diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift
index 225d8d0ea..86401e561 100644
--- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift
+++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift
@@ -188,6 +188,18 @@ final class TypesRoundTripFixtureTests: XCTestCase {
return try reencode(dec.decode(Implementation.self, from: inputData))
case "InitializeResult":
return try reencode(dec.decode(InitializeResult.self, from: inputData))
+ case "InitializeParams":
+ return try reencode(dec.decode(InitializeParams.self, from: inputData))
+ case "ListSessionsResult":
+ return try reencode(dec.decode(ListSessionsResult.self, from: inputData))
+ case "SessionSummaryChangedParams":
+ return try reencode(dec.decode(SessionSummaryChangedParams.self, from: inputData))
+ case "CreateSessionParams":
+ return try reencode(dec.decode(CreateSessionParams.self, from: inputData))
+ case "ResolveSessionConfigParams":
+ return try reencode(dec.decode(ResolveSessionConfigParams.self, from: inputData))
+ case "SessionConfigCompletionsParams":
+ return try reencode(dec.decode(SessionConfigCompletionsParams.self, from: inputData))
case "CreateChatParams":
return try reencode(dec.decode(CreateChatParams.self, from: inputData))
case "ChatSource":
diff --git a/clients/typescript/test/client.test.ts b/clients/typescript/test/client.test.ts
index 8b4611ea7..4c8d6cb73 100644
--- a/clients/typescript/test/client.test.ts
+++ b/clients/typescript/test/client.test.ts
@@ -42,7 +42,14 @@ import type {
import { JsonRpcErrorCodes } from '../src/types/common/errors.js';
import { AutomationOperation, type AutomationEntry } from '../src/types/channels-automation/state.js';
import { MessageKind } from '../src/types/channels-chat/state.js';
-import { SessionLifecycle, SessionStatus } from '../src/types/channels-session/state.js';
+import {
+ SessionLifecycle,
+ SessionStatus,
+ WorkingDirectoryOriginKind,
+ type SessionConfigSchema,
+ type SessionState,
+ type WorkingDirectory,
+} from '../src/types/index.js';
import type { CanvasState } from '../src/types/channels-canvas/state.js';
import type { Snapshot } from '../src/types/common/state.js';
@@ -111,6 +118,180 @@ test('initialize round-trip', async () => {
await client.shutdown();
});
+for (const repositoryPreparation of [
+ undefined, {}, { revision: true }, { multipleRepositories: false },
+ { multipleRepositories: true }, { revision: true, multipleRepositories: true },
+]) {
+ test(`initialize preserves host repository capability ${JSON.stringify(repositoryPreparation)}`, async t => {
+ const [c, s] = InMemoryTransport.pair();
+ const client = new AhpClient(c);
+ t.after(() => client.shutdown());
+ client.connect();
+
+ const initialization = client.initialize({ clientId: 'repository-client', protocolVersions: ['0.9.0'] });
+ const request = await readRequest(s);
+ const result: InitializeResult = {
+ protocolVersion: '0.9.0', serverSeq: 0, snapshots: [],
+ ...(repositoryPreparation === undefined ? {} : { repositoryPreparation }),
+ };
+ reply(s, request.id, result);
+ assert.deepEqual(await initialization, result);
+ });
+}
+
+const repositorySource = 'https://example.org/team/project.git';
+for (const { name, repositories } of [
+ { name: 'default revision', repositories: [{ source: repositorySource }] },
+ { name: 'explicit revision', repositories: [{ source: repositorySource, revision: 'refs/tags/v1.2.3' }] },
+ { name: 'same source at two revisions', repositories: [
+ { source: repositorySource, revision: 'main' },
+ { source: repositorySource, revision: 'feature' },
+ ] },
+]) {
+ test(`typed requests round-trip repository lists (${name}) outside config`, async t => {
+ const [c, s] = InMemoryTransport.pair();
+ const client = new AhpClient(c);
+ t.after(() => client.shutdown());
+ client.connect();
+
+ const schema: SessionConfigSchema = {
+ type: 'object',
+ properties: {
+ mode: { type: 'string', title: 'Mode', default: 'review' },
+ },
+ };
+ const discovery = client.request('resolveSessionConfig', { channel: ROOT });
+ const discoveryRequest = await readRequest(s);
+ assert.equal(discoveryRequest.method, 'resolveSessionConfig');
+ assert.deepEqual(discoveryRequest.params, { channel: ROOT });
+ reply(s, discoveryRequest.id, { schema, values: { mode: 'review' } });
+
+ const discovered = await discovery;
+ assert.deepEqual(discovered.schema, schema);
+ const context = { repositories, workingDirectory: 'file:///work/context' };
+ const config = discovered.values;
+ const resolution = client.request('resolveSessionConfig', { channel: ROOT, ...context, config });
+ const resolveRequest = await readRequest(s);
+ assert.equal(resolveRequest.method, 'resolveSessionConfig');
+ assert.deepEqual(resolveRequest.params, { channel: ROOT, ...context, config });
+ reply(s, resolveRequest.id, { schema, values: config });
+ const resolved = await resolution;
+ assert.deepEqual(resolved.values, config);
+
+ const completions = client.request('sessionConfigCompletions', { channel: ROOT, ...context, config: resolved.values, property: 'mode' });
+ const completionRequest = await readRequest(s);
+ assert.deepEqual(completionRequest.params, { channel: ROOT, ...context, config: resolved.values, property: 'mode' });
+ reply(s, completionRequest.id, { items: [] });
+ await completions;
+
+ const params = { channel: 'ahp-session:/repository-test', repositories, config: resolved.values };
+ const creation = client.request('createSession', params);
+ const createRequest = await readRequest(s);
+ assert.equal(createRequest.method, 'createSession');
+ assert.deepEqual(createRequest.params, params);
+ reply(s, createRequest.id, null);
+ assert.equal(await creation, null);
+ });
+}
+
+for (const method of ['resolveSessionConfig', 'sessionConfigCompletions', 'createSession'] as const) {
+ test(`${method} surfaces host rejection of repository intent`, async t => {
+ const [c, s] = InMemoryTransport.pair();
+ const client = new AhpClient(c);
+ t.after(() => client.shutdown());
+ client.connect();
+
+ const channel = method === 'createSession' ? 'ahp-session:/repository-test' : ROOT;
+ const params = {
+ channel,
+ repositories: [{ source: repositorySource, revision: 'unsupported' }],
+ ...(method === 'sessionConfigCompletions' ? { property: 'mode' } : {}),
+ };
+ const request = method === 'sessionConfigCompletions'
+ ? client.request(method, { ...params, property: 'mode' })
+ : client.request(method, params);
+ const rejected = assert.rejects(request, new RpcError(JsonRpcErrorCodes.InvalidParams, 'Unsupported repository revision'));
+ const sent = await readRequest(s);
+ assert.deepEqual({ method: sent.method, params: sent.params }, { method, params });
+ replyError(s, sent.id, JsonRpcErrorCodes.InvalidParams, 'Unsupported repository revision');
+ await rejected;
+ });
+}
+
+for (const failed of [false, true]) {
+ test(`session state retains resolved directory metadata after creation ${failed ? 'fails' : 'succeeds'}`, () => {
+ const resource = 'ahp-session:/repository-test';
+ const workingDirectories: WorkingDirectory[] = [
+ { uri: 'file:///work/project', repo: repositorySource, origin: { kind: WorkingDirectoryOriginKind.Repo } },
+ {
+ uri: 'file:///work/project-worktree/packages/api',
+ repo: repositorySource,
+ origin: { kind: WorkingDirectoryOriginKind.Worktree, mainWorktree: 'file:///work/project' },
+ },
+ ];
+ const initial: SessionState = {
+ provider: 'example',
+ title: 'Repository session',
+ status: SessionStatus.Idle,
+ lifecycle: SessionLifecycle.Creating,
+ activeClients: [],
+ chats: [],
+ workingDirectories: [],
+ config: {
+ schema: { type: 'object', properties: { mode: { type: 'string', title: 'Mode' } } },
+ values: { mode: 'review' },
+ },
+ };
+ const mirror = new AhpStateMirror();
+ mirror.applySnapshot({ resource, state: initial, fromSeq: 0 });
+ mirror.apply({
+ channel: resource,
+ serverSeq: 1,
+ origin: undefined,
+ action: { type: ActionType.SessionWorkingDirectorySet, directory: workingDirectories[0] },
+ });
+ mirror.apply({
+ channel: resource,
+ serverSeq: 2,
+ origin: undefined,
+ action: { type: ActionType.SessionWorkingDirectorySet, directory: workingDirectories[1] },
+ });
+ const preparing = mirror.getSession(resource);
+ assert.ok(preparing);
+ assert.equal(preparing.lifecycle, SessionLifecycle.Creating);
+ assert.deepEqual(preparing.config, initial.config);
+ assert.deepEqual(preparing.workingDirectories, workingDirectories);
+
+ const joining = new AhpStateMirror();
+ joining.applySnapshot({ resource, state: preparing, fromSeq: 2 });
+ assert.deepEqual(joining.getSession(resource), preparing);
+
+ const completion: ActionEnvelope = {
+ channel: resource,
+ serverSeq: 3,
+ origin: undefined,
+ action: failed
+ ? { type: ActionType.SessionCreationFailed, error: { errorType: 'preparationFailed', message: 'Preparation failed' } }
+ : { type: ActionType.SessionReady },
+ };
+ mirror.apply(completion);
+ joining.apply(completion);
+ const completed = mirror.getSession(resource);
+ assert.ok(completed);
+ assert.equal(completed.lifecycle, failed ? SessionLifecycle.Failed : SessionLifecycle.Ready);
+ assert.deepEqual(completed.config, initial.config);
+ assert.deepEqual(completed.workingDirectories, workingDirectories);
+ assert.deepEqual(joining.getSession(resource), completed);
+ if (failed) {
+ assert.deepEqual(completed.creationError, { errorType: 'preparationFailed', message: 'Preparation failed' });
+ }
+
+ const reconnected = new AhpStateMirror();
+ reconnected.applySnapshot({ resource, state: completed, fromSeq: 3 });
+ assert.deepEqual(reconnected.getSession(resource), completed);
+ });
+}
+
test('subscribe attaches before sending the request and fans out an action', async () => {
const [c, s] = InMemoryTransport.pair();
const client = new AhpClient(c);
diff --git a/clients/typescript/test/types-round-trip.test.ts b/clients/typescript/test/types-round-trip.test.ts
index 086c8bf6a..5c2c602a9 100644
--- a/clients/typescript/test/types-round-trip.test.ts
+++ b/clients/typescript/test/types-round-trip.test.ts
@@ -58,15 +58,17 @@ import type {
Customization,
SessionSummary,
} from '../src/types/channels-session/state.js';
-import type { SessionAddedParams } from '../src/types/channels-root/notifications.js';
+import type { SessionAddedParams, SessionSummaryChangedParams } from '../src/types/channels-root/notifications.js';
import type {
- Implementation, InitializeResult, SubscribeParams, SubscribeResult, ReconnectParams, ReconnectResult,
+ Implementation, InitializeParams, InitializeResult, SubscribeParams, SubscribeResult, ReconnectParams, ReconnectResult,
ResourceReadParams, ResourceReadResult, AuthenticateParams,
} from '../src/types/common/commands.js';
import type { ChannelFrameParams, ChannelCreditParams } from '../src/types/common/notifications.js';
import type { TcpDataParams, TcpEofParams, TcpResetParams } from '../src/types/channels-tcp/notifications.js';
import type { CreateTcpConnectionParams } from '../src/types/channels-tcp/commands.js';
import type { ChatSource, CreateChatParams } from '../src/types/channels-chat/commands.js';
+import type { CreateSessionParams } from '../src/types/channels-session/commands.js';
+import type { ListSessionsResult, ResolveSessionConfigParams, SessionConfigCompletionsParams } from '../src/types/channels-root/commands.js';
// ─── Fixture directory ───────────────────────────────────────────────────────
@@ -249,6 +251,12 @@ function bindToType(file: string, type: string, parsed: unknown): void {
case 'PartialSessionSummary': void (parsed as Partial); break;
case 'Implementation': void (parsed as Implementation); break;
case 'InitializeResult': void (parsed as InitializeResult); break;
+ case 'InitializeParams': void (parsed as InitializeParams); break;
+ case 'ListSessionsResult': void (parsed as ListSessionsResult); break;
+ case 'SessionSummaryChangedParams': void (parsed as SessionSummaryChangedParams); break;
+ case 'CreateSessionParams': void (parsed as CreateSessionParams); break;
+ case 'ResolveSessionConfigParams': void (parsed as ResolveSessionConfigParams); break;
+ case 'SessionConfigCompletionsParams': void (parsed as SessionConfigCompletionsParams); break;
case 'CreateChatParams': void (parsed as CreateChatParams); break;
case 'ChatSource': void (parsed as ChatSource); break;
case 'Snapshot': void (parsed as Snapshot); break;
diff --git a/clients/typescript/test/working-directories.test.ts b/clients/typescript/test/working-directories.test.ts
new file mode 100644
index 000000000..9773973b3
--- /dev/null
+++ b/clients/typescript/test/working-directories.test.ts
@@ -0,0 +1,37 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { readFileSync, readdirSync } from 'node:fs';
+import { sessionReducer } from '../src/types/reducers.js';
+import { ActionType } from '../src/types/common/actions.js';
+import type { SessionAction } from '../src/types/action-origin.generated.js';
+import type { SessionState } from '../src/types/channels-session/state.js';
+
+interface Fixture {
+ reducer: string;
+ initial: SessionState;
+ actions: SessionAction[];
+ expected: SessionState;
+}
+
+const directoryActions = new Set([
+ ActionType.SessionWorkingDirectorySet,
+ ActionType.SessionWorkingDirectoryRemoved,
+ ActionType.SessionWorkingDirectoryReplaced,
+]);
+const fixtureDirectory = new URL('../../../types/test-cases/reducers/', import.meta.url);
+
+for (const file of readdirSync(fixtureDirectory).filter(file => file.endsWith('.json')).sort()) {
+ const fixture = JSON.parse(readFileSync(new URL(file, fixtureDirectory), 'utf8')) as Fixture;
+ if (fixture.reducer !== 'session' || !fixture.actions.some(action => directoryActions.has(action.type))) {
+ continue;
+ }
+ test(`SDK working-directory fixture: ${file}`, () => {
+ const actual = fixture.actions.reduce(sessionReducer, fixture.initial);
+ assert.deepEqual(actual, fixture.expected, file);
+ });
+}
diff --git a/docs/.changes/20260915-repository-session-config.json b/docs/.changes/20260915-repository-session-config.json
new file mode 100644
index 000000000..be53ebdfa
--- /dev/null
+++ b/docs/.changes/20260915-repository-session-config.json
@@ -0,0 +1,4 @@
+{
+ "type": "added",
+ "message": "Typed repository preparation inputs with subdirectory selection, host capability discovery, and opt-in URI-keyed working-directory provenance across session state and actions."
+}
diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md
index 8a4013c6e..10db90c27 100644
--- a/docs/guide/state-model.md
+++ b/docs/guide/state-model.md
@@ -76,7 +76,7 @@ SessionState {
status: number // SessionStatus bitset
activity?: string
project?: ProjectInfo
- workingDirectories?: URI[] // equal-peer working directories
+ workingDirectories?: (URI | WorkingDirectory)[] // URI-keyed working directories
annotations?: AnnotationsSummary
lifecycle: 'creating' | 'ready' | 'failed'
@@ -110,7 +110,7 @@ SessionSummary {
createdAt: string // ISO 8601, e.g. "2025-03-10T18:42:03.123Z"
modifiedAt: string // ISO 8601
project?: ProjectInfo
- workingDirectories?: URI[] // equal-peer working directories
+ workingDirectories?: (URI | WorkingDirectory)[] // URI-keyed working directories
annotations?: AnnotationsSummary
changes?: ChangesSummary
chats?: SessionChatSummary[] // compact list presentation, including per-chat status
@@ -713,9 +713,14 @@ directories of the source session.
The directory set is state (`SessionState.workingDirectories`), so clients
mutate it by **dispatching actions**, not by calling commands:
+Entries are keyed by URI, whether represented as URI strings or rich
+`WorkingDirectory` records. Rich records require the client's
+`workingDirectoryInfo` capability; see [repository-backed creation and
+compatibility](/specification/session-channel#repository-backed-creation).
+
| Action | Effect |
| --- | --- |
-| `session/workingDirectorySet` | Adds `directory` to the set (creating it if absent). A no-op when the directory is already present. |
+| `session/workingDirectorySet` | Upserts `directory` by URI, appending when absent and replacing a rich same-URI entry in place. A URI-only duplicate preserves known metadata. |
| `session/workingDirectoryRemoved` | Removes `directory` from the set. A no-op when it is not present. There is no atomic backend "remove one" primitive — the host reconfigures its agent to the reduced set. A host MAY decline to apply the removal (e.g. the immutable primary at index 0), leaving the set unchanged. |
| `session/workingDirectoryReplaced` | Targeted compare-and-swap replacement of any entry that deduplicates `replacement`, preserving every other directory's relative order. An existing replacement after the target moves to the target's position (`[A, B, C]`, `B -> C` becomes `[A, C]`); an earlier replacement stays in place and the target is removed (`[A, B, C]`, `C -> A` becomes `[A, B]`). A no-op when `directory` is absent. Replacing index `0` additionally requires `primaryReplacement`. |
diff --git a/docs/specification/chat-channel.md b/docs/specification/chat-channel.md
index 8bed3089c..591d6db12 100644
--- a/docs/specification/chat-channel.md
+++ b/docs/specification/chat-channel.md
@@ -68,7 +68,7 @@ Clients MAY periodically sync their local input state into the draft by dispatch
### Per-chat working directory
-`ChatState.workingDirectories` (and its mirror on [`ChatSummary`](/reference/chat#chatsummary)) is **optional**. When absent, the chat inherits the session's full [`workingDirectories`](/reference/session#sessionsummary) set; when present it MUST be a subset of that set. Hosts MAY set a per-chat subset to give individual chats their own filesystem context — for example, allocating a separate git worktree per chat so multiple chats in the same session can make independent edits that the orchestrating chat later merges back.
+`ChatState.workingDirectories` (and its mirror on [`ChatSummary`](/reference/chat#chatsummary)) is an **optional URI array**. When absent, the chat inherits the session's full [`workingDirectories`](/reference/session#sessionsummary) set. When present, every URI MUST match a session URI string or a record's `uri`; directory metadata remains on the session. Hosts MAY set a per-chat subset to give individual chats their own filesystem context — for example, allocating a separate git worktree per chat so multiple chats in the same session can make independent edits that the orchestrating chat later merges back.
### Per-chat changesets
diff --git a/docs/specification/session-channel.md b/docs/specification/session-channel.md
index 133121f19..96e3272b1 100644
--- a/docs/specification/session-channel.md
+++ b/docs/specification/session-channel.md
@@ -35,6 +35,117 @@ Subscribers receive a [`SessionState`](/reference/session#sessionstate) snapshot
[`createSession`](/reference/session#createsession) is a JSON-RPC request. The client picks the URI; the server allocates session state and begins backend initialisation. If the URI is already in use the server returns `SessionAlreadyExists` (`-32003`).
+#### Repository-backed creation
+
+A host can prepare repositories for a new session. The client supplies sources; the host owns authorization, credentials, and preparation. This does not introduce a clone command or require a particular repository or worktree backend.
+
+##### Capability and field constraints
+
+The **host**, not an individual agent, opts in through [`InitializeResult.repositoryPreparation`](/reference/common#initialize), a [`RepositoryPreparationCapability`](/reference/common#repositorypreparationcapability):
+
+| Capability | Meaning |
+|---|---|
+| Absent | Repository preparation and repository context in configuration queries are unsupported. |
+| `{}` | Supports one repository at its default revision. |
+| `revision: true` | Also supports an explicit branch, tag, or commit. |
+| `multipleRepositories: true` | Also supports a list containing more than one repository. Absent or `false` means exactly one entry when a list is present. |
+
+The optional typed `repositories: RepositorySource[]` field is shared by [`CreateSessionParams`](/reference/session#createsession), [`ResolveSessionConfigParams`](/reference/root#resolvesessionconfig), and [`SessionConfigCompletionsParams`](/reference/root#sessionconfigcompletions). Each [`RepositorySource`](/reference/session#repositorysource) contains:
+
+| Field | Meaning |
+|---|---|
+| `source` | Required credential-free repository URI identifying the requested source. |
+| `revision` | Optional branch, tag, or commit. Omission requests the host's default revision. |
+| `subdirectory` | Optional repository-relative selected folder, such as `packages/api`. Omission selects the repository root. |
+
+Clients MUST check the host capability. Hosts MUST reject unsupported repository input, explicit revisions without `revision: true`, and multiple entries without `multipleRepositories: true` **before preparation**. An omitted or empty list requests no repository preparation and retains directory/default creation. Generic `config` remains unchanged.
+
+##### Request and resolved directories
+
+Pass the repository list as context to configuration queries, then beside the resolved configuration in `createSession`:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 2,
+ "method": "createSession",
+ "params": {
+ "channel": "ahp-session:/new-session",
+ "repositories": [
+ {
+ "source": "https://example.org/team/project.git",
+ "revision": "main",
+ "subdirectory": "packages/api"
+ }
+ ],
+ "config": { "mode": "interactive" }
+ }
+}
+```
+
+A host that prepares a worktree could publish this resolved session state:
+
+```json
+{
+ "lifecycle": "ready",
+ "workingDirectories": [
+ {
+ "uri": "file:///worktrees/session-42/packages/api",
+ "repo": "https://example.org/team/project.git",
+ "origin": {
+ "kind": "worktree",
+ "mainWorktree": "file:///workspaces/project"
+ }
+ }
+ ]
+}
+```
+
+`SessionMetadata.workingDirectories` accepts URI strings and [`WorkingDirectory`](/reference/session#workingdirectory) records, shared by full state and summaries:
+
+| Record field | Meaning |
+|---|---|
+| `uri` | Required actual selected directory. The unique entry key, including when selecting a subdirectory. |
+| `repo` | Optional credential-free source association, **not** a checkout identity. |
+| `origin` | Optional host-reported result: `local`, `repo`, or `worktree`. `worktree` requires `mainWorktree: URI`. Omission is unspecified provenance. |
+
+`local` describes a supplied local directory; `repo` describes repository preparation without a host-created worktree; `worktree` describes a host-created worktree even when the input was a repository source. Multiple entries may share `repo`. Clients MUST NOT deduplicate by source, associate requests and results by array position, or infer a checkout root from the selected directory. Metadata does not grant cleanup authority or imply generic worktree-creation support.
+
+##### Host validation and lifecycle
+
+For creation and configuration queries, hosts MUST reject invalid or unsupported input with `InvalidParams` (`-32602`): empty sources or revisions, malformed or credential-bearing source URIs, and empty, absolute, or escaping `subdirectory` paths. Hosts MUST ensure the selected folder resolves inside the prepared repository, including through symbolic links, and MUST NOT silently substitute the root or a default revision.
+
+A non-empty `createSession.repositories` and a non-empty `createSession.workingDirectories` are mutually exclusive. Configuration queries may also include an existing `workingDirectory` as context but MUST NOT prepare repositories. Hosts MUST authorize the client before preparation, use the existing [authentication contract](./authentication), and keep credentials out of source URIs, state, progress, and logs.
+
+Preparation stays within the existing `creating` lifecycle. Before `session/ready` or `session/creationFailed`, the host MUST publish any actual resolved directories. Unresolved directories stay absent or empty; failure uses `creationError`, not a fabricated checkout. The original repository list is **not** echoed into session metadata.
+
+Clients MUST wait for native session readiness before sending turns and use the actual resolved directories, not requested sources or guessed paths. Optional `createSession.progressToken` / [`root/progress`](./root-channel#progress) updates and a successful command response do not establish readiness.
+
+Repository preparation does not change the agent's existing multi-directory, immutable-primary, or primary-replacement rules. Resolved directories must fit those capabilities; later directory changes remain available under the same rules.
+
+##### Compatibility and host authority
+
+Clients opt into rich records with `initialize.capabilities.workingDirectoryInfo: {}` ([`WorkingDirectoryInfoCapability`](/reference/common#workingdirectoryinfocapability)). New clients MUST also accept URI-only and mixed lists, including responses from older hosts.
+
+Hosts MUST retain authoritative rich state and project every record to its `uri` for clients without this capability. Projection is per client and MUST NOT erase metadata from shared state or the replay log. It applies to **every carrier**:
+
+- `initialize` and `subscribe` snapshots;
+- `listSessions`, `root/sessionAdded`, and `root/sessionSummaryChanged` deltas;
+- live and replayed `session/workingDirectorySet` / `session/workingDirectoryReplaced` actions;
+- reconnect snapshots and any full-snapshot reset.
+
+The capability remains in effect across reconnect, for both replay and snapshot recovery.
+
+Set actions carry `directory: URI | WorkingDirectory`; replacement actions carry the target `directory: URI` and `replacement: URI | WorkingDirectory`; removal carries only a URI. Clients MUST NOT supply `repo` or non-local provenance. Hosts MUST validate and enrich client input before broadcasting the accepted action. A rich set replaces the complete same-URI record; a URI-only duplicate preserves known metadata. Remove and replace compare URI keys and preserve existing ordering and primary-slot rules; a URI-only replacement preserves metadata already known for its URI.
+
+Chats retain `workingDirectories?: URI[]`: each URI matches a session URI string or record's `uri`. Chats do not duplicate directory metadata.
+
+##### Lost creation responses and cleanup
+
+`createSession` remains non-idempotent. An existing URI returns `SessionAlreadyExists` (`-32003`) and MUST NOT start another preparation. There is no creation token or original-request echo. After a lost response, a duplicate error MUST NOT be treated as proof of successful creation, and clients MUST NOT automatically send pending turns on that basis. A user may explicitly reopen the existing session or retry with a new URI. Ordinary reconnect of an already attached session remains unchanged.
+
+The host owns resources it creates, not arbitrary existing or shared directories. This capability adds no cancellation RPC or disposal rule; `repo` and `origin` do not authorize deletion.
+
### Active session
Once a session reaches `lifecycle: 'ready'`, clients may create chats on it with [`createChat`](/reference/chat#createchat). Each chat is independently subscribable at its own `ahp-chat:/` URI; see the [Chat Channel specification](./chat-channel) for the per-chat lifecycle, turn flow, tool calls, and input request handling.
diff --git a/schema/actions.schema.json b/schema/actions.schema.json
index 154cb2ac0..61de6c3ce 100644
--- a/schema/actions.schema.json
+++ b/schema/actions.schema.json
@@ -439,13 +439,20 @@
},
"SessionWorkingDirectorySetAction": {
"type": "object",
- "description": "A working directory was added to the session's\n{@link SessionState.workingDirectories} set.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the set does not already contain it (creating the set if\nabsent) and is a no-op when it is already present. Only valid when the agent\nadvertises {@link AgentCapabilities.multipleWorkingDirectories}.",
+ "description": "A working directory was added or updated in the session's\n{@link SessionState.workingDirectories} set.\n\nUpsert keyed by URI. A rich record replaces the existing entry in place;\na URI-only duplicate preserves known metadata. Only valid when the agent\nadvertises {@link AgentCapabilities.multipleWorkingDirectories}. Clients\nMUST omit `repo` and use only local or absent `origin`; the host validates\nand enriches the entry before accepting it.",
"properties": {
"type": {
"const": "session/workingDirectorySet"
},
"directory": {
- "$ref": "#/$defs/URI",
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ],
"description": "The working directory to grant the session's agent tool access to."
}
},
@@ -473,7 +480,7 @@
},
"SessionWorkingDirectoryReplacedAction": {
"type": "object",
- "description": "Atomically replaces one of the session's working directories.\n\nThis is a targeted compare-and-swap: the reducer is a no-op when\n{@link SessionState.workingDirectories} does not contain `directory`.\nOtherwise it replaces that entry with `replacement` and deduplicates the\nresult, preserving every other directory's relative order. When\n`replacement` occurs after the target, it moves to the target's position;\nfor example, `[A, B, C]` with `B → C` becomes `[A, C]`. When it occurs\nbefore the target, it retains its earlier position and the target is removed;\n`[A, B, C]` with `C → A` becomes `[A, B]`.\n\nOnly valid when the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}. Replacing index `0`\nadditionally requires\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement}; clients\nMUST NOT target an immutable primary. The host MUST validate and apply its\nbackend side effect before broadcasting an accepted action, or reject it.",
+ "description": "Atomically replaces one of the session's working directories.\n\nThis is a targeted compare-and-swap: the reducer is a no-op when\n{@link SessionState.workingDirectories} does not contain `directory`.\nOtherwise it replaces that entry with `replacement` and deduplicates the\nresult, preserving every other directory's relative order. When\n`replacement` occurs after the target, it moves to the target's position;\nfor example, `[A, B, C]` with `B → C` becomes `[A, C]`. When it occurs\nbefore the target, it retains its earlier position and the target is removed;\n`[A, B, C]` with `C → A` becomes `[A, B]`.\nA rich replacement supplies the complete entry; a URI-only replacement\npreserves metadata already known for that URI.\n\nOnly valid when the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}. Replacing index `0`\nadditionally requires\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement}; clients\nMUST NOT target an immutable primary. The host MUST validate and apply its\nbackend side effect before broadcasting an accepted action, or reject it.\nClients MUST omit `repo` and use only local or absent `origin`.",
"properties": {
"type": {
"const": "session/workingDirectoryReplaced"
@@ -483,8 +490,15 @@
"description": "URI of the existing entry to replace."
},
"replacement": {
- "$ref": "#/$defs/URI",
- "description": "URI to place in the replaced entry's position."
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ],
+ "description": "Complete entry to place in the replaced entry's position."
}
},
"required": [
@@ -1462,7 +1476,7 @@
},
"ChatWorkingDirectorySetAction": {
"type": "object",
- "description": "A working directory was added to this chat's\n{@link ChatState.workingDirectories} subset.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the chat's subset does not already contain it (creating the\nsubset if absent) and is a no-op when it is already present. `directory` MUST\nbe one of the owning session's {@link SessionState.workingDirectories}; a host\nMUST reject a directory that is not. Only valid when the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}.",
+ "description": "A working directory was added to this chat's\n{@link ChatState.workingDirectories} subset.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the chat's subset does not already contain it (creating the\nsubset if absent) and is a no-op when it is already present. `directory` MUST\nmatch a URI string or record's `uri` in the owning session's\n{@link SessionState.workingDirectories}; a host MUST reject a directory that\ndoes not. Only valid when the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}.",
"properties": {
"type": {
"const": "chat/workingDirectorySet"
@@ -3493,6 +3507,65 @@
"run"
]
},
+ "LocalWorkingDirectoryOrigin": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "local"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "RepoWorkingDirectoryOrigin": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "repo"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "WorktreeWorkingDirectoryOrigin": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "worktree"
+ },
+ "mainWorktree": {
+ "$ref": "#/$defs/URI",
+ "description": "Main worktree associated with the host-prepared worktree."
+ }
+ },
+ "required": [
+ "kind",
+ "mainWorktree"
+ ]
+ },
+ "WorkingDirectory": {
+ "type": "object",
+ "description": "An actual working directory, uniquely keyed by `uri` within the session.",
+ "properties": {
+ "uri": {
+ "$ref": "#/$defs/URI",
+ "description": "Actual selected directory, which may be a repository subdirectory."
+ },
+ "repo": {
+ "$ref": "#/$defs/URI",
+ "description": "Credential-free repository source association, not a checkout identity."
+ },
+ "origin": {
+ "$ref": "#/$defs/WorkingDirectoryOrigin",
+ "description": "Host-reported provenance; omission means unspecified."
+ }
+ },
+ "required": [
+ "uri"
+ ]
+ },
"SessionMetadata": {
"type": "object",
"description": "Metadata shared between the full {@link SessionState} (delivered when a\nclient subscribes to a session's URI) and the lightweight\n{@link SessionSummary} (carried in the root-channel session catalog).\n\nThese fields describe the session at a glance and appear in both places.\n`SessionState` owns the authoritative values for a subscribed session;\n`SessionSummary` mirrors them into the catalog so clients that only render a\nsession list don't have to subscribe to every session URI. The host keeps\nthe catalog in sync via `root/sessionSummaryChanged`.",
@@ -3524,9 +3597,16 @@
"workingDirectories": {
"type": "array",
"items": {
- "$ref": "#/$defs/URI"
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ]
},
- "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
+ "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set.\nEntries are uniquely keyed by URI. Rich records require\n{@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs."
},
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
@@ -3570,9 +3650,16 @@
"workingDirectories": {
"type": "array",
"items": {
- "$ref": "#/$defs/URI"
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ]
},
- "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
+ "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set.\nEntries are uniquely keyed by URI. Rich records require\n{@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs."
},
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
@@ -3878,9 +3965,16 @@
"workingDirectories": {
"type": "array",
"items": {
- "$ref": "#/$defs/URI"
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ]
},
- "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
+ "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set.\nEntries are uniquely keyed by URI. Rich records require\n{@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs."
},
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
@@ -5281,7 +5375,7 @@
"items": {
"$ref": "#/$defs/URI"
},
- "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat."
+ "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every URI MUST match a URI string or a\nrecord's `uri` in the owning session's set; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDirectory metadata belongs to the session, not the chat.\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat."
},
"changesets": {
"type": "array",
@@ -8839,6 +8933,20 @@
"$ref": "#/$defs/AutomationSessionOrigin",
"description": "Durable provenance for sessions created by a higher-level AHP workflow."
},
+ "WorkingDirectoryOrigin": {
+ "oneOf": [
+ {
+ "$ref": "#/$defs/LocalWorkingDirectoryOrigin"
+ },
+ {
+ "$ref": "#/$defs/RepoWorkingDirectoryOrigin"
+ },
+ {
+ "$ref": "#/$defs/WorktreeWorkingDirectoryOrigin"
+ }
+ ],
+ "description": "Host-reported preparation result, independent of the creation input."
+ },
"SessionInputRequest": {
"oneOf": [
{
diff --git a/schema/commands.schema.json b/schema/commands.schema.json
index dda6fae69..387858cbc 100644
--- a/schema/commands.schema.json
+++ b/schema/commands.schema.json
@@ -124,6 +124,10 @@
"type": "object",
"description": "Optional capabilities a client declares during `initialize`.\n\nEach field is a presence flag: an empty object `{}` means \"supported\",\nabsence means \"not supported\". Sub-fields on individual capabilities\nare reserved for future per-capability options.",
"properties": {
+ "workingDirectoryInfo": {
+ "$ref": "#/$defs/WorkingDirectoryInfoCapability",
+ "description": "Client accepts rich {@link WorkingDirectory} records as well as URI strings.\nHosts project records to URIs when absent and retain this choice on reconnect."
+ },
"mcpApps": {
"type": "object",
"additionalProperties": {},
@@ -131,6 +135,11 @@
}
}
},
+ "WorkingDirectoryInfoCapability": {
+ "type": "object",
+ "description": "Presence capability for {@link ClientCapabilities.workingDirectoryInfo}.",
+ "properties": {}
+ },
"HostEnvironment": {
"type": "object",
"description": "Describes the connection's default host-side execution environment.\n\nThe descriptor applies to {@link InitializeResult.defaultDirectory},\nsession working directories, and `file:` resources handled by the server\nthrough the `resource*` commands, unless more specific environment metadata\nis available. It does not apply to resources provided by the client.\n\nA host that exposes mixed environments MUST only populate fields that have a\nsingle connection-wide default.",
@@ -177,6 +186,10 @@
"$ref": "#/$defs/URI",
"description": "Suggested default directory for remote filesystem browsing"
},
+ "repositoryPreparation": {
+ "$ref": "#/$defs/RepositoryPreparationCapability",
+ "description": "Host repository preparation support; absent when unsupported."
+ },
"completionTriggerCharacters": {
"type": "array",
"items": {
@@ -216,6 +229,20 @@
"description": "Presence capability for {@link InitializeResult.multipleChatProviders}.\n\nStability: 1.0 - Early development.",
"properties": {}
},
+ "RepositoryPreparationCapability": {
+ "type": "object",
+ "description": "Host support for repository preparation at session creation. Presence\nalone supports one repository at its default revision.",
+ "properties": {
+ "revision": {
+ "type": "boolean",
+ "description": "When true, clients may supply {@link RepositorySource.revision}."
+ },
+ "multipleRepositories": {
+ "type": "boolean",
+ "description": "When true, clients may supply more than one repository."
+ }
+ }
+ },
"AutomationCapabilities": {
"type": "object",
"description": "Automation features supported by this host authority.\n\nThe presence of this object advertises the baseline `ahp-automations://`\ncatalogue. Optional fields describe additional host features and\nrestrictions.\n\nCapabilities describe implementation support.\n{@link AutomationEntry.operations} remains authoritative for which\ndefinition mutations are currently allowed on a particular automation.",
@@ -1148,6 +1175,13 @@
"$ref": "#/$defs/URI",
"description": "Working directory for the session"
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Repositories used as configuration context; an empty list is equivalent to omission."
+ },
"config": {
"type": "object",
"additionalProperties": {},
@@ -1222,6 +1256,13 @@
"$ref": "#/$defs/URI",
"description": "Working directory for the session"
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Repositories used as configuration context; an empty list is equivalent to omission."
+ },
"config": {
"type": "object",
"additionalProperties": {},
@@ -1257,6 +1298,27 @@
"items"
]
},
+ "RepositorySource": {
+ "type": "object",
+ "description": "Requested repository source, not a resolved working directory.",
+ "properties": {
+ "source": {
+ "$ref": "#/$defs/URI",
+ "description": "Credential-free repository source URI."
+ },
+ "revision": {
+ "type": "string",
+ "description": "Requested branch, tag, or commit. Omit to use the host's default revision."
+ },
+ "subdirectory": {
+ "type": "string",
+ "description": "Repository-relative selected folder; omit for the root. Hosts reject empty, absolute, or escaping paths."
+ }
+ },
+ "required": [
+ "source"
+ ]
+ },
"CreateSessionParams": {
"type": "object",
"description": "Creates a new session with the specified agent provider.\n\nIf the session URI already exists, the server MUST return an error with code\n`-32003` (`SessionAlreadyExists`).\n\nAfter creation, the client should subscribe to the session URI to receive state\nupdates. The server also broadcasts a `root/sessionAdded` notification to all\nclients.",
@@ -1281,6 +1343,13 @@
},
"description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started."
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Repositories to prepare instead of an explicit `workingDirectories` list.\nAn empty list is equivalent to omission."
+ },
"config": {
"type": "object",
"additionalProperties": {},
@@ -1499,7 +1568,7 @@
"items": {
"$ref": "#/$defs/URI"
},
- "description": "Initial working-directory subset for this chat. Every entry MUST be\npresent in the owning session's `workingDirectories`; the server MUST\nreject any entry that is not. When absent, the chat inherits the full\nsession set. Forked chats (those whose `source.kind` is `\"fork\"`) inherit\nthe source chat's `workingDirectories`; this field is ignored for forks.\n\nA client MUST NOT supply this field unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}."
+ "description": "Initial working-directory URI subset for this chat. Every URI MUST match\na URI string or record's `uri` in the owning session's `workingDirectories`;\nthe server MUST reject any entry that does not. When absent, the chat inherits the full\nsession set. Forked chats (those whose `source.kind` is `\"fork\"`) inherit\nthe source chat's `workingDirectories`; this field is ignored for forks.\n\nA client MUST NOT supply this field unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}."
},
"title": {
"type": "string",
@@ -2825,6 +2894,65 @@
"run"
]
},
+ "LocalWorkingDirectoryOrigin": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "local"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "RepoWorkingDirectoryOrigin": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "repo"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "WorktreeWorkingDirectoryOrigin": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "worktree"
+ },
+ "mainWorktree": {
+ "$ref": "#/$defs/URI",
+ "description": "Main worktree associated with the host-prepared worktree."
+ }
+ },
+ "required": [
+ "kind",
+ "mainWorktree"
+ ]
+ },
+ "WorkingDirectory": {
+ "type": "object",
+ "description": "An actual working directory, uniquely keyed by `uri` within the session.",
+ "properties": {
+ "uri": {
+ "$ref": "#/$defs/URI",
+ "description": "Actual selected directory, which may be a repository subdirectory."
+ },
+ "repo": {
+ "$ref": "#/$defs/URI",
+ "description": "Credential-free repository source association, not a checkout identity."
+ },
+ "origin": {
+ "$ref": "#/$defs/WorkingDirectoryOrigin",
+ "description": "Host-reported provenance; omission means unspecified."
+ }
+ },
+ "required": [
+ "uri"
+ ]
+ },
"SessionMetadata": {
"type": "object",
"description": "Metadata shared between the full {@link SessionState} (delivered when a\nclient subscribes to a session's URI) and the lightweight\n{@link SessionSummary} (carried in the root-channel session catalog).\n\nThese fields describe the session at a glance and appear in both places.\n`SessionState` owns the authoritative values for a subscribed session;\n`SessionSummary` mirrors them into the catalog so clients that only render a\nsession list don't have to subscribe to every session URI. The host keeps\nthe catalog in sync via `root/sessionSummaryChanged`.",
@@ -2856,9 +2984,16 @@
"workingDirectories": {
"type": "array",
"items": {
- "$ref": "#/$defs/URI"
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ]
},
- "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
+ "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set.\nEntries are uniquely keyed by URI. Rich records require\n{@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs."
},
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
@@ -2902,9 +3037,16 @@
"workingDirectories": {
"type": "array",
"items": {
- "$ref": "#/$defs/URI"
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ]
},
- "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
+ "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set.\nEntries are uniquely keyed by URI. Rich records require\n{@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs."
},
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
@@ -3210,9 +3352,16 @@
"workingDirectories": {
"type": "array",
"items": {
- "$ref": "#/$defs/URI"
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ]
},
- "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
+ "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set.\nEntries are uniquely keyed by URI. Rich records require\n{@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs."
},
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
@@ -4613,7 +4762,7 @@
"items": {
"$ref": "#/$defs/URI"
},
- "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat."
+ "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every URI MUST match a URI string or a\nrecord's `uri` in the owning session's set; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDirectory metadata belongs to the session, not the chat.\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat."
},
"changesets": {
"type": "array",
@@ -8565,13 +8714,20 @@
},
"SessionWorkingDirectorySetAction": {
"type": "object",
- "description": "A working directory was added to the session's\n{@link SessionState.workingDirectories} set.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the set does not already contain it (creating the set if\nabsent) and is a no-op when it is already present. Only valid when the agent\nadvertises {@link AgentCapabilities.multipleWorkingDirectories}.",
+ "description": "A working directory was added or updated in the session's\n{@link SessionState.workingDirectories} set.\n\nUpsert keyed by URI. A rich record replaces the existing entry in place;\na URI-only duplicate preserves known metadata. Only valid when the agent\nadvertises {@link AgentCapabilities.multipleWorkingDirectories}. Clients\nMUST omit `repo` and use only local or absent `origin`; the host validates\nand enriches the entry before accepting it.",
"properties": {
"type": {
"const": "session/workingDirectorySet"
},
"directory": {
- "$ref": "#/$defs/URI",
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ],
"description": "The working directory to grant the session's agent tool access to."
}
},
@@ -8599,7 +8755,7 @@
},
"SessionWorkingDirectoryReplacedAction": {
"type": "object",
- "description": "Atomically replaces one of the session's working directories.\n\nThis is a targeted compare-and-swap: the reducer is a no-op when\n{@link SessionState.workingDirectories} does not contain `directory`.\nOtherwise it replaces that entry with `replacement` and deduplicates the\nresult, preserving every other directory's relative order. When\n`replacement` occurs after the target, it moves to the target's position;\nfor example, `[A, B, C]` with `B → C` becomes `[A, C]`. When it occurs\nbefore the target, it retains its earlier position and the target is removed;\n`[A, B, C]` with `C → A` becomes `[A, B]`.\n\nOnly valid when the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}. Replacing index `0`\nadditionally requires\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement}; clients\nMUST NOT target an immutable primary. The host MUST validate and apply its\nbackend side effect before broadcasting an accepted action, or reject it.",
+ "description": "Atomically replaces one of the session's working directories.\n\nThis is a targeted compare-and-swap: the reducer is a no-op when\n{@link SessionState.workingDirectories} does not contain `directory`.\nOtherwise it replaces that entry with `replacement` and deduplicates the\nresult, preserving every other directory's relative order. When\n`replacement` occurs after the target, it moves to the target's position;\nfor example, `[A, B, C]` with `B → C` becomes `[A, C]`. When it occurs\nbefore the target, it retains its earlier position and the target is removed;\n`[A, B, C]` with `C → A` becomes `[A, B]`.\nA rich replacement supplies the complete entry; a URI-only replacement\npreserves metadata already known for that URI.\n\nOnly valid when the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}. Replacing index `0`\nadditionally requires\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement}; clients\nMUST NOT target an immutable primary. The host MUST validate and apply its\nbackend side effect before broadcasting an accepted action, or reject it.\nClients MUST omit `repo` and use only local or absent `origin`.",
"properties": {
"type": {
"const": "session/workingDirectoryReplaced"
@@ -8609,8 +8765,15 @@
"description": "URI of the existing entry to replace."
},
"replacement": {
- "$ref": "#/$defs/URI",
- "description": "URI to place in the replaced entry's position."
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ],
+ "description": "Complete entry to place in the replaced entry's position."
}
},
"required": [
@@ -9588,7 +9751,7 @@
},
"ChatWorkingDirectorySetAction": {
"type": "object",
- "description": "A working directory was added to this chat's\n{@link ChatState.workingDirectories} subset.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the chat's subset does not already contain it (creating the\nsubset if absent) and is a no-op when it is already present. `directory` MUST\nbe one of the owning session's {@link SessionState.workingDirectories}; a host\nMUST reject a directory that is not. Only valid when the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}.",
+ "description": "A working directory was added to this chat's\n{@link ChatState.workingDirectories} subset.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the chat's subset does not already contain it (creating the\nsubset if absent) and is a no-op when it is already present. `directory` MUST\nmatch a URI string or record's `uri` in the owning session's\n{@link SessionState.workingDirectories}; a host MUST reject a directory that\ndoes not. Only valid when the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}.",
"properties": {
"type": {
"const": "chat/workingDirectorySet"
@@ -11188,6 +11351,20 @@
"type": "string",
"description": "Policy configuration state for a model."
},
+ "WorkingDirectoryOrigin": {
+ "oneOf": [
+ {
+ "$ref": "#/$defs/LocalWorkingDirectoryOrigin"
+ },
+ {
+ "$ref": "#/$defs/RepoWorkingDirectoryOrigin"
+ },
+ {
+ "$ref": "#/$defs/WorktreeWorkingDirectoryOrigin"
+ }
+ ],
+ "description": "Host-reported preparation result, independent of the creation input."
+ },
"SessionStatus": {
"type": "integer",
"minimum": 0,
diff --git a/schema/errors.schema.json b/schema/errors.schema.json
index 20d2481de..90acc6b2a 100644
--- a/schema/errors.schema.json
+++ b/schema/errors.schema.json
@@ -866,6 +866,65 @@
"run"
]
},
+ "LocalWorkingDirectoryOrigin": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "local"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "RepoWorkingDirectoryOrigin": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "repo"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "WorktreeWorkingDirectoryOrigin": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "worktree"
+ },
+ "mainWorktree": {
+ "$ref": "#/$defs/URI",
+ "description": "Main worktree associated with the host-prepared worktree."
+ }
+ },
+ "required": [
+ "kind",
+ "mainWorktree"
+ ]
+ },
+ "WorkingDirectory": {
+ "type": "object",
+ "description": "An actual working directory, uniquely keyed by `uri` within the session.",
+ "properties": {
+ "uri": {
+ "$ref": "#/$defs/URI",
+ "description": "Actual selected directory, which may be a repository subdirectory."
+ },
+ "repo": {
+ "$ref": "#/$defs/URI",
+ "description": "Credential-free repository source association, not a checkout identity."
+ },
+ "origin": {
+ "$ref": "#/$defs/WorkingDirectoryOrigin",
+ "description": "Host-reported provenance; omission means unspecified."
+ }
+ },
+ "required": [
+ "uri"
+ ]
+ },
"SessionMetadata": {
"type": "object",
"description": "Metadata shared between the full {@link SessionState} (delivered when a\nclient subscribes to a session's URI) and the lightweight\n{@link SessionSummary} (carried in the root-channel session catalog).\n\nThese fields describe the session at a glance and appear in both places.\n`SessionState` owns the authoritative values for a subscribed session;\n`SessionSummary` mirrors them into the catalog so clients that only render a\nsession list don't have to subscribe to every session URI. The host keeps\nthe catalog in sync via `root/sessionSummaryChanged`.",
@@ -897,9 +956,16 @@
"workingDirectories": {
"type": "array",
"items": {
- "$ref": "#/$defs/URI"
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ]
},
- "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
+ "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set.\nEntries are uniquely keyed by URI. Rich records require\n{@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs."
},
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
@@ -943,9 +1009,16 @@
"workingDirectories": {
"type": "array",
"items": {
- "$ref": "#/$defs/URI"
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ]
},
- "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
+ "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set.\nEntries are uniquely keyed by URI. Rich records require\n{@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs."
},
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
@@ -1251,9 +1324,16 @@
"workingDirectories": {
"type": "array",
"items": {
- "$ref": "#/$defs/URI"
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ]
},
- "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
+ "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set.\nEntries are uniquely keyed by URI. Rich records require\n{@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs."
},
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
@@ -2654,7 +2734,7 @@
"items": {
"$ref": "#/$defs/URI"
},
- "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat."
+ "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every URI MUST match a URI string or a\nrecord's `uri` in the owning session's set; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDirectory metadata belongs to the session, not the chat.\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat."
},
"changesets": {
"type": "array",
@@ -6291,6 +6371,10 @@
"type": "object",
"description": "Optional capabilities a client declares during `initialize`.\n\nEach field is a presence flag: an empty object `{}` means \"supported\",\nabsence means \"not supported\". Sub-fields on individual capabilities\nare reserved for future per-capability options.",
"properties": {
+ "workingDirectoryInfo": {
+ "$ref": "#/$defs/WorkingDirectoryInfoCapability",
+ "description": "Client accepts rich {@link WorkingDirectory} records as well as URI strings.\nHosts project records to URIs when absent and retain this choice on reconnect."
+ },
"mcpApps": {
"type": "object",
"additionalProperties": {},
@@ -6298,6 +6382,11 @@
}
}
},
+ "WorkingDirectoryInfoCapability": {
+ "type": "object",
+ "description": "Presence capability for {@link ClientCapabilities.workingDirectoryInfo}.",
+ "properties": {}
+ },
"HostEnvironment": {
"type": "object",
"description": "Describes the connection's default host-side execution environment.\n\nThe descriptor applies to {@link InitializeResult.defaultDirectory},\nsession working directories, and `file:` resources handled by the server\nthrough the `resource*` commands, unless more specific environment metadata\nis available. It does not apply to resources provided by the client.\n\nA host that exposes mixed environments MUST only populate fields that have a\nsingle connection-wide default.",
@@ -6344,6 +6433,10 @@
"$ref": "#/$defs/URI",
"description": "Suggested default directory for remote filesystem browsing"
},
+ "repositoryPreparation": {
+ "$ref": "#/$defs/RepositoryPreparationCapability",
+ "description": "Host repository preparation support; absent when unsupported."
+ },
"completionTriggerCharacters": {
"type": "array",
"items": {
@@ -6383,6 +6476,20 @@
"description": "Presence capability for {@link InitializeResult.multipleChatProviders}.\n\nStability: 1.0 - Early development.",
"properties": {}
},
+ "RepositoryPreparationCapability": {
+ "type": "object",
+ "description": "Host support for repository preparation at session creation. Presence\nalone supports one repository at its default revision.",
+ "properties": {
+ "revision": {
+ "type": "boolean",
+ "description": "When true, clients may supply {@link RepositorySource.revision}."
+ },
+ "multipleRepositories": {
+ "type": "boolean",
+ "description": "When true, clients may supply more than one repository."
+ }
+ }
+ },
"AutomationCapabilities": {
"type": "object",
"description": "Automation features supported by this host authority.\n\nThe presence of this object advertises the baseline `ahp-automations://`\ncatalogue. Optional fields describe additional host features and\nrestrictions.\n\nCapabilities describe implementation support.\n{@link AutomationEntry.operations} remains authoritative for which\ndefinition mutations are currently allowed on a particular automation.",
@@ -7315,6 +7422,13 @@
"$ref": "#/$defs/URI",
"description": "Working directory for the session"
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Repositories used as configuration context; an empty list is equivalent to omission."
+ },
"config": {
"type": "object",
"additionalProperties": {},
@@ -7389,6 +7503,13 @@
"$ref": "#/$defs/URI",
"description": "Working directory for the session"
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Repositories used as configuration context; an empty list is equivalent to omission."
+ },
"config": {
"type": "object",
"additionalProperties": {},
@@ -7424,6 +7545,27 @@
"items"
]
},
+ "RepositorySource": {
+ "type": "object",
+ "description": "Requested repository source, not a resolved working directory.",
+ "properties": {
+ "source": {
+ "$ref": "#/$defs/URI",
+ "description": "Credential-free repository source URI."
+ },
+ "revision": {
+ "type": "string",
+ "description": "Requested branch, tag, or commit. Omit to use the host's default revision."
+ },
+ "subdirectory": {
+ "type": "string",
+ "description": "Repository-relative selected folder; omit for the root. Hosts reject empty, absolute, or escaping paths."
+ }
+ },
+ "required": [
+ "source"
+ ]
+ },
"CreateSessionParams": {
"type": "object",
"description": "Creates a new session with the specified agent provider.\n\nIf the session URI already exists, the server MUST return an error with code\n`-32003` (`SessionAlreadyExists`).\n\nAfter creation, the client should subscribe to the session URI to receive state\nupdates. The server also broadcasts a `root/sessionAdded` notification to all\nclients.",
@@ -7448,6 +7590,13 @@
},
"description": "The working directories the session's agent is granted tool access to.\nA session may span multiple directories; they are equal peers except when\nthe agent advertises a protected-primary capability. An\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary | immutable\nprimary} is fixed, while a\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement | replaceable\nprimary} is changed only with `session/workingDirectoryReplaced`.\n\nA client MUST NOT supply more than one entry unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}; a server without that\ncapability treats only the first entry as the session's working directory\nand ignores the rest. Dispatch working-directory actions to change the set\nafter the session has started."
},
+ "repositories": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/RepositorySource"
+ },
+ "description": "Repositories to prepare instead of an explicit `workingDirectories` list.\nAn empty list is equivalent to omission."
+ },
"config": {
"type": "object",
"additionalProperties": {},
@@ -7666,7 +7815,7 @@
"items": {
"$ref": "#/$defs/URI"
},
- "description": "Initial working-directory subset for this chat. Every entry MUST be\npresent in the owning session's `workingDirectories`; the server MUST\nreject any entry that is not. When absent, the chat inherits the full\nsession set. Forked chats (those whose `source.kind` is `\"fork\"`) inherit\nthe source chat's `workingDirectories`; this field is ignored for forks.\n\nA client MUST NOT supply this field unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}."
+ "description": "Initial working-directory URI subset for this chat. Every URI MUST match\na URI string or record's `uri` in the owning session's `workingDirectories`;\nthe server MUST reject any entry that does not. When absent, the chat inherits the full\nsession set. Forked chats (those whose `source.kind` is `\"fork\"`) inherit\nthe source chat's `workingDirectories`; this field is ignored for forks.\n\nA client MUST NOT supply this field unless the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}."
},
"title": {
"type": "string",
@@ -8177,6 +8326,20 @@
"type": "string",
"description": "Policy configuration state for a model."
},
+ "WorkingDirectoryOrigin": {
+ "oneOf": [
+ {
+ "$ref": "#/$defs/LocalWorkingDirectoryOrigin"
+ },
+ {
+ "$ref": "#/$defs/RepoWorkingDirectoryOrigin"
+ },
+ {
+ "$ref": "#/$defs/WorktreeWorkingDirectoryOrigin"
+ }
+ ],
+ "description": "Host-reported preparation result, independent of the creation input."
+ },
"SessionStatus": {
"type": "integer",
"minimum": 0,
@@ -9700,13 +9863,20 @@
},
"SessionWorkingDirectorySetAction": {
"type": "object",
- "description": "A working directory was added to the session's\n{@link SessionState.workingDirectories} set.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the set does not already contain it (creating the set if\nabsent) and is a no-op when it is already present. Only valid when the agent\nadvertises {@link AgentCapabilities.multipleWorkingDirectories}.",
+ "description": "A working directory was added or updated in the session's\n{@link SessionState.workingDirectories} set.\n\nUpsert keyed by URI. A rich record replaces the existing entry in place;\na URI-only duplicate preserves known metadata. Only valid when the agent\nadvertises {@link AgentCapabilities.multipleWorkingDirectories}. Clients\nMUST omit `repo` and use only local or absent `origin`; the host validates\nand enriches the entry before accepting it.",
"properties": {
"type": {
"const": "session/workingDirectorySet"
},
"directory": {
- "$ref": "#/$defs/URI",
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ],
"description": "The working directory to grant the session's agent tool access to."
}
},
@@ -9734,7 +9904,7 @@
},
"SessionWorkingDirectoryReplacedAction": {
"type": "object",
- "description": "Atomically replaces one of the session's working directories.\n\nThis is a targeted compare-and-swap: the reducer is a no-op when\n{@link SessionState.workingDirectories} does not contain `directory`.\nOtherwise it replaces that entry with `replacement` and deduplicates the\nresult, preserving every other directory's relative order. When\n`replacement` occurs after the target, it moves to the target's position;\nfor example, `[A, B, C]` with `B → C` becomes `[A, C]`. When it occurs\nbefore the target, it retains its earlier position and the target is removed;\n`[A, B, C]` with `C → A` becomes `[A, B]`.\n\nOnly valid when the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}. Replacing index `0`\nadditionally requires\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement}; clients\nMUST NOT target an immutable primary. The host MUST validate and apply its\nbackend side effect before broadcasting an accepted action, or reject it.",
+ "description": "Atomically replaces one of the session's working directories.\n\nThis is a targeted compare-and-swap: the reducer is a no-op when\n{@link SessionState.workingDirectories} does not contain `directory`.\nOtherwise it replaces that entry with `replacement` and deduplicates the\nresult, preserving every other directory's relative order. When\n`replacement` occurs after the target, it moves to the target's position;\nfor example, `[A, B, C]` with `B → C` becomes `[A, C]`. When it occurs\nbefore the target, it retains its earlier position and the target is removed;\n`[A, B, C]` with `C → A` becomes `[A, B]`.\nA rich replacement supplies the complete entry; a URI-only replacement\npreserves metadata already known for that URI.\n\nOnly valid when the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}. Replacing index `0`\nadditionally requires\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement}; clients\nMUST NOT target an immutable primary. The host MUST validate and apply its\nbackend side effect before broadcasting an accepted action, or reject it.\nClients MUST omit `repo` and use only local or absent `origin`.",
"properties": {
"type": {
"const": "session/workingDirectoryReplaced"
@@ -9744,8 +9914,15 @@
"description": "URI of the existing entry to replace."
},
"replacement": {
- "$ref": "#/$defs/URI",
- "description": "URI to place in the replaced entry's position."
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ],
+ "description": "Complete entry to place in the replaced entry's position."
}
},
"required": [
@@ -10698,7 +10875,7 @@
},
"ChatWorkingDirectorySetAction": {
"type": "object",
- "description": "A working directory was added to this chat's\n{@link ChatState.workingDirectories} subset.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the chat's subset does not already contain it (creating the\nsubset if absent) and is a no-op when it is already present. `directory` MUST\nbe one of the owning session's {@link SessionState.workingDirectories}; a host\nMUST reject a directory that is not. Only valid when the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}.",
+ "description": "A working directory was added to this chat's\n{@link ChatState.workingDirectories} subset.\n\nMembership semantics keyed by the directory URI: the reducer appends\n`directory` when the chat's subset does not already contain it (creating the\nsubset if absent) and is a no-op when it is already present. `directory` MUST\nmatch a URI string or record's `uri` in the owning session's\n{@link SessionState.workingDirectories}; a host MUST reject a directory that\ndoes not. Only valid when the agent advertises\n{@link AgentCapabilities.multipleWorkingDirectories}.",
"properties": {
"type": {
"const": "chat/workingDirectorySet"
diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json
index 40702c709..9861edd75 100644
--- a/schema/notifications.schema.json
+++ b/schema/notifications.schema.json
@@ -166,9 +166,16 @@
"workingDirectories": {
"type": "array",
"items": {
- "$ref": "#/$defs/URI"
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ]
},
- "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
+ "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set.\nEntries are uniquely keyed by URI. Rich records require\n{@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs."
},
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
@@ -1168,6 +1175,65 @@
"run"
]
},
+ "LocalWorkingDirectoryOrigin": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "local"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "RepoWorkingDirectoryOrigin": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "repo"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "WorktreeWorkingDirectoryOrigin": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "worktree"
+ },
+ "mainWorktree": {
+ "$ref": "#/$defs/URI",
+ "description": "Main worktree associated with the host-prepared worktree."
+ }
+ },
+ "required": [
+ "kind",
+ "mainWorktree"
+ ]
+ },
+ "WorkingDirectory": {
+ "type": "object",
+ "description": "An actual working directory, uniquely keyed by `uri` within the session.",
+ "properties": {
+ "uri": {
+ "$ref": "#/$defs/URI",
+ "description": "Actual selected directory, which may be a repository subdirectory."
+ },
+ "repo": {
+ "$ref": "#/$defs/URI",
+ "description": "Credential-free repository source association, not a checkout identity."
+ },
+ "origin": {
+ "$ref": "#/$defs/WorkingDirectoryOrigin",
+ "description": "Host-reported provenance; omission means unspecified."
+ }
+ },
+ "required": [
+ "uri"
+ ]
+ },
"SessionMetadata": {
"type": "object",
"description": "Metadata shared between the full {@link SessionState} (delivered when a\nclient subscribes to a session's URI) and the lightweight\n{@link SessionSummary} (carried in the root-channel session catalog).\n\nThese fields describe the session at a glance and appear in both places.\n`SessionState` owns the authoritative values for a subscribed session;\n`SessionSummary` mirrors them into the catalog so clients that only render a\nsession list don't have to subscribe to every session URI. The host keeps\nthe catalog in sync via `root/sessionSummaryChanged`.",
@@ -1199,9 +1265,16 @@
"workingDirectories": {
"type": "array",
"items": {
- "$ref": "#/$defs/URI"
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ]
},
- "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
+ "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set.\nEntries are uniquely keyed by URI. Rich records require\n{@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs."
},
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
@@ -1245,9 +1318,16 @@
"workingDirectories": {
"type": "array",
"items": {
- "$ref": "#/$defs/URI"
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ]
},
- "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
+ "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set.\nEntries are uniquely keyed by URI. Rich records require\n{@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs."
},
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
@@ -1553,9 +1633,16 @@
"workingDirectories": {
"type": "array",
"items": {
- "$ref": "#/$defs/URI"
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ]
},
- "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
+ "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set.\nEntries are uniquely keyed by URI. Rich records require\n{@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs."
},
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
@@ -2956,7 +3043,7 @@
"items": {
"$ref": "#/$defs/URI"
},
- "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat."
+ "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every URI MUST match a URI string or a\nrecord's `uri` in the owning session's set; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDirectory metadata belongs to the session, not the chat.\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat."
},
"changesets": {
"type": "array",
@@ -6545,6 +6632,20 @@
"type": "string",
"description": "Policy configuration state for a model."
},
+ "WorkingDirectoryOrigin": {
+ "oneOf": [
+ {
+ "$ref": "#/$defs/LocalWorkingDirectoryOrigin"
+ },
+ {
+ "$ref": "#/$defs/RepoWorkingDirectoryOrigin"
+ },
+ {
+ "$ref": "#/$defs/WorktreeWorkingDirectoryOrigin"
+ }
+ ],
+ "description": "Host-reported preparation result, independent of the creation input."
+ },
"SessionLifecycle": {
"enum": [
"creating",
diff --git a/schema/state.schema.json b/schema/state.schema.json
index 20c80d8cb..6009a349a 100644
--- a/schema/state.schema.json
+++ b/schema/state.schema.json
@@ -777,6 +777,65 @@
"run"
]
},
+ "LocalWorkingDirectoryOrigin": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "local"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "RepoWorkingDirectoryOrigin": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "repo"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "WorktreeWorkingDirectoryOrigin": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "worktree"
+ },
+ "mainWorktree": {
+ "$ref": "#/$defs/URI",
+ "description": "Main worktree associated with the host-prepared worktree."
+ }
+ },
+ "required": [
+ "kind",
+ "mainWorktree"
+ ]
+ },
+ "WorkingDirectory": {
+ "type": "object",
+ "description": "An actual working directory, uniquely keyed by `uri` within the session.",
+ "properties": {
+ "uri": {
+ "$ref": "#/$defs/URI",
+ "description": "Actual selected directory, which may be a repository subdirectory."
+ },
+ "repo": {
+ "$ref": "#/$defs/URI",
+ "description": "Credential-free repository source association, not a checkout identity."
+ },
+ "origin": {
+ "$ref": "#/$defs/WorkingDirectoryOrigin",
+ "description": "Host-reported provenance; omission means unspecified."
+ }
+ },
+ "required": [
+ "uri"
+ ]
+ },
"SessionMetadata": {
"type": "object",
"description": "Metadata shared between the full {@link SessionState} (delivered when a\nclient subscribes to a session's URI) and the lightweight\n{@link SessionSummary} (carried in the root-channel session catalog).\n\nThese fields describe the session at a glance and appear in both places.\n`SessionState` owns the authoritative values for a subscribed session;\n`SessionSummary` mirrors them into the catalog so clients that only render a\nsession list don't have to subscribe to every session URI. The host keeps\nthe catalog in sync via `root/sessionSummaryChanged`.",
@@ -808,9 +867,16 @@
"workingDirectories": {
"type": "array",
"items": {
- "$ref": "#/$defs/URI"
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ]
},
- "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
+ "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set.\nEntries are uniquely keyed by URI. Rich records require\n{@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs."
},
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
@@ -854,9 +920,16 @@
"workingDirectories": {
"type": "array",
"items": {
- "$ref": "#/$defs/URI"
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ]
},
- "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
+ "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set.\nEntries are uniquely keyed by URI. Rich records require\n{@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs."
},
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
@@ -1162,9 +1235,16 @@
"workingDirectories": {
"type": "array",
"items": {
- "$ref": "#/$defs/URI"
+ "oneOf": [
+ {
+ "$ref": "#/$defs/URI"
+ },
+ {
+ "$ref": "#/$defs/WorkingDirectory"
+ }
+ ]
},
- "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set."
+ "description": "The working directories the session's agent has tool access to, as\nmaintained by working-directory actions. Directories are equal peers except\nwhen the agent advertises\n{@link MultipleWorkingDirectoriesCapability.immutablePrimary} without\n{@link MultipleWorkingDirectoriesCapability.primaryReplacement} (the first\nentry is then a fixed process root), or advertises `primaryReplacement`\n(the first entry is a protected, replaceable primary slot). Individual chats\nMAY restrict to a subset via\n{@link ChatSummary.workingDirectories | their own `workingDirectories`}; a\nchat that sets none operates against this full set.\nEntries are uniquely keyed by URI. Rich records require\n{@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs."
},
"annotations": {
"$ref": "#/$defs/AnnotationsSummary",
@@ -2565,7 +2645,7 @@
"items": {
"$ref": "#/$defs/URI"
},
- "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every entry MUST be present in the owning\nsession's `workingDirectories`; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat."
+ "description": "The subset of the session's\n{@link SessionState.workingDirectories | `workingDirectories`} that this\nchat's agent has tool access to. Every URI MUST match a URI string or a\nrecord's `uri` in the owning session's set; servers MUST reject a\n`chat/workingDirectorySet` action that violates this constraint.\n\nWhen absent, the chat inherits the full session set. When present but empty\n(not recommended), the chat has no working-directory tool access at all.\n\nDirectory metadata belongs to the session, not the chat.\nDispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to\nupdate the subset on a running chat."
},
"changesets": {
"type": "array",
@@ -6123,6 +6203,20 @@
"$ref": "#/$defs/AutomationSessionOrigin",
"description": "Durable provenance for sessions created by a higher-level AHP workflow."
},
+ "WorkingDirectoryOrigin": {
+ "oneOf": [
+ {
+ "$ref": "#/$defs/LocalWorkingDirectoryOrigin"
+ },
+ {
+ "$ref": "#/$defs/RepoWorkingDirectoryOrigin"
+ },
+ {
+ "$ref": "#/$defs/WorktreeWorkingDirectoryOrigin"
+ }
+ ],
+ "description": "Host-reported preparation result, independent of the creation input."
+ },
"SessionInputRequest": {
"oneOf": [
{
diff --git a/scripts/generate-csharp.ts b/scripts/generate-csharp.ts
index f427e35b3..73bacdd3e 100644
--- a/scripts/generate-csharp.ts
+++ b/scripts/generate-csharp.ts
@@ -173,6 +173,7 @@ function mapType(tsType: string): string {
if (tsType === 'URI') return 'string';
if (tsType === 'StringOrMarkdown') return 'StringOrMarkdown';
+ if (tsType === 'URI | WorkingDirectory') return 'WorkingDirectoryEntry';
// ChildCustomizationType is a TS-only subset alias of CustomizationType.
if (tsType === 'ChildCustomizationType') return 'CustomizationType';
@@ -761,7 +762,7 @@ function generateDiscriminatedUnion(project: Project, cfg: UnionConfig): string
const STATE_ENUMS = [
'ChannelRecoveryKind',
'PolicyState', 'PendingMessageKind', 'SessionLifecycle', 'SessionStatus',
- 'SessionOriginKind',
+ 'SessionOriginKind', 'WorkingDirectoryOriginKind',
'ChatOriginKind', 'ChatInteractivity', 'ChatInputAnswerState', 'ChatInputAnswerValueKind',
'ChatInputQuestionKind', 'ChatInputResponseKind', 'SessionInputRequestKind',
'TurnState', 'MessageKind', 'MessageAttachmentKind', 'ResponsePartKind', 'ToolCallStatus',
@@ -793,6 +794,10 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: strin
{ name: 'AgentCapabilities' },
{ name: 'MultipleChatsCapability' },
{ name: 'MultipleWorkingDirectoriesCapability' },
+ { name: 'WorkingDirectory' },
+ { name: 'LocalWorkingDirectoryOrigin' },
+ { name: 'RepoWorkingDirectoryOrigin' },
+ { name: 'WorktreeWorkingDirectoryOrigin' },
{ name: 'SessionModelInfo' },
{ name: 'ModelSelection' },
{ name: 'AgentSelection' },
@@ -1034,6 +1039,37 @@ const TERMINAL_CONTENT_PART_UNION: UnionConfig = {
// inline form, an object is the content reference. Mirrors rust's
// `#[serde(untagged)] enum ToolInput { Inline(String), ContentRef(ContentRef) }`
// (clients/rust/crates/ahp-types/src/state.rs) and swift's `.inline` / `.contentRef`.
+const WORKING_DIRECTORY_ENTRY_CS = `/// A legacy URI or a complete working-directory record.
+[JsonConverter(typeof(WorkingDirectoryEntryConverter))]
+public sealed class WorkingDirectoryEntry
+{
+ public WorkingDirectoryEntry(string uri) { Uri = uri; }
+ public WorkingDirectoryEntry(WorkingDirectory directory) { Uri = directory.Uri; Directory = directory; }
+
+ public string Uri { get; }
+ public WorkingDirectory? Directory { get; }
+}
+
+internal sealed class WorkingDirectoryEntryConverter : JsonConverter
+{
+ public override WorkingDirectoryEntry Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ return new WorkingDirectoryEntry(reader.GetString()!);
+ }
+ return new WorkingDirectoryEntry(
+ JsonSerializer.Deserialize(ref reader, AhpJsonTypeInfo.Get(options))
+ ?? throw new JsonException("WorkingDirectoryEntry requires a URI or directory"));
+ }
+
+ public override void Write(Utf8JsonWriter writer, WorkingDirectoryEntry value, JsonSerializerOptions options)
+ {
+ if (value.Directory is null) { writer.WriteStringValue(value.Uri); return; }
+ JsonSerializer.Serialize(writer, value.Directory, AhpJsonTypeInfo.Get(options));
+ }
+}`;
+
const TOOL_INPUT_UNION_CS = `///
/// ToolInput is a tool call's raw input, carried either inline as a string or by
/// content reference for inputs large enough to be fetched lazily.
@@ -1329,6 +1365,18 @@ const TERMINAL_LIFECYCLE_STATE_UNION: UnionConfig = {
],
};
+const WORKING_DIRECTORY_ORIGIN_UNION: UnionConfig = {
+ name: 'WorkingDirectoryOrigin',
+ discriminantField: 'kind',
+ doc: 'Host-reported working-directory provenance.',
+ variants: [
+ { variantName: 'Local', innerType: 'LocalWorkingDirectoryOrigin', wireValue: 'local' },
+ { variantName: 'Repo', innerType: 'RepoWorkingDirectoryOrigin', wireValue: 'repo' },
+ { variantName: 'Worktree', innerType: 'WorktreeWorkingDirectoryOrigin', wireValue: 'worktree' },
+ ],
+ unknown: true,
+};
+
const SESSION_ORIGIN_UNION: UnionConfig = {
name: 'SessionOrigin',
discriminantField: 'kind',
@@ -1617,7 +1665,7 @@ function generateStateFile(project: Project): string {
TOOL_RESULT_CONTENT_UNION, MESSAGE_ATTACHMENT_UNION, CUSTOMIZATION_UNION,
CHILD_CUSTOMIZATION_UNION, CUSTOMIZATION_LOAD_STATE_UNION,
MCP_SERVER_STATUS_UNION, TOOL_CALL_CONTRIBUTOR_UNION, SESSION_INPUT_REQUEST_UNION, BACKGROUND_WORK_UNION,
- TERMINAL_LIFECYCLE_STATE_UNION, SESSION_ORIGIN_UNION, AUTOMATION_TRIGGER_UNION,
+ TERMINAL_LIFECYCLE_STATE_UNION, SESSION_ORIGIN_UNION, WORKING_DIRECTORY_ORIGIN_UNION, AUTOMATION_TRIGGER_UNION,
AUTOMATION_DISABLE_CONDITION_UNION,
AUTOMATION_RUN_ORIGIN_UNION, AUTOMATION_RUN_LIFECYCLE_UNION,
]) {
@@ -1628,6 +1676,8 @@ function generateStateFile(project: Project): string {
lines.push('');
lines.push(TOOL_INPUT_UNION_CS);
lines.push('');
+ lines.push(WORKING_DIRECTORY_ENTRY_CS);
+ lines.push('');
lines.push(generateSnapshotState());
lines.push('');
@@ -2316,6 +2366,7 @@ function generateActionsFile(project: Project): string {
const COMMAND_ENUMS = ['ReconnectResultType', 'ChatSourceKind', 'ChatMoveDestinationKind', 'PathStyle', 'ContentEncoding', 'CompletionItemKind', 'ResourceType', 'ResourceWriteMode', 'AuthenticateTargetKind'];
const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: string }[] = [
+ { name: 'RepositorySource' },
{ name: 'CreateTcpConnectionParams' }, { name: 'CreateTcpConnectionResult' },
{ name: 'InitializeParams' }, { name: 'HostEnvironment' }, { name: 'InitializeResult' },
// Implementation identity carried by InitializeParams.clientInfo /
@@ -2323,6 +2374,7 @@ const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; csName?: str
// record or those fields reference a non-existent type (CS0246).
{ name: 'Implementation' },
{ name: 'ClientCapabilities' },
+ { name: 'RepositoryPreparationCapability' }, { name: 'WorkingDirectoryInfoCapability' },
{ name: 'AutomationCapabilities' },
{ name: 'AutomationCreateCapability' },
{ name: 'AutomationScheduleCapabilities' },
@@ -2928,7 +2980,7 @@ function checkExhaustiveness(project: Project): void {
'TerminalContentPart', 'MessageAttachment', 'MessageAttachmentBase',
'Customization', 'ChildCustomization', 'ChildCustomizationType',
'CustomizationLoadState', 'McpServerState', 'ToolCallContributor',
- 'SessionOrigin', 'TerminalLifecycleState', 'AutomationTrigger',
+ 'SessionOrigin', 'WorkingDirectoryOrigin', 'TerminalLifecycleState', 'AutomationTrigger',
'AutomationDisableCondition',
'AutomationRunOrigin', 'AutomationRunLifecycle',
'SessionInputRequest', 'BackgroundWork', 'ToolCallConfirmationState', 'ToolCallRiskAssessment',
diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts
index 7ceb0a331..3e63b1136 100644
--- a/scripts/generate-go.ts
+++ b/scripts/generate-go.ts
@@ -160,6 +160,7 @@ function mapType(tsType: string): string {
if (tsType === 'URI') return 'URI';
if (tsType === 'StringOrMarkdown') return 'StringOrMarkdown';
if (tsType === 'ToolInput') return 'ToolInput';
+ if (tsType === 'URI | WorkingDirectory') return 'WorkingDirectoryEntry';
// ChildCustomizationType is a TS-only subset alias of CustomizationType.
if (tsType === 'ChildCustomizationType') return 'CustomizationType';
@@ -758,7 +759,7 @@ const STATE_ENUMS = [
'BackgroundWorkKind',
'McpServerStatus', 'McpAuthRequiredReason',
'ChangesetStatus', 'ChangesetFileStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType',
- 'SessionOriginKind',
+ 'SessionOriginKind', 'WorkingDirectoryOriginKind',
'AutomationOperation', 'AutomationMisfirePolicy', 'AutomationTriggerKind',
'AutomationDisableConditionKind',
'AutomationRunStatus', 'AutomationRunOriginKind',
@@ -776,6 +777,10 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin
{ name: 'AgentCapabilities' },
{ name: 'MultipleChatsCapability' },
{ name: 'MultipleWorkingDirectoriesCapability' },
+ { name: 'WorkingDirectory' },
+ { name: 'LocalWorkingDirectoryOrigin' },
+ { name: 'RepoWorkingDirectoryOrigin' },
+ { name: 'WorktreeWorkingDirectoryOrigin' },
{ name: 'SessionModelInfo' },
{ name: 'ModelSelection' },
{ name: 'AgentSelection' },
@@ -1180,6 +1185,19 @@ const BACKGROUND_WORK_UNION: UnionConfig = {
unknown: true,
};
+const WORKING_DIRECTORY_ORIGIN_UNION: UnionConfig = {
+ name: 'WorkingDirectoryOrigin',
+ discriminantField: 'kind',
+ doc: 'WorkingDirectoryOrigin is the host-reported working-directory provenance.',
+ variants: [
+ { variantName: 'Local', innerType: 'LocalWorkingDirectoryOrigin', wireValue: 'local' },
+ { variantName: 'Repo', innerType: 'RepoWorkingDirectoryOrigin', wireValue: 'repo' },
+ { variantName: 'Worktree', innerType: 'WorktreeWorkingDirectoryOrigin', wireValue: 'worktree' },
+ ],
+ unknown: true,
+ injectDiscriminantOnMarshal: true,
+};
+
const SESSION_ORIGIN_UNION: UnionConfig = {
name: 'SessionOrigin',
discriminantField: 'kind',
@@ -1482,6 +1500,52 @@ func containsAll(m map[string]json.RawMessage, keys ...string) bool {
}`;
}
+function generateWorkingDirectoryEntry(): string {
+ return `// WorkingDirectoryEntry is a legacy URI or a complete working-directory record.
+type WorkingDirectoryEntry struct {
+\tURI *URI
+\tDirectory *WorkingDirectory
+}
+
+func (d WorkingDirectoryEntry) GetURI() URI {
+\tif d.Directory != nil {
+\t\treturn d.Directory.Uri
+\t}
+\tif d.URI != nil {
+\t\treturn *d.URI
+\t}
+\treturn ""
+}
+
+func (d WorkingDirectoryEntry) MarshalJSON() ([]byte, error) {
+\tif d.Directory != nil {
+\t\treturn json.Marshal(d.Directory)
+\t}
+\tif d.URI != nil {
+\t\treturn json.Marshal(*d.URI)
+\t}
+\treturn nil, fmt.Errorf("WorkingDirectoryEntry requires a URI or directory")
+}
+
+func (d *WorkingDirectoryEntry) UnmarshalJSON(data []byte) error {
+\t*d = WorkingDirectoryEntry{}
+\tvar uri *URI
+\tif err := json.Unmarshal(data, &uri); err == nil && uri != nil {
+\t\td.URI = uri
+\t\treturn nil
+\t}
+\tvar directory *WorkingDirectory
+\tif err := json.Unmarshal(data, &directory); err != nil {
+\t\treturn err
+\t}
+\tif directory == nil {
+\t\treturn fmt.Errorf("WorkingDirectoryEntry requires a URI or directory")
+\t}
+\td.Directory = directory
+\treturn nil
+}`;
+}
+
function generateToolInput(): string {
return `// ToolInput is raw tool input represented inline or by content reference.
type ToolInput struct {
@@ -1516,7 +1580,7 @@ func (t *ToolInput) UnmarshalJSON(data []byte) error {
}
function generateStateFile(project: Project): string {
- const lines: string[] = [HEADER_WITH_IMPORTS];
+ const lines: string[] = [GENERATED_BANNER, '\nimport (\n\t"encoding/json"\n\t"fmt"\n)\n'];
lines.push('// ─── Enums ────────────────────────────────────────────────────────────\n');
for (const enumName of STATE_ENUMS) {
@@ -1549,6 +1613,8 @@ function generateStateFile(project: Project): string {
lines.push(generateToolInput());
lines.push('');
+ lines.push(generateWorkingDirectoryEntry());
+ lines.push('');
lines.push('// ─── Discriminated Unions ─────────────────────────────────────────────\n');
lines.push(generateDiscriminatedUnion(project, RESPONSE_PART_UNION));
@@ -1590,6 +1656,8 @@ function generateStateFile(project: Project): string {
lines.push('');
lines.push(generateDiscriminatedUnion(project, SESSION_ORIGIN_UNION));
lines.push('');
+ lines.push(generateDiscriminatedUnion(project, WORKING_DIRECTORY_ORIGIN_UNION));
+ lines.push('');
lines.push(generateDiscriminatedUnion(project, AUTOMATION_TRIGGER_UNION));
lines.push('');
lines.push(generateDiscriminatedUnion(project, AUTOMATION_DISABLE_CONDITION_UNION));
@@ -1825,8 +1893,10 @@ function generateActionsFile(project: Project): string {
const COMMAND_ENUMS = ['ReconnectResultType', 'ChatSourceKind', 'ChatMoveDestinationKind', 'PathStyle', 'ContentEncoding', 'CompletionItemKind', 'ResourceType', 'ResourceWriteMode', 'AuthenticateTargetKind'];
const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: string }[] = [
+ { name: 'RepositorySource' },
{ name: 'CreateTcpConnectionParams' }, { name: 'CreateTcpConnectionResult' },
{ name: 'InitializeParams' }, { name: 'HostEnvironment' }, { name: 'InitializeResult' },
+ { name: 'RepositoryPreparationCapability' }, { name: 'WorkingDirectoryInfoCapability' },
{ name: 'ClientCapabilities' }, { name: 'AutomationCapabilities' },
{ name: 'AutomationCreateCapability' },
{ name: 'AutomationScheduleCapabilities' },
@@ -1892,6 +1962,7 @@ const RECONNECT_RESULT_UNION: UnionConfig = {
{ variantName: 'Replay', innerType: 'ReconnectReplayResult', wireValue: 'replay' },
{ variantName: 'Snapshot', innerType: 'ReconnectSnapshotResult', wireValue: 'snapshot' },
],
+ injectDiscriminantOnMarshal: true,
};
const CHAT_SOURCE_UNION: UnionConfig = {
@@ -2558,6 +2629,7 @@ function checkExhaustiveness(project: Project): void {
'ToolCallConfirmationState',
'ReconnectResult',
'SessionOrigin',
+ 'WorkingDirectoryOrigin',
'AutomationTrigger',
'AutomationDisableCondition',
'AutomationRunOrigin',
diff --git a/scripts/generate-json-schema.test.ts b/scripts/generate-json-schema.test.ts
index 6cd94baa3..4d67fbcaa 100644
--- a/scripts/generate-json-schema.test.ts
+++ b/scripts/generate-json-schema.test.ts
@@ -398,6 +398,69 @@ describe('generated JSON schemas', () => {
assert.match(expiresIn.description as string, /MUST be a positive integer/);
});
+ it('keeps session config schema generic without repository metadata', () => {
+ const defs = schema.$defs as Record>;
+ const configSchema = defs.SessionConfigSchema;
+ const properties = configSchema.properties as Record>;
+ assert.deepEqual({
+ fields: Object.keys(properties).sort(),
+ required: configSchema.required,
+ propertySchema: properties.properties.additionalProperties,
+ repositoryType: defs.RepositorySessionConfig,
+ }, {
+ fields: ['properties', 'required', 'type'],
+ required: ['type', 'properties'],
+ propertySchema: { $ref: '#/$defs/SessionConfigPropertySchema' },
+ repositoryType: undefined,
+ });
+
+ const legacy = {
+ type: 'object',
+ properties: { mode: { type: 'string', title: 'Mode' } },
+ required: ['mode'],
+ };
+ assert.equal(schemaAccepts(schema, configSchema, legacy), true);
+ });
+
+ it('declares optional repository lists beside generic config', () => {
+ if (file !== 'commands.schema.json') {
+ return;
+ }
+ const defs = schema.$defs as Record>;
+ for (const [definition, channel] of [
+ ['ResolveSessionConfigParams', 'ahp-root://'],
+ ['SessionConfigCompletionsParams', 'ahp-root://'],
+ ['CreateSessionParams', 'ahp-session:/repository-test'],
+ ]) {
+ const properties = defs[definition].properties as Record>;
+ assert.equal(properties.config.type, 'object');
+ assert.deepEqual(
+ Object.keys(properties).filter(name => name.startsWith('repositor')),
+ ['repositories'],
+ );
+ const base = { channel, ...(definition === 'SessionConfigCompletionsParams' ? { property: 'mode' } : {}) };
+ assert.equal(schemaAccepts(schema, defs[definition], base), true);
+ const source = 'https://example.org/team/project.git';
+ for (const repositories of [
+ [],
+ [{ source }],
+ [{ source, revision: 'refs/tags/v1.2.3' }],
+ [{ source, subdirectory: 'packages/api' }],
+ [{ source, revision: 'main' }, { source, revision: 'feature' }],
+ ]) {
+ assert.equal(schemaAccepts(schema, defs[definition], { ...base, repositories, config: { mode: 'review' } }), true);
+ }
+ for (const repositories of [
+ null, {}, source, [null], [source], [{}],
+ [{ revision: 'main' }], [{ source: 42 }], [{ source: null }],
+ [{ source, revision: 42 }],
+ [{ source, subdirectory: 42 }],
+ ]) {
+ assert.equal(schemaAccepts(schema, defs[definition], { ...base, repositories }), false);
+ }
+ }
+ });
+
it('accepts an optional account-selection hint without requiring it for authentication', () => {
if (file !== 'commands.schema.json') {
return;
@@ -436,6 +499,117 @@ describe('generated JSON schemas', () => {
}
});
+ it('declares URI-keyed directory records instead of repository input metadata', () => {
+ const defs = schema.$defs as Record>;
+ for (const name of ['SessionMetadata', 'SessionState', 'SessionSummary']) {
+ const properties = defs[name].properties as Record>;
+ assert.equal(properties.repositories, undefined);
+ assert.equal(properties.workingDirectories.type, 'array');
+ const item = properties.workingDirectories.items as JsonNode;
+ const uri = 'file:///work/project/packages/api';
+ for (const directory of [
+ uri, { uri }, { uri, repo: 'https://example.org/team/project.git' },
+ { uri, origin: { kind: 'local' } },
+ { uri, origin: { kind: 'repo' } },
+ { uri, origin: { kind: 'worktree', mainWorktree: 'file:///work/project' } },
+ ]) {
+ assert.equal(schemaAccepts(schema, item, directory), true);
+ }
+ for (const directory of [
+ 42, null, {}, { repo: 'https://example.org/team/project.git' },
+ { uri: 42 }, { uri, repo: 42 },
+ { uri, origin: { kind: 'worktree' } },
+ { uri, origin: { kind: 'worktree', mainWorktree: 42 } },
+ ]) {
+ assert.equal(schemaAccepts(schema, item, directory), false);
+ }
+ }
+ if (file !== 'commands.schema.json') {
+ return;
+ }
+ const source = defs.RepositorySource;
+ const properties = source.properties as Record>;
+ assert.deepEqual({
+ fields: Object.keys(properties),
+ required: source.required,
+ sourceType: dereferenceSchema(schema, properties.source).type,
+ revisionType: properties.revision.type,
+ subdirectoryType: properties.subdirectory.type,
+ }, {
+ fields: ['source', 'revision', 'subdirectory'], required: ['source'],
+ sourceType: 'string', revisionType: 'string', subdirectoryType: 'string',
+ });
+ });
+
+ it('gates rich directories with a presence-only client capability', () => {
+ if (file !== 'commands.schema.json') {
+ return;
+ }
+ const defs = schema.$defs as Record>;
+ const capabilities = defs.ClientCapabilities;
+ const properties = capabilities.properties as Record>;
+ assert.equal(properties.workingDirectoryInfo.$ref, '#/$defs/WorkingDirectoryInfoCapability');
+ for (const value of [{}, { workingDirectoryInfo: {} }, { mcpApps: {}, workingDirectoryInfo: {} }]) {
+ assert.equal(schemaAccepts(schema, capabilities, value), true);
+ }
+ for (const value of [true, null, []]) {
+ assert.equal(schemaAccepts(schema, capabilities, { workingDirectoryInfo: value }), false);
+ }
+ });
+
+ it('carries complete keyed metadata in set and replace actions', () => {
+ if (file !== 'actions.schema.json') {
+ return;
+ }
+ const defs = schema.$defs as Record>;
+ const directory = { uri: 'file:///work/tree', origin: { kind: 'worktree', mainWorktree: 'file:///work/main' } };
+ for (const [name, action] of [
+ ['SessionWorkingDirectorySetAction', { type: 'session/workingDirectorySet', directory }],
+ ['SessionWorkingDirectoryReplacedAction', { type: 'session/workingDirectoryReplaced', directory: 'file:///work/old', replacement: directory }],
+ ] as const) {
+ assert.equal(schemaAccepts(schema, defs[name], action), true);
+ const properties = defs[name].properties as Record>;
+ assert.equal(properties.origin, undefined);
+ }
+ assert.equal(schemaAccepts(schema, defs.SessionWorkingDirectoryRemovedAction, {
+ type: 'session/workingDirectoryRemoved', directory,
+ }), false);
+ assert.equal(schemaAccepts(schema, defs.SessionWorkingDirectoryReplacedAction, {
+ type: 'session/workingDirectoryReplaced', directory, replacement: 'file:///work/new',
+ }), false);
+ });
+
+ it('advertises repository preparation on the host, not the agent', () => {
+ const defs = schema.$defs as Record>;
+ const capabilities = defs.AgentCapabilities.properties as Record>;
+ assert.deepEqual({
+ agentSource: capabilities.repositorySource,
+ agentPreparation: capabilities.repositoryPreparation,
+ oldCapability: defs.RepositorySourceCapability,
+ }, {
+ agentSource: undefined, agentPreparation: undefined, oldCapability: undefined,
+ });
+ if (file !== 'commands.schema.json') {
+ return;
+ }
+ const properties = defs.InitializeResult.properties as Record>;
+ assert.equal(properties.repositoryPreparation.$ref, '#/$defs/RepositoryPreparationCapability');
+ const hostCapabilities = defs.RepositoryPreparationCapability;
+ assert.equal(hostCapabilities.required, undefined);
+ const base = { protocolVersion: '0.9.0', serverSeq: 0, snapshots: [] };
+ assert.equal(schemaAccepts(schema, defs.InitializeResult, base), true);
+ for (const repositoryPreparation of [
+ {}, { revision: true }, { revision: false },
+ { multipleRepositories: false }, { multipleRepositories: true },
+ { revision: true, multipleRepositories: true },
+ ]) {
+ assert.equal(schemaAccepts(schema, defs.InitializeResult, { ...base, repositoryPreparation }), true);
+ }
+ for (const repositoryPreparation of [true, null, [], { revision: 'true' }, { multipleRepositories: 2 }]) {
+ assert.equal(schemaAccepts(schema, defs.InitializeResult, { ...base, repositoryPreparation }), false);
+ }
+ });
+
it('rejects duplicate disable-condition kinds, including different values of the same kind', () => {
const defs = schema.$defs as Record>;
const afterRuns = { kind: 'afterRuns', max: 3 };
diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts
index 01219a4c7..9c9cd560f 100644
--- a/scripts/generate-kotlin.ts
+++ b/scripts/generate-kotlin.ts
@@ -146,6 +146,7 @@ function mapType(tsType: string): string {
if (tsType === 'URI') return 'String';
if (tsType === 'StringOrMarkdown') return 'StringOrMarkdown';
if (tsType === 'ToolInput') return 'ToolInput';
+ if (tsType === 'URI | WorkingDirectory') return 'WorkingDirectoryEntry';
// ChildCustomizationType is a TS-only subset alias of CustomizationType.
if (tsType === 'ChildCustomizationType') return 'CustomizationType';
@@ -809,6 +810,52 @@ internal object StringOrMarkdownSerializer : KSerializer {
}`;
}
+function generateWorkingDirectoryEntry(): string {
+ return `/** A legacy URI or a complete working-directory record. */
+@Serializable(with = WorkingDirectoryEntrySerializer::class)
+sealed interface WorkingDirectoryEntry {
+ val uri: String
+
+ data class Uri(val value: String) : WorkingDirectoryEntry {
+ override val uri: String get() = value
+ }
+ data class Directory(val value: WorkingDirectory) : WorkingDirectoryEntry {
+ override val uri: String get() = value.uri
+ }
+}
+
+internal object WorkingDirectoryEntrySerializer : KSerializer {
+ override val descriptor: SerialDescriptor =
+ buildClassSerialDescriptor("WorkingDirectoryEntry")
+
+ override fun deserialize(decoder: Decoder): WorkingDirectoryEntry {
+ val input = decoder as? JsonDecoder
+ ?: error("WorkingDirectoryEntry requires JSON")
+ return when (val element = input.decodeJsonElement()) {
+ is JsonPrimitive -> {
+ require(element.isString) { "WorkingDirectoryEntry URI must be a string" }
+ WorkingDirectoryEntry.Uri(element.content)
+ }
+ is JsonObject -> WorkingDirectoryEntry.Directory(
+ input.json.decodeFromJsonElement(WorkingDirectory.serializer(), element),
+ )
+ else -> error("WorkingDirectoryEntry requires a URI or directory")
+ }
+ }
+
+ override fun serialize(encoder: Encoder, value: WorkingDirectoryEntry) {
+ val output = encoder as? JsonEncoder
+ ?: error("WorkingDirectoryEntry requires JSON")
+ val element = when (value) {
+ is WorkingDirectoryEntry.Uri -> JsonPrimitive(value.value)
+ is WorkingDirectoryEntry.Directory ->
+ output.json.encodeToJsonElement(WorkingDirectory.serializer(), value.value)
+ }
+ output.encodeJsonElement(element)
+ }
+}`;
+}
+
function generateToolInput(): string {
return `/**
* Raw tool input represented inline or by content reference.
@@ -1013,7 +1060,7 @@ const STATE_ENUMS = [
'BackgroundWorkKind',
'McpServerStatus', 'McpAuthRequiredReason',
'ChangesetStatus', 'ChangesetFileStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType',
- 'SessionOriginKind',
+ 'SessionOriginKind', 'WorkingDirectoryOriginKind',
'AutomationOperation', 'AutomationMisfirePolicy', 'AutomationTriggerKind',
'AutomationDisableConditionKind',
'AutomationRunStatus', 'AutomationRunOriginKind',
@@ -1027,6 +1074,7 @@ const STATE_STRUCTS = [
'AgentCapabilities',
'MultipleChatsCapability',
'MultipleWorkingDirectoriesCapability',
+ 'WorkingDirectory', 'LocalWorkingDirectoryOrigin', 'RepoWorkingDirectoryOrigin', 'WorktreeWorkingDirectoryOrigin',
'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema',
'PendingMessage', 'ChatState', 'ChatSummary', 'SideChatSelection', 'SessionState', 'SessionActiveClient',
'BackgroundShellWork', 'BackgroundSubagentWork',
@@ -1375,6 +1423,18 @@ const BACKGROUND_WORK_UNION: UnionConfig = {
unknown: true,
};
+const WORKING_DIRECTORY_ORIGIN_UNION: UnionConfig = {
+ name: 'WorkingDirectoryOrigin',
+ discriminantField: 'kind',
+ variants: [
+ { caseName: 'Local', structName: 'LocalWorkingDirectoryOrigin', discriminantValue: 'local' },
+ { caseName: 'Repo', structName: 'RepoWorkingDirectoryOrigin', discriminantValue: 'repo' },
+ { caseName: 'Worktree', structName: 'WorktreeWorkingDirectoryOrigin', discriminantValue: 'worktree' },
+ ],
+ unknown: true,
+ injectDiscriminantOnSerialize: true,
+};
+
const SESSION_ORIGIN_UNION: UnionConfig = {
name: 'SessionOrigin',
discriminantField: 'kind',
@@ -1477,6 +1537,8 @@ function generateStateFile(project: Project): string {
lines.push('');
lines.push(generateToolInput());
lines.push('');
+ lines.push(generateWorkingDirectoryEntry());
+ lines.push('');
lines.push('// ─── Discriminated Unions ───────────────────────────────────────────────────');
lines.push('');
@@ -1519,6 +1581,8 @@ function generateStateFile(project: Project): string {
lines.push('');
lines.push(generateDiscriminatedUnion(project, SESSION_ORIGIN_UNION));
lines.push('');
+ lines.push(generateDiscriminatedUnion(project, WORKING_DIRECTORY_ORIGIN_UNION));
+ lines.push('');
lines.push(generateDiscriminatedUnion(project, AUTOMATION_TRIGGER_UNION));
lines.push('');
lines.push(generateDiscriminatedUnion(project, AUTOMATION_DISABLE_CONDITION_UNION));
@@ -1805,8 +1869,10 @@ function generateActionsFile(project: Project): string {
const COMMAND_ENUMS = ['ReconnectResultType', 'ChatSourceKind', 'ChatMoveDestinationKind', 'PathStyle', 'ContentEncoding', 'CompletionItemKind', 'ResourceType', 'ResourceWriteMode', 'AuthenticateTargetKind'];
const COMMAND_STRUCTS = [
+ 'RepositorySource',
'CreateTcpConnectionParams', 'CreateTcpConnectionResult',
'InitializeParams', 'HostEnvironment', 'InitializeResult',
+ 'RepositoryPreparationCapability', 'WorkingDirectoryInfoCapability',
'ClientCapabilities', 'AutomationCapabilities',
'AutomationCreateCapability',
'AutomationScheduleCapabilities',
@@ -2511,6 +2577,7 @@ function checkExhaustiveness(project: Project): void {
'JsonRpcErrorCode', // type-level alias over JsonRpcErrorCodes const enum
'ReconnectResult', // RECONNECT_RESULT_UNION discriminated union
'SessionOrigin', // SESSION_ORIGIN_UNION discriminated union
+ 'WorkingDirectoryOrigin',
'AutomationTrigger', // AUTOMATION_TRIGGER_UNION discriminated union
'AutomationDisableCondition', // AUTOMATION_DISABLE_CONDITION_UNION discriminated union
'AutomationRunOrigin', // AUTOMATION_RUN_ORIGIN_UNION discriminated union
diff --git a/scripts/generate-markdown.test.ts b/scripts/generate-markdown.test.ts
new file mode 100644
index 000000000..134b0bc4c
--- /dev/null
+++ b/scripts/generate-markdown.test.ts
@@ -0,0 +1,41 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { strict as assert } from 'node:assert';
+import { randomUUID } from 'node:crypto';
+import { readFileSync, rmSync } from 'node:fs';
+import { dirname, join, resolve } from 'node:path';
+import { test } from 'node:test';
+import { fileURLToPath } from 'node:url';
+import { Project } from 'ts-morph';
+import { generateMarkdownDocs } from './generate-markdown.js';
+
+test('command helper types have public reference anchors and fields', () => {
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
+ const output = join(root, 'node_modules', '.cache', `markdown-${randomUUID()}`);
+ try {
+ const project = new Project({ tsConfigFilePath: join(root, 'types', 'tsconfig.json') });
+ generateMarkdownDocs(project, output);
+ const session = readFileSync(join(output, 'session.md'), 'utf8');
+ const common = readFileSync(join(output, 'common.md'), 'utf8');
+ assert.deepEqual({
+ source: session.includes('### `RepositorySource`'),
+ subdirectory: session.includes('| `subdirectory` |'),
+ clientCapabilities: common.includes('### `ClientCapabilities`'),
+ workingDirectoryInfo: common.includes('| `workingDirectoryInfo` |'),
+ preparation: common.includes('### `RepositoryPreparationCapability`'),
+ workingDirectoryInfoCapability: common.includes('### `WorkingDirectoryInfoCapability`'),
+ }, {
+ source: true,
+ subdirectory: true,
+ clientCapabilities: true,
+ workingDirectoryInfo: true,
+ preparation: true,
+ workingDirectoryInfoCapability: true,
+ });
+ } finally {
+ rmSync(output, { recursive: true, force: true });
+ }
+});
diff --git a/scripts/generate-markdown.ts b/scripts/generate-markdown.ts
index f513924cc..e0fa16d05 100644
--- a/scripts/generate-markdown.ts
+++ b/scripts/generate-markdown.ts
@@ -647,6 +647,7 @@ function getActionTypeValue(iface: InterfaceDeclaration): string | undefined {
* table, and a result table looked up from the registry.
*/
function emitCommandsSection(project: Project, sourceFiles: SourceFile[]): string {
+ const supportingTypes = new Set(['RepositorySource', 'RepositoryPreparationCapability', 'ClientCapabilities', 'WorkingDirectoryInfoCapability']);
const commandMap = parseRegistryInterface(project, 'CommandMap', true);
const methodByParams = new Map();
for (const entry of commandMap) methodByParams.set(entry.paramsType, entry);
@@ -663,6 +664,10 @@ function emitCommandsSection(project: Project, sourceFiles: SourceFile[]): strin
for (const stmt of sf.getStatements()) {
if (!Node.isInterfaceDeclaration(stmt) || !stmt.isExported()) continue;
const name = stmt.getName();
+ if (supportingTypes.has(name)) {
+ lines.push(renderInterfaceBlock(stmt));
+ continue;
+ }
const entry = methodByParams.get(name);
// Fallback: any *Params interface with a @method tag we missed.
if (!entry) {
diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts
index e90bd4a15..31f432492 100644
--- a/scripts/generate-rust.ts
+++ b/scripts/generate-rust.ts
@@ -155,6 +155,7 @@ function mapType(tsType: string, propName?: string, containerName?: string): str
if (tsType === 'URI') return 'Uri';
if (tsType === 'StringOrMarkdown') return 'StringOrMarkdown';
if (tsType === 'ToolInput') return 'ToolInput';
+ if (tsType === 'URI | WorkingDirectory') return 'WorkingDirectoryEntry';
// ChildCustomizationType is a TS-only subset alias of CustomizationType.
if (tsType === 'ChildCustomizationType') return 'CustomizationType';
@@ -855,7 +856,7 @@ const STATE_ENUMS = [
'BackgroundWorkKind',
'McpServerStatus', 'McpAuthRequiredReason',
'ChangesetStatus', 'ChangesetFileStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType',
- 'SessionOriginKind',
+ 'SessionOriginKind', 'WorkingDirectoryOriginKind',
'AutomationOperation', 'AutomationMisfirePolicy', 'AutomationTriggerKind',
'AutomationDisableConditionKind',
'AutomationRunStatus', 'AutomationRunOriginKind',
@@ -878,6 +879,10 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str
{ name: 'AgentCapabilities' },
{ name: 'MultipleChatsCapability' },
{ name: 'MultipleWorkingDirectoriesCapability' },
+ { name: 'WorkingDirectory' },
+ { name: 'LocalWorkingDirectoryOrigin', omitDiscriminants: true },
+ { name: 'RepoWorkingDirectoryOrigin', omitDiscriminants: true },
+ { name: 'WorktreeWorkingDirectoryOrigin', omitDiscriminants: true },
{ name: 'SessionModelInfo' },
{ name: 'ModelSelection' },
{ name: 'AgentSelection' },
@@ -1287,6 +1292,18 @@ const BACKGROUND_WORK_UNION: UnionConfig = {
unknown: true,
};
+const WORKING_DIRECTORY_ORIGIN_UNION: UnionConfig = {
+ name: 'WorkingDirectoryOrigin',
+ discriminantField: 'kind',
+ doc: 'Host-reported working-directory provenance.',
+ variants: [
+ { variantName: 'Local', innerType: 'LocalWorkingDirectoryOrigin', wireValue: 'local' },
+ { variantName: 'Repo', innerType: 'RepoWorkingDirectoryOrigin', wireValue: 'repo' },
+ { variantName: 'Worktree', innerType: 'WorktreeWorkingDirectoryOrigin', wireValue: 'worktree' },
+ ],
+ unknown: true,
+};
+
const SESSION_ORIGIN_UNION: UnionConfig = {
name: 'SessionOrigin',
discriminantField: 'kind',
@@ -1462,6 +1479,37 @@ impl<'de> Deserialize<'de> for Snapshot {
}`;
}
+function generateWorkingDirectoryEntry(): string {
+ return `/// A legacy URI or a complete working-directory record.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum WorkingDirectoryEntry {
+ Uri(Uri),
+ Directory(WorkingDirectory),
+}
+
+impl WorkingDirectoryEntry {
+ pub fn uri(&self) -> &Uri {
+ match self {
+ Self::Uri(uri) => uri,
+ Self::Directory(directory) => &directory.uri,
+ }
+ }
+}
+
+impl From for WorkingDirectoryEntry {
+ fn from(uri: Uri) -> Self {
+ Self::Uri(uri)
+ }
+}
+
+impl From for WorkingDirectoryEntry {
+ fn from(directory: WorkingDirectory) -> Self {
+ Self::Directory(directory)
+ }
+}`;
+}
+
function generateToolInput(): string {
return `/// Raw tool input represented inline or by content reference.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -1540,6 +1588,8 @@ where D: serde::Deserializer<'de>,
lines.push(generateToolInput());
lines.push('');
+ lines.push(generateWorkingDirectoryEntry());
+ lines.push('');
lines.push('// ─── Discriminated Unions ─────────────────────────────────────────────\n');
lines.push(generateChatOrigin(project));
@@ -1583,6 +1633,8 @@ where D: serde::Deserializer<'de>,
lines.push('');
lines.push(generateDiscriminatedUnion(project, SESSION_ORIGIN_UNION));
lines.push('');
+ lines.push(generateDiscriminatedUnion(project, WORKING_DIRECTORY_ORIGIN_UNION));
+ lines.push('');
lines.push(generateDiscriminatedUnion(project, AUTOMATION_TRIGGER_UNION));
lines.push('');
lines.push(generateDiscriminatedUnion(project, AUTOMATION_DISABLE_CONDITION_UNION));
@@ -1792,7 +1844,7 @@ impl Serialize for ChatErrorAction {
function generateActionsFile(project: Project): string {
const lines: string[] = [GENERATED_HEADER];
lines.push('#[allow(unused_imports)]');
- lines.push('use crate::state::{AgentInfo, AgentSelection, Annotation, AnnotationEntry, AnnotationOrigin, AutomationDefinition, AutomationDefinitionPatch, AutomationEntry, AutomationRunLifecycle, AutomationRunSummary, BackgroundWork, CanvasReference, CanvasState, ChangesSummary, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ConfirmationOption, ContentRef, Customization, CustomizationEnablement, ErrorInfo, ErrorResponsePart, FileEditCollection, McpAuthRequirement, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, SessionConfigSchema, SessionInputRequest, SideChatSelection, TerminalClaim, TerminalInfo, TextRange, ToolCallContributor, ToolCallResult, ToolCallRiskAssessment, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolInput, ToolResultContent, UsageInfo, Message, PendingMessageKind, Turn, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, Changeset, ChatSummary};');
+ lines.push('use crate::state::{AgentInfo, AgentSelection, Annotation, AnnotationEntry, AnnotationOrigin, AutomationDefinition, AutomationDefinitionPatch, AutomationEntry, AutomationRunLifecycle, AutomationRunSummary, BackgroundWork, CanvasReference, CanvasState, ChangesSummary, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ConfirmationOption, ContentRef, Customization, CustomizationEnablement, ErrorInfo, ErrorResponsePart, FileEditCollection, McpAuthRequirement, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, SessionConfigSchema, SessionInputRequest, SideChatSelection, TerminalClaim, TerminalInfo, TextRange, ToolCallContributor, ToolCallResult, ToolCallRiskAssessment, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolInput, ToolResultContent, UsageInfo, Message, PendingMessageKind, Turn, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, Changeset, ChatSummary, WorkingDirectoryEntry};');
lines.push('');
// ActionType enum
lines.push('// ─── ActionType ──────────────────────────────────────────────────────\n');
@@ -1898,8 +1950,10 @@ pub struct ActionEnvelope {
const COMMAND_ENUMS = ['ReconnectResultType', 'ChatSourceKind', 'ChatMoveDestinationKind', 'PathStyle', 'ContentEncoding', 'CompletionItemKind', 'ResourceType', 'ResourceWriteMode', 'AuthenticateTargetKind'];
const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: string }[] = [
+ { name: 'RepositorySource' },
{ name: 'CreateTcpConnectionParams' }, { name: 'CreateTcpConnectionResult' },
{ name: 'InitializeParams' }, { name: 'HostEnvironment' }, { name: 'InitializeResult' },
+ { name: 'RepositoryPreparationCapability' }, { name: 'WorkingDirectoryInfoCapability' },
{ name: 'ClientCapabilities' }, { name: 'AutomationCapabilities' },
{ name: 'AutomationCreateCapability' },
{ name: 'AutomationScheduleCapabilities' },
@@ -2166,7 +2220,7 @@ const NOTIFICATION_STRUCTS = [
function generateNotificationsFile(project: Project): string {
const lines: string[] = [GENERATED_HEADER];
lines.push('#[allow(unused_imports)]');
- lines.push('use crate::state::{AgentSelection, AnnotationsSummary, ChangesSummary, Changeset, FileEdit, ModelSelection, ProjectInfo, ProtectedResourceMetadata, SessionChatSummary, SessionOrigin, SessionStatus, SessionSummary, Snapshot};');
+ lines.push('use crate::state::{AgentSelection, AnnotationsSummary, ChangesSummary, Changeset, FileEdit, ModelSelection, ProjectInfo, ProtectedResourceMetadata, SessionChatSummary, SessionOrigin, SessionStatus, SessionSummary, Snapshot, WorkingDirectoryEntry};');
lines.push('');
lines.push('// ─── Enums ────────────────────────────────────────────────────────────\n');
@@ -2535,6 +2589,7 @@ function checkExhaustiveness(project: Project): void {
'ToolCallConfirmationState', // TOOL_CALL_CONFIRMATION_STATE_UNION discriminated union
'ReconnectResult',
'SessionOrigin',
+ 'WorkingDirectoryOrigin',
'AutomationTrigger',
'AutomationDisableCondition',
'AutomationRunOrigin',
diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts
index 6209f1774..0211a3e4c 100644
--- a/scripts/generate-swift.ts
+++ b/scripts/generate-swift.ts
@@ -116,6 +116,7 @@ function mapType(tsType: string, propName?: string, containerName?: string): str
if (tsType === 'URI') return 'String';
if (tsType === 'StringOrMarkdown') return 'StringOrMarkdown';
if (tsType === 'ToolInput') return 'ToolInput';
+ if (tsType === 'URI | WorkingDirectory') return 'WorkingDirectoryEntry';
// ChildCustomizationType is a TS-only subset alias of CustomizationType.
if (tsType === 'ChildCustomizationType') return 'CustomizationType';
@@ -736,7 +737,7 @@ const STATE_ENUMS = [
'BackgroundWorkKind',
'McpServerStatus', 'McpAuthRequiredReason',
'ChangesetStatus', 'ChangesetFileStatus', 'ChangesetOperationStatus', 'ChangesetOperationScope', 'ResourceChangeType',
- 'SessionOriginKind',
+ 'SessionOriginKind', 'WorkingDirectoryOriginKind',
'AutomationOperation', 'AutomationMisfirePolicy', 'AutomationTriggerKind',
'AutomationDisableConditionKind',
'AutomationRunStatus', 'AutomationRunOriginKind',
@@ -750,6 +751,7 @@ const STATE_STRUCTS = [
'AgentCapabilities',
'MultipleChatsCapability',
'MultipleWorkingDirectoriesCapability',
+ 'WorkingDirectory', 'LocalWorkingDirectoryOrigin', 'RepoWorkingDirectoryOrigin', 'WorktreeWorkingDirectoryOrigin',
'SessionModelInfo', 'ModelSelection', 'AgentSelection', 'ConfigPropertySchema', 'ConfigSchema',
'PendingMessage', 'ChatState', 'ChatSummary', 'SideChatSelection', 'SessionState', 'SessionActiveClient',
'BackgroundShellWork', 'BackgroundSubagentWork',
@@ -1138,6 +1140,38 @@ public enum StringOrMarkdown: Codable, Sendable, Equatable {
}`;
}
+function generateWorkingDirectoryEntry(): string {
+ return `/// A legacy URI or a complete working-directory record.
+public enum WorkingDirectoryEntry: Codable, Sendable {
+ case uri(String)
+ case directory(WorkingDirectory)
+
+ public var uri: String {
+ switch self {
+ case .uri(let value): return value
+ case .directory(let value): return value.uri
+ }
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.singleValueContainer()
+ if let uri = try? container.decode(String.self) {
+ self = .uri(uri)
+ } else {
+ self = .directory(try container.decode(WorkingDirectory.self))
+ }
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.singleValueContainer()
+ switch self {
+ case .uri(let value): try container.encode(value)
+ case .directory(let value): try container.encode(value)
+ }
+ }
+}`;
+}
+
function generateToolInput(): string {
return `/// Raw tool input represented inline or by content reference.
public enum ToolInput: Codable, Sendable, Equatable {
@@ -1298,6 +1332,18 @@ public enum ChatOrigin: Codable, Sendable, Equatable {
}`;
}
+const WORKING_DIRECTORY_ORIGIN_UNION: UnionConfig = {
+ name: 'WorkingDirectoryOrigin',
+ discriminantField: 'kind',
+ variants: [
+ { caseName: 'local', structName: 'LocalWorkingDirectoryOrigin', discriminantValue: 'local' },
+ { caseName: 'repo', structName: 'RepoWorkingDirectoryOrigin', discriminantValue: 'repo' },
+ { caseName: 'worktree', structName: 'WorktreeWorkingDirectoryOrigin', discriminantValue: 'worktree' },
+ ],
+ unknown: true,
+ injectDiscriminantOnEncode: true,
+};
+
const SESSION_ORIGIN_UNION: UnionConfig = {
name: 'SessionOrigin',
discriminantField: 'kind',
@@ -1388,6 +1434,8 @@ function generateStateFile(project: Project): string {
lines.push('// MARK: - Tool Input\n');
lines.push(generateToolInput());
lines.push('');
+ lines.push(generateWorkingDirectoryEntry());
+ lines.push('');
lines.push('// MARK: - Discriminated Unions\n');
lines.push(generateChatOriginSwift());
@@ -1429,6 +1477,8 @@ function generateStateFile(project: Project): string {
lines.push('');
lines.push(generateDiscriminatedUnion(project, SESSION_ORIGIN_UNION));
lines.push('');
+ lines.push(generateDiscriminatedUnion(project, WORKING_DIRECTORY_ORIGIN_UNION));
+ lines.push('');
lines.push(generateDiscriminatedUnion(project, AUTOMATION_TRIGGER_UNION));
lines.push('');
lines.push(generateDiscriminatedUnion(project, AUTOMATION_DISABLE_CONDITION_UNION));
@@ -1753,8 +1803,10 @@ public func isClientDispatchable(_ action: StateAction) -> Bool {
const COMMAND_ENUMS = ['ReconnectResultType', 'ChatSourceKind', 'ChatMoveDestinationKind', 'PathStyle', 'ContentEncoding', 'CompletionItemKind', 'ResourceType', 'ResourceWriteMode', 'AuthenticateTargetKind'];
const COMMAND_STRUCTS = [
+ 'RepositorySource',
'CreateTcpConnectionParams', 'CreateTcpConnectionResult',
'InitializeParams', 'HostEnvironment', 'InitializeResult', 'PingParams', 'ClientCapabilities', 'AutomationCapabilities',
+ 'RepositoryPreparationCapability', 'WorkingDirectoryInfoCapability',
'AutomationCreateCapability',
'AutomationScheduleCapabilities',
'AutomationRunCancellationCapability',
@@ -2624,6 +2676,7 @@ function checkExhaustiveness(project: Project): void {
'JsonRpcErrorCode', // type-level alias over JsonRpcErrorCodes const enum
'ReconnectResult', // RECONNECT_RESULT_UNION discriminated union
'SessionOrigin', // SESSION_ORIGIN_UNION discriminated union
+ 'WorkingDirectoryOrigin',
'AutomationTrigger', // AUTOMATION_TRIGGER_UNION discriminated union
'AutomationDisableCondition', // AUTOMATION_DISABLE_CONDITION_UNION discriminated union
'AutomationRunOrigin', // AUTOMATION_RUN_ORIGIN_UNION discriminated union
diff --git a/types/channels-chat/actions.ts b/types/channels-chat/actions.ts
index c8f076508..a8098bb7a 100644
--- a/types/channels-chat/actions.ts
+++ b/types/channels-chat/actions.ts
@@ -634,8 +634,9 @@ export interface ChatCanvasesChangedAction {
* Membership semantics keyed by the directory URI: the reducer appends
* `directory` when the chat's subset does not already contain it (creating the
* subset if absent) and is a no-op when it is already present. `directory` MUST
- * be one of the owning session's {@link SessionState.workingDirectories}; a host
- * MUST reject a directory that is not. Only valid when the agent advertises
+ * match a URI string or record's `uri` in the owning session's
+ * {@link SessionState.workingDirectories}; a host MUST reject a directory that
+ * does not. Only valid when the agent advertises
* {@link AgentCapabilities.multipleWorkingDirectories}.
*
* @category Chat Actions
diff --git a/types/channels-chat/commands.ts b/types/channels-chat/commands.ts
index 846b3db57..dda889234 100644
--- a/types/channels-chat/commands.ts
+++ b/types/channels-chat/commands.ts
@@ -108,9 +108,9 @@ export interface CreateChatParams extends BaseParams {
*/
source?: ChatSource;
/**
- * Initial working-directory subset for this chat. Every entry MUST be
- * present in the owning session's `workingDirectories`; the server MUST
- * reject any entry that is not. When absent, the chat inherits the full
+ * Initial working-directory URI subset for this chat. Every URI MUST match
+ * a URI string or record's `uri` in the owning session's `workingDirectories`;
+ * the server MUST reject any entry that does not. When absent, the chat inherits the full
* session set. Forked chats (those whose `source.kind` is `"fork"`) inherit
* the source chat's `workingDirectories`; this field is ignored for forks.
*
diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts
index 40e517a0e..64c7a28b4 100644
--- a/types/channels-chat/state.ts
+++ b/types/channels-chat/state.ts
@@ -96,13 +96,14 @@ export interface ChatState {
/**
* The subset of the session's
* {@link SessionState.workingDirectories | `workingDirectories`} that this
- * chat's agent has tool access to. Every entry MUST be present in the owning
- * session's `workingDirectories`; servers MUST reject a
+ * chat's agent has tool access to. Every URI MUST match a URI string or a
+ * record's `uri` in the owning session's set; servers MUST reject a
* `chat/workingDirectorySet` action that violates this constraint.
*
* When absent, the chat inherits the full session set. When present but empty
* (not recommended), the chat has no working-directory tool access at all.
*
+ * Directory metadata belongs to the session, not the chat.
* Dispatch `chat/workingDirectorySet` / `chat/workingDirectoryRemoved` to
* update the subset on a running chat.
*/
diff --git a/types/channels-root/commands.ts b/types/channels-root/commands.ts
index ff3c706b7..e312664ae 100644
--- a/types/channels-root/commands.ts
+++ b/types/channels-root/commands.ts
@@ -9,6 +9,7 @@
import type { URI } from '../common/state.js';
import type { BaseParams, PaginatedParams, PaginatedResult } from '../common/commands.js';
import type { SessionSummary, SessionConfigSchema } from '../channels-session/state.js';
+import type { RepositorySource } from '../channels-session/commands.js';
// Re-export schema types so the legacy `commands.ts` aggregator continues to
// expose them from the same import path.
@@ -130,6 +131,8 @@ export interface ResolveSessionConfigParams extends BaseParams {
provider?: string;
/** Working directory for the session */
workingDirectory?: URI;
+ /** Repositories used as configuration context; an empty list is equivalent to omission. */
+ repositories?: RepositorySource[];
/** Current user-filled configuration values */
config?: Record;
}
@@ -195,6 +198,8 @@ export interface SessionConfigCompletionsParams extends BaseParams {
provider?: string;
/** Working directory for the session */
workingDirectory?: URI;
+ /** Repositories used as configuration context; an empty list is equivalent to omission. */
+ repositories?: RepositorySource[];
/** Current user-filled configuration values (provides context for the query) */
config?: Record;
/** Property id from the schema to query values for */
diff --git a/types/channels-session/actions.ts b/types/channels-session/actions.ts
index fbe89fa22..edb5621f8 100644
--- a/types/channels-session/actions.ts
+++ b/types/channels-session/actions.ts
@@ -13,6 +13,7 @@ import type {
Customization,
CustomizationEnablement,
McpServerState,
+ WorkingDirectory,
} from './state.js';
import type { URI } from '../common/state.js';
import type { Changeset } from '../channels-changeset/state.js';
@@ -283,13 +284,14 @@ export interface SessionActiveClientRemovedAction {
// ─── Working Directory Actions ───────────────────────────────────────────────
/**
- * A working directory was added to the session's
+ * A working directory was added or updated in the session's
* {@link SessionState.workingDirectories} set.
*
- * Membership semantics keyed by the directory URI: the reducer appends
- * `directory` when the set does not already contain it (creating the set if
- * absent) and is a no-op when it is already present. Only valid when the agent
- * advertises {@link AgentCapabilities.multipleWorkingDirectories}.
+ * Upsert keyed by URI. A rich record replaces the existing entry in place;
+ * a URI-only duplicate preserves known metadata. Only valid when the agent
+ * advertises {@link AgentCapabilities.multipleWorkingDirectories}. Clients
+ * MUST omit `repo` and use only local or absent `origin`; the host validates
+ * and enriches the entry before accepting it.
*
* @category Session Actions
* @version 1
@@ -298,7 +300,7 @@ export interface SessionActiveClientRemovedAction {
export interface SessionWorkingDirectorySetAction {
type: ActionType.SessionWorkingDirectorySet;
/** The working directory to grant the session's agent tool access to. */
- directory: URI;
+ directory: URI | WorkingDirectory;
}
/**
@@ -336,6 +338,8 @@ export interface SessionWorkingDirectoryRemovedAction {
* for example, `[A, B, C]` with `B → C` becomes `[A, C]`. When it occurs
* before the target, it retains its earlier position and the target is removed;
* `[A, B, C]` with `C → A` becomes `[A, B]`.
+ * A rich replacement supplies the complete entry; a URI-only replacement
+ * preserves metadata already known for that URI.
*
* Only valid when the agent advertises
* {@link AgentCapabilities.multipleWorkingDirectories}. Replacing index `0`
@@ -343,6 +347,7 @@ export interface SessionWorkingDirectoryRemovedAction {
* {@link MultipleWorkingDirectoriesCapability.primaryReplacement}; clients
* MUST NOT target an immutable primary. The host MUST validate and apply its
* backend side effect before broadcasting an accepted action, or reject it.
+ * Clients MUST omit `repo` and use only local or absent `origin`.
*
* @category Session Actions
* @version 1
@@ -352,8 +357,8 @@ export interface SessionWorkingDirectoryReplacedAction {
type: ActionType.SessionWorkingDirectoryReplaced;
/** URI of the existing entry to replace. */
directory: URI;
- /** URI to place in the replaced entry's position. */
- replacement: URI;
+ /** Complete entry to place in the replaced entry's position. */
+ replacement: URI | WorkingDirectory;
}
// ─── Input Needed Actions ────────────────────────────────────────────────────
diff --git a/types/channels-session/commands.ts b/types/channels-session/commands.ts
index 5c452067b..4577880e4 100644
--- a/types/channels-session/commands.ts
+++ b/types/channels-session/commands.ts
@@ -16,6 +16,20 @@ import type {
// ─── createSession ───────────────────────────────────────────────────────────
+/**
+ * Requested repository source, not a resolved working directory.
+ *
+ * @category Commands
+ */
+export interface RepositorySource {
+ /** Credential-free repository source URI. */
+ source: URI;
+ /** Requested branch, tag, or commit. Omit to use the host's default revision. */
+ revision?: string;
+ /** Repository-relative selected folder; omit for the root. Hosts reject empty, absolute, or escaping paths. */
+ subdirectory?: string;
+}
+
/**
* Creates a new session with the specified agent provider.
*
@@ -66,9 +80,13 @@ export interface CreateSessionParams extends BaseParams {
* capability treats only the first entry as the session's working directory
* and ignores the rest. Dispatch working-directory actions to change the set
* after the session has started.
- *
*/
workingDirectories?: URI[];
+ /**
+ * Repositories to prepare instead of an explicit `workingDirectories` list.
+ * An empty list is equivalent to omission.
+ */
+ repositories?: RepositorySource[];
/**
* Agent-specific configuration values collected via `resolveSessionConfig`.
* Keys and values correspond to the schema returned by the server.
diff --git a/types/channels-session/reducer.ts b/types/channels-session/reducer.ts
index 8de046dd4..dd79665fc 100644
--- a/types/channels-session/reducer.ts
+++ b/types/channels-session/reducer.ts
@@ -12,7 +12,9 @@ import type {
Customization,
CustomizationEnablement,
McpServerCustomization,
+ WorkingDirectory,
} from './state.js';
+import type { URI } from '../common/state.js';
import {
SessionLifecycle,
SessionStatus,
@@ -25,6 +27,10 @@ import { softAssertNever } from '../common/reducer-helpers.js';
// ─── Helpers ─────────────────────────────────────────────────────────────────
+function workingDirectoryUri(directory: URI | WorkingDirectory): URI {
+ return typeof directory === 'string' ? directory : directory.uri;
+}
+
/** Bitmask covering the mutually-exclusive activity bits (bits 0–4). */
const STATUS_ACTIVITY_MASK = (1 << 5) - 1;
@@ -322,10 +328,17 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?:
case ActionType.SessionWorkingDirectorySet: {
const list = state.workingDirectories ?? [];
- if (list.includes(action.directory)) {
+ const uri = workingDirectoryUri(action.directory);
+ const idx = list.findIndex(directory => workingDirectoryUri(directory) === uri);
+ if (idx < 0) {
+ return { ...state, workingDirectories: [...list, action.directory] };
+ }
+ if (typeof action.directory === 'string') {
return state;
}
- return { ...state, workingDirectories: [...list, action.directory] };
+ const updated = list.slice();
+ updated[idx] = action.directory;
+ return { ...state, workingDirectories: updated };
}
case ActionType.SessionWorkingDirectoryRemoved: {
@@ -333,7 +346,7 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?:
if (!list) {
return state;
}
- const idx = list.indexOf(action.directory);
+ const idx = list.findIndex(directory => workingDirectoryUri(directory) === action.directory);
if (idx < 0) {
return state;
}
@@ -347,23 +360,26 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?:
if (!list) {
return state;
}
- const idx = list.indexOf(action.directory);
+ const idx = list.findIndex(directory => workingDirectoryUri(directory) === action.directory);
if (idx < 0) {
return state;
}
- const replacementIdx = list.indexOf(action.replacement);
+ const uri = workingDirectoryUri(action.replacement);
+ const replacementIdx = list.findIndex(directory => workingDirectoryUri(directory) === uri);
+ const replacement = typeof action.replacement === 'string' && replacementIdx >= 0
+ ? list[replacementIdx]
+ : action.replacement;
+ const updated = list.slice();
if (replacementIdx >= 0 && replacementIdx < idx) {
- return {
- ...state,
- workingDirectories: list.filter((_, index) => index !== idx),
- };
+ updated[replacementIdx] = replacement;
+ updated.splice(idx, 1);
+ } else {
+ updated[idx] = replacement;
+ if (replacementIdx > idx) {
+ updated.splice(replacementIdx, 1);
+ }
}
- return {
- ...state,
- workingDirectories: list
- .map((directory, index) => (index === idx ? action.replacement : directory))
- .filter((directory, index) => index === idx || directory !== action.replacement),
- };
+ return { ...state, workingDirectories: updated };
}
// ── Input Needed ────────────────────────────────────────────────────
diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts
index 8b437edb4..f0ce352f6 100644
--- a/types/channels-session/state.ts
+++ b/types/channels-session/state.ts
@@ -103,6 +103,59 @@ export interface AutomationSessionOrigin {
*/
export type SessionOrigin = AutomationSessionOrigin;
+/**
+ * How a working directory was prepared.
+ *
+ * @category Session State
+ * @nonexhaustive
+ */
+export const enum WorkingDirectoryOriginKind {
+ Local = 'local',
+ Repo = 'repo',
+ Worktree = 'worktree',
+}
+
+/** @category Session State */
+export interface LocalWorkingDirectoryOrigin {
+ kind: WorkingDirectoryOriginKind.Local;
+}
+
+/** @category Session State */
+export interface RepoWorkingDirectoryOrigin {
+ kind: WorkingDirectoryOriginKind.Repo;
+}
+
+/** @category Session State */
+export interface WorktreeWorkingDirectoryOrigin {
+ kind: WorkingDirectoryOriginKind.Worktree;
+ /** Main worktree associated with the host-prepared worktree. */
+ mainWorktree: URI;
+}
+
+/**
+ * Host-reported preparation result, independent of the creation input.
+ *
+ * @category Session State
+ */
+export type WorkingDirectoryOrigin =
+ | LocalWorkingDirectoryOrigin
+ | RepoWorkingDirectoryOrigin
+ | WorktreeWorkingDirectoryOrigin;
+
+/**
+ * An actual working directory, uniquely keyed by `uri` within the session.
+ *
+ * @category Session State
+ */
+export interface WorkingDirectory {
+ /** Actual selected directory, which may be a repository subdirectory. */
+ uri: URI;
+ /** Credential-free repository source association, not a checkout identity. */
+ repo?: URI;
+ /** Host-reported provenance; omission means unspecified. */
+ origin?: WorkingDirectoryOrigin;
+}
+
/**
* Metadata shared between the full {@link SessionState} (delivered when a
* client subscribes to a session's URI) and the lightweight
@@ -140,8 +193,11 @@ export interface SessionMetadata {
* MAY restrict to a subset via
* {@link ChatSummary.workingDirectories | their own `workingDirectories`}; a
* chat that sets none operates against this full set.
+ * Entries are uniquely keyed by URI. Rich records require
+ * {@link ClientCapabilities.workingDirectoryInfo}; other clients receive URIs.
*/
- workingDirectories?: URI[];
+ workingDirectories?: (URI | WorkingDirectory)[];
+
/**
* Lightweight summary of this session's inline annotations channel
* (`ahp-annotations:/`). Surfaced so badge UI can render
diff --git a/types/common/commands.ts b/types/common/commands.ts
index 385d7f888..b447e9440 100644
--- a/types/common/commands.ts
+++ b/types/common/commands.ts
@@ -215,6 +215,11 @@ export interface InitializeParams extends BaseParams {
* @category Commands
*/
export interface ClientCapabilities {
+ /**
+ * Client accepts rich {@link WorkingDirectory} records as well as URI strings.
+ * Hosts project records to URIs when absent and retain this choice on reconnect.
+ */
+ workingDirectoryInfo?: WorkingDirectoryInfoCapability;
/**
* Client can render
* [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) — i.e.
@@ -231,6 +236,13 @@ export interface ClientCapabilities {
mcpApps?: Record;
}
+/**
+ * Presence capability for {@link ClientCapabilities.workingDirectoryInfo}.
+ *
+ * @category Commands
+ */
+export interface WorkingDirectoryInfoCapability {}
+
/**
* Native filesystem path grammar used by a host environment.
*
@@ -314,6 +326,8 @@ export interface InitializeResult {
environment?: HostEnvironment;
/** Suggested default directory for remote filesystem browsing */
defaultDirectory?: URI;
+ /** Host repository preparation support; absent when unsupported. */
+ repositoryPreparation?: RepositoryPreparationCapability;
/**
* Characters that, when typed in a {@link Message} input, SHOULD cause
* the client to issue a `completions` request with
@@ -372,6 +386,19 @@ export interface InitializeResult {
*/
export interface MultipleChatProvidersCapability {}
+/**
+ * Host support for repository preparation at session creation. Presence
+ * alone supports one repository at its default revision.
+ *
+ * @category Commands
+ */
+export interface RepositoryPreparationCapability {
+ /** When true, clients may supply {@link RepositorySource.revision}. */
+ revision?: boolean;
+ /** When true, clients may supply more than one repository. */
+ multipleRepositories?: boolean;
+}
+
/**
* Automation features supported by this host authority.
*
diff --git a/types/test-cases/reducers/272-directory-provenance-survives-ready-and-config.json b/types/test-cases/reducers/272-directory-provenance-survives-ready-and-config.json
new file mode 100644
index 000000000..c3e3017ca
--- /dev/null
+++ b/types/test-cases/reducers/272-directory-provenance-survives-ready-and-config.json
@@ -0,0 +1,48 @@
+{
+ "description": "Resolved directory provenance survives readiness and config changes without echoing creation inputs",
+ "reducer": "session",
+ "initial": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "creating",
+ "activeClients": [],
+ "chats": [],
+ "config": {
+ "schema": { "type": "object", "properties": { "mode": { "type": "string", "title": "Mode" } } },
+ "values": { "mode": "review" }
+ }
+ },
+ "actions": [
+ { "type": "session/workingDirectorySet", "directory": {
+ "uri": "file:///work/project",
+ "repo": "https://example.org/team/project.git",
+ "origin": { "kind": "repo" }
+ } },
+ { "type": "session/ready" },
+ { "type": "session/configChanged", "config": { "mode": "interactive" }, "replace": true },
+ { "type": "session/workingDirectorySet", "directory": {
+ "uri": "file:///work/project-worktree/packages/api",
+ "repo": "https://example.org/team/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///work/project" }
+ } },
+ { "type": "session/workingDirectoryRemoved", "directory": "file:///work/project" }
+ ],
+ "expected": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "ready",
+ "activeClients": [],
+ "chats": [],
+ "workingDirectories": [{
+ "uri": "file:///work/project-worktree/packages/api",
+ "repo": "https://example.org/team/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///work/project" }
+ }],
+ "config": {
+ "schema": { "type": "object", "properties": { "mode": { "type": "string", "title": "Mode" } } },
+ "values": { "mode": "interactive" }
+ }
+ }
+}
diff --git a/types/test-cases/reducers/273-repository-preparation-failure-no-directories.json b/types/test-cases/reducers/273-repository-preparation-failure-no-directories.json
new file mode 100644
index 000000000..1f886eec3
--- /dev/null
+++ b/types/test-cases/reducers/273-repository-preparation-failure-no-directories.json
@@ -0,0 +1,24 @@
+{
+ "description": "Failed preparation does not fabricate directories or echo repository inputs",
+ "reducer": "session",
+ "initial": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "creating",
+ "activeClients": [],
+ "chats": []
+ },
+ "actions": [
+ { "type": "session/creationFailed", "error": { "errorType": "preparationFailed", "message": "Preparation failed" } }
+ ],
+ "expected": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "failed",
+ "creationError": { "errorType": "preparationFailed", "message": "Preparation failed" },
+ "activeClients": [],
+ "chats": []
+ }
+}
diff --git a/types/test-cases/reducers/274-rich-directory-set-inserts.json b/types/test-cases/reducers/274-rich-directory-set-inserts.json
new file mode 100644
index 000000000..254d88c74
--- /dev/null
+++ b/types/test-cases/reducers/274-rich-directory-set-inserts.json
@@ -0,0 +1,23 @@
+{
+ "description": "Rich directory set creates the set and retains complete provenance",
+ "reducer": "session",
+ "initial": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": []
+ },
+ "actions": [{
+ "type": "session/workingDirectorySet",
+ "directory": {
+ "uri": "file:///work/tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///work/main" }
+ }
+ }],
+ "expected": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": [{
+ "uri": "file:///work/tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///work/main" }
+ }]
+ }
+}
diff --git a/types/test-cases/reducers/275-rich-directory-set-upgrades-uri.json b/types/test-cases/reducers/275-rich-directory-set-upgrades-uri.json
new file mode 100644
index 000000000..0ec891224
--- /dev/null
+++ b/types/test-cases/reducers/275-rich-directory-set-upgrades-uri.json
@@ -0,0 +1,22 @@
+{
+ "description": "Rich same-URI set upgrades a legacy entry without changing order",
+ "reducer": "session",
+ "initial": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": ["file:///a", "file:///b", { "uri": "file:///c", "origin": { "kind": "local" } }]
+ },
+ "actions": [{
+ "type": "session/workingDirectorySet",
+ "directory": { "uri": "file:///b", "repo": "https://example.org/project.git", "origin": { "kind": "repo" } }
+ }],
+ "expected": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": [
+ "file:///a",
+ { "uri": "file:///b", "repo": "https://example.org/project.git", "origin": { "kind": "repo" } },
+ { "uri": "file:///c", "origin": { "kind": "local" } }
+ ]
+ }
+}
diff --git a/types/test-cases/reducers/276-rich-directory-set-replaces-complete-record.json b/types/test-cases/reducers/276-rich-directory-set-replaces-complete-record.json
new file mode 100644
index 000000000..26ed8e871
--- /dev/null
+++ b/types/test-cases/reducers/276-rich-directory-set-replaces-complete-record.json
@@ -0,0 +1,18 @@
+{
+ "description": "Rich same-URI set replaces the whole record rather than merging stale metadata",
+ "reducer": "session",
+ "initial": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": [{
+ "uri": "file:///a", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///main" }
+ }, "file:///b"]
+ },
+ "actions": [{ "type": "session/workingDirectorySet", "directory": { "uri": "file:///a" } }],
+ "expected": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": [{ "uri": "file:///a" }, "file:///b"]
+ }
+}
diff --git a/types/test-cases/reducers/277-uri-directory-set-preserves-record.json b/types/test-cases/reducers/277-uri-directory-set-preserves-record.json
new file mode 100644
index 000000000..2c57699cd
--- /dev/null
+++ b/types/test-cases/reducers/277-uri-directory-set-preserves-record.json
@@ -0,0 +1,15 @@
+{
+ "description": "URI-only duplicate set preserves an existing rich record",
+ "reducer": "session",
+ "initial": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": ["file:///a", { "uri": "file:///b", "repo": "https://example.org/project.git", "origin": { "kind": "repo" } }]
+ },
+ "actions": [{ "type": "session/workingDirectorySet", "directory": "file:///b" }],
+ "expected": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": ["file:///a", { "uri": "file:///b", "repo": "https://example.org/project.git", "origin": { "kind": "repo" } }]
+ }
+}
diff --git a/types/test-cases/reducers/278-rich-directory-remove-by-uri.json b/types/test-cases/reducers/278-rich-directory-remove-by-uri.json
new file mode 100644
index 000000000..d8349cfba
--- /dev/null
+++ b/types/test-cases/reducers/278-rich-directory-remove-by-uri.json
@@ -0,0 +1,15 @@
+{
+ "description": "Remove targets a rich entry by URI and preserves remaining order",
+ "reducer": "session",
+ "initial": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": ["file:///a", { "uri": "file:///b", "origin": { "kind": "local" } }, { "uri": "file:///c" }]
+ },
+ "actions": [{ "type": "session/workingDirectoryRemoved", "directory": "file:///b" }],
+ "expected": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": ["file:///a", { "uri": "file:///c" }]
+ }
+}
diff --git a/types/test-cases/reducers/279-rich-directory-replace-same-uri.json b/types/test-cases/reducers/279-rich-directory-replace-same-uri.json
new file mode 100644
index 000000000..bc0f92dea
--- /dev/null
+++ b/types/test-cases/reducers/279-rich-directory-replace-same-uri.json
@@ -0,0 +1,18 @@
+{
+ "description": "Same-URI replacement refreshes the full primary record in place",
+ "reducer": "session",
+ "initial": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": [{ "uri": "file:///a", "origin": { "kind": "local" } }, "file:///b"]
+ },
+ "actions": [{
+ "type": "session/workingDirectoryReplaced", "directory": "file:///a",
+ "replacement": { "uri": "file:///a", "repo": "https://example.org/project.git" }
+ }],
+ "expected": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": [{ "uri": "file:///a", "repo": "https://example.org/project.git" }, "file:///b"]
+ }
+}
diff --git a/types/test-cases/reducers/280-rich-directory-replace-new-uri.json b/types/test-cases/reducers/280-rich-directory-replace-new-uri.json
new file mode 100644
index 000000000..183f682c0
--- /dev/null
+++ b/types/test-cases/reducers/280-rich-directory-replace-new-uri.json
@@ -0,0 +1,24 @@
+{
+ "description": "Replacement changes the selected primary URI and provenance atomically",
+ "reducer": "session",
+ "initial": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": [{ "uri": "file:///a", "origin": { "kind": "local" } }, "file:///b"]
+ },
+ "actions": [{
+ "type": "session/workingDirectoryReplaced", "directory": "file:///a",
+ "replacement": {
+ "uri": "file:///tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///a" }
+ }
+ }],
+ "expected": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": [{
+ "uri": "file:///tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///a" }
+ }, "file:///b"]
+ }
+}
diff --git a/types/test-cases/reducers/281-rich-directory-replace-deduplicates-later.json b/types/test-cases/reducers/281-rich-directory-replace-deduplicates-later.json
new file mode 100644
index 000000000..a2489c6eb
--- /dev/null
+++ b/types/test-cases/reducers/281-rich-directory-replace-deduplicates-later.json
@@ -0,0 +1,18 @@
+{
+ "description": "A rich replacement refreshes a later same-URI entry and moves it to the target position",
+ "reducer": "session",
+ "initial": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": ["file:///a", { "uri": "file:///b", "origin": { "kind": "local" } }, { "uri": "file:///c", "repo": "https://example.org/old.git" }]
+ },
+ "actions": [{
+ "type": "session/workingDirectoryReplaced", "directory": "file:///b",
+ "replacement": { "uri": "file:///c", "origin": { "kind": "local" } }
+ }],
+ "expected": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": ["file:///a", { "uri": "file:///c", "origin": { "kind": "local" } }]
+ }
+}
diff --git a/types/test-cases/reducers/282-rich-directory-replace-deduplicates-earlier.json b/types/test-cases/reducers/282-rich-directory-replace-deduplicates-earlier.json
new file mode 100644
index 000000000..d95642cbf
--- /dev/null
+++ b/types/test-cases/reducers/282-rich-directory-replace-deduplicates-earlier.json
@@ -0,0 +1,18 @@
+{
+ "description": "A rich replacement refreshes an earlier same-URI record without moving its primary slot",
+ "reducer": "session",
+ "initial": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": [{ "uri": "file:///a", "origin": { "kind": "local" } }, "file:///b", "file:///c"]
+ },
+ "actions": [{
+ "type": "session/workingDirectoryReplaced", "directory": "file:///c",
+ "replacement": { "uri": "file:///a", "repo": "https://example.org/project.git", "origin": { "kind": "repo" } }
+ }],
+ "expected": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": [{ "uri": "file:///a", "repo": "https://example.org/project.git", "origin": { "kind": "repo" } }, "file:///b"]
+ }
+}
diff --git a/types/test-cases/reducers/283-uri-directory-replace-preserves-later-record.json b/types/test-cases/reducers/283-uri-directory-replace-preserves-later-record.json
new file mode 100644
index 000000000..e811a4db6
--- /dev/null
+++ b/types/test-cases/reducers/283-uri-directory-replace-preserves-later-record.json
@@ -0,0 +1,15 @@
+{
+ "description": "URI-only replacement moves a later rich record without dropping metadata",
+ "reducer": "session",
+ "initial": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": ["file:///a", "file:///b", { "uri": "file:///c", "origin": { "kind": "worktree", "mainWorktree": "file:///main" } }]
+ },
+ "actions": [{ "type": "session/workingDirectoryReplaced", "directory": "file:///b", "replacement": "file:///c" }],
+ "expected": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": ["file:///a", { "uri": "file:///c", "origin": { "kind": "worktree", "mainWorktree": "file:///main" } }]
+ }
+}
diff --git a/types/test-cases/reducers/284-uri-directory-replace-preserves-earlier-record.json b/types/test-cases/reducers/284-uri-directory-replace-preserves-earlier-record.json
new file mode 100644
index 000000000..49a457135
--- /dev/null
+++ b/types/test-cases/reducers/284-uri-directory-replace-preserves-earlier-record.json
@@ -0,0 +1,15 @@
+{
+ "description": "URI-only replacement retains an earlier rich record and removes only the target",
+ "reducer": "session",
+ "initial": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": [{ "uri": "file:///a", "repo": "https://example.org/project.git" }, "file:///b", { "uri": "file:///c" }]
+ },
+ "actions": [{ "type": "session/workingDirectoryReplaced", "directory": "file:///c", "replacement": "file:///a" }],
+ "expected": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": [{ "uri": "file:///a", "repo": "https://example.org/project.git" }, "file:///b"]
+ }
+}
diff --git a/types/test-cases/reducers/285-uri-directory-replace-preserves-same-record.json b/types/test-cases/reducers/285-uri-directory-replace-preserves-same-record.json
new file mode 100644
index 000000000..295ed5f86
--- /dev/null
+++ b/types/test-cases/reducers/285-uri-directory-replace-preserves-same-record.json
@@ -0,0 +1,15 @@
+{
+ "description": "Same-URI legacy replacement retains known metadata",
+ "reducer": "session",
+ "initial": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": [{ "uri": "file:///a", "origin": { "kind": "local" } }, "file:///b"]
+ },
+ "actions": [{ "type": "session/workingDirectoryReplaced", "directory": "file:///a", "replacement": "file:///a" }],
+ "expected": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": [{ "uri": "file:///a", "origin": { "kind": "local" } }, "file:///b"]
+ }
+}
diff --git a/types/test-cases/reducers/286-rich-directory-replace-missing-target.json b/types/test-cases/reducers/286-rich-directory-replace-missing-target.json
new file mode 100644
index 000000000..5572f3033
--- /dev/null
+++ b/types/test-cases/reducers/286-rich-directory-replace-missing-target.json
@@ -0,0 +1,18 @@
+{
+ "description": "A rich replacement for a missing target leaves the mixed set unchanged",
+ "reducer": "session",
+ "initial": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": ["file:///a", { "uri": "file:///b", "origin": { "kind": "local" } }]
+ },
+ "actions": [{
+ "type": "session/workingDirectoryReplaced", "directory": "file:///missing",
+ "replacement": { "uri": "file:///b", "repo": "https://example.org/project.git" }
+ }],
+ "expected": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": ["file:///a", { "uri": "file:///b", "origin": { "kind": "local" } }]
+ }
+}
diff --git a/types/test-cases/round-trips/045-session-config-without-repository.json b/types/test-cases/round-trips/045-session-config-without-repository.json
new file mode 100644
index 000000000..1b930eb09
--- /dev/null
+++ b/types/test-cases/round-trips/045-session-config-without-repository.json
@@ -0,0 +1,50 @@
+{
+ "name": "session-config-without-repository",
+ "group": "A",
+ "description": "An existing directory-backed session config remains valid without repository properties or values.",
+ "type": "Snapshot",
+ "input": {
+ "resource": "ahp-session:/directory-session",
+ "state": {
+ "provider": "example",
+ "title": "Directory session",
+ "status": 1,
+ "lifecycle": "ready",
+ "activeClients": [],
+ "chats": [],
+ "workingDirectories": ["file:///work/existing"],
+ "config": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "mode": { "type": "string", "title": "Mode", "sessionMutable": true }
+ }
+ },
+ "values": { "mode": "review" }
+ }
+ },
+ "fromSeq": 1
+ },
+ "acceptableOutputs": [{
+ "resource": "ahp-session:/directory-session",
+ "state": {
+ "provider": "example",
+ "title": "Directory session",
+ "status": 1,
+ "lifecycle": "ready",
+ "activeClients": [],
+ "chats": [],
+ "workingDirectories": ["file:///work/existing"],
+ "config": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "mode": { "type": "string", "title": "Mode", "sessionMutable": true }
+ }
+ },
+ "values": { "mode": "review" }
+ }
+ },
+ "fromSeq": 1
+ }]
+}
diff --git a/types/test-cases/round-trips/046-session-directory-provenance.json b/types/test-cases/round-trips/046-session-directory-provenance.json
new file mode 100644
index 000000000..d0ffc3741
--- /dev/null
+++ b/types/test-cases/round-trips/046-session-directory-provenance.json
@@ -0,0 +1,38 @@
+{
+ "name": "session-directory-provenance",
+ "group": "A",
+ "description": "A ready session preserves multiple actual directories associated with the same source and different preparation results.",
+ "type": "Snapshot",
+ "input": {
+ "resource": "ahp-session:/repository-session",
+ "state": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "ready",
+ "activeClients": [],
+ "chats": [],
+ "workingDirectories": [
+ { "uri": "file:///work/project", "repo": "https://example.org/team/project.git", "origin": { "kind": "repo" } },
+ { "uri": "file:///work/project-worktree/packages/api", "repo": "https://example.org/team/project.git", "origin": { "kind": "worktree", "mainWorktree": "file:///work/project" } }
+ ]
+ },
+ "fromSeq": 2
+ },
+ "acceptableOutputs": [{
+ "resource": "ahp-session:/repository-session",
+ "state": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "ready",
+ "activeClients": [],
+ "chats": [],
+ "workingDirectories": [
+ { "uri": "file:///work/project", "repo": "https://example.org/team/project.git", "origin": { "kind": "repo" } },
+ { "uri": "file:///work/project-worktree/packages/api", "repo": "https://example.org/team/project.git", "origin": { "kind": "worktree", "mainWorktree": "file:///work/project" } }
+ ]
+ },
+ "fromSeq": 2
+ }]
+}
diff --git a/types/test-cases/round-trips/047-repository-session-creating.json b/types/test-cases/round-trips/047-repository-session-creating.json
new file mode 100644
index 000000000..75c8ec967
--- /dev/null
+++ b/types/test-cases/round-trips/047-repository-session-creating.json
@@ -0,0 +1,30 @@
+{
+ "name": "repository-session-creating",
+ "group": "A",
+ "description": "A creating session does not claim unresolved directories or echo the original repository request.",
+ "type": "Snapshot",
+ "input": {
+ "resource": "ahp-session:/repository-session",
+ "state": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "creating",
+ "activeClients": [],
+ "chats": []
+ },
+ "fromSeq": 0
+ },
+ "acceptableOutputs": [{
+ "resource": "ahp-session:/repository-session",
+ "state": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "creating",
+ "activeClients": [],
+ "chats": []
+ },
+ "fromSeq": 0
+ }]
+}
diff --git a/types/test-cases/round-trips/048-repository-session-failed.json b/types/test-cases/round-trips/048-repository-session-failed.json
new file mode 100644
index 000000000..af63994c0
--- /dev/null
+++ b/types/test-cases/round-trips/048-repository-session-failed.json
@@ -0,0 +1,32 @@
+{
+ "name": "repository-session-failed",
+ "group": "A",
+ "description": "A failed session carries its creation error without claiming a directory was resolved.",
+ "type": "Snapshot",
+ "input": {
+ "resource": "ahp-session:/repository-session",
+ "state": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "failed",
+ "creationError": { "errorType": "preparationFailed", "message": "Preparation failed" },
+ "activeClients": [],
+ "chats": []
+ },
+ "fromSeq": 1
+ },
+ "acceptableOutputs": [{
+ "resource": "ahp-session:/repository-session",
+ "state": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "failed",
+ "creationError": { "errorType": "preparationFailed", "message": "Preparation failed" },
+ "activeClients": [],
+ "chats": []
+ },
+ "fromSeq": 1
+ }]
+}
diff --git a/types/test-cases/round-trips/049-repository-preparation-capability.json b/types/test-cases/round-trips/049-repository-preparation-capability.json
new file mode 100644
index 000000000..235182054
--- /dev/null
+++ b/types/test-cases/round-trips/049-repository-preparation-capability.json
@@ -0,0 +1,18 @@
+{
+ "name": "repository-preparation-capability",
+ "group": "A",
+ "description": "An empty host capability supports one default-revision repository without an agent capability.",
+ "type": "InitializeResult",
+ "input": {
+ "protocolVersion": "0.9.0",
+ "serverSeq": 0,
+ "snapshots": [],
+ "repositoryPreparation": {}
+ },
+ "acceptableOutputs": [{
+ "protocolVersion": "0.9.0",
+ "serverSeq": 0,
+ "snapshots": [],
+ "repositoryPreparation": {}
+ }]
+}
diff --git a/types/test-cases/round-trips/050-repository-preparation-revisions.json b/types/test-cases/round-trips/050-repository-preparation-revisions.json
new file mode 100644
index 000000000..a17335aad
--- /dev/null
+++ b/types/test-cases/round-trips/050-repository-preparation-revisions.json
@@ -0,0 +1,18 @@
+{
+ "name": "repository-preparation-revisions",
+ "group": "A",
+ "description": "Host revision support does not imply multiple-repository support.",
+ "type": "InitializeResult",
+ "input": {
+ "protocolVersion": "0.9.0",
+ "serverSeq": 0,
+ "snapshots": [],
+ "repositoryPreparation": { "revision": true }
+ },
+ "acceptableOutputs": [{
+ "protocolVersion": "0.9.0",
+ "serverSeq": 0,
+ "snapshots": [],
+ "repositoryPreparation": { "revision": true }
+ }]
+}
diff --git a/types/test-cases/round-trips/051-repository-preparation-single.json b/types/test-cases/round-trips/051-repository-preparation-single.json
new file mode 100644
index 000000000..89b785dbf
--- /dev/null
+++ b/types/test-cases/round-trips/051-repository-preparation-single.json
@@ -0,0 +1,18 @@
+{
+ "name": "repository-preparation-single",
+ "group": "A",
+ "description": "Explicit false capability options round-trip without being confused with absence of host support.",
+ "type": "InitializeResult",
+ "input": {
+ "protocolVersion": "0.9.0",
+ "serverSeq": 0,
+ "snapshots": [],
+ "repositoryPreparation": { "revision": false, "multipleRepositories": false }
+ },
+ "acceptableOutputs": [{
+ "protocolVersion": "0.9.0",
+ "serverSeq": 0,
+ "snapshots": [],
+ "repositoryPreparation": { "revision": false, "multipleRepositories": false }
+ }]
+}
diff --git a/types/test-cases/round-trips/052-repository-preparation-multiple.json b/types/test-cases/round-trips/052-repository-preparation-multiple.json
new file mode 100644
index 000000000..226d1faf8
--- /dev/null
+++ b/types/test-cases/round-trips/052-repository-preparation-multiple.json
@@ -0,0 +1,18 @@
+{
+ "name": "repository-preparation-multiple",
+ "group": "A",
+ "description": "Future hosts can advertise multiple repositories and explicit revisions independently of agent capabilities.",
+ "type": "InitializeResult",
+ "input": {
+ "protocolVersion": "0.9.0",
+ "serverSeq": 0,
+ "snapshots": [],
+ "repositoryPreparation": { "revision": true, "multipleRepositories": true }
+ },
+ "acceptableOutputs": [{
+ "protocolVersion": "0.9.0",
+ "serverSeq": 0,
+ "snapshots": [],
+ "repositoryPreparation": { "revision": true, "multipleRepositories": true }
+ }]
+}
diff --git a/types/test-cases/round-trips/053-repository-session-request-default-revision.json b/types/test-cases/round-trips/053-repository-session-request-default-revision.json
new file mode 100644
index 000000000..eef807c9f
--- /dev/null
+++ b/types/test-cases/round-trips/053-repository-session-request-default-revision.json
@@ -0,0 +1,16 @@
+{
+ "name": "repository-session-request-default-revision",
+ "group": "A",
+ "description": "Creation uses a typed repository list and preserves omission of the requested revision.",
+ "type": "CreateSessionParams",
+ "input": {
+ "channel": "ahp-session:/repository-session",
+ "repositories": [{ "source": "https://example.org/team/project.git" }],
+ "config": { "mode": "review" }
+ },
+ "acceptableOutputs": [{
+ "channel": "ahp-session:/repository-session",
+ "repositories": [{ "source": "https://example.org/team/project.git" }],
+ "config": { "mode": "review" }
+ }]
+}
diff --git a/types/test-cases/round-trips/054-repository-session-request-multiple.json b/types/test-cases/round-trips/054-repository-session-request-multiple.json
new file mode 100644
index 000000000..269597ded
--- /dev/null
+++ b/types/test-cases/round-trips/054-repository-session-request-multiple.json
@@ -0,0 +1,20 @@
+{
+ "name": "repository-session-request-multiple",
+ "group": "A",
+ "description": "A future multi-repository request preserves two revisions of the same source in their requested order.",
+ "type": "CreateSessionParams",
+ "input": {
+ "channel": "ahp-session:/repository-session",
+ "repositories": [
+ { "source": "https://example.org/team/project.git", "revision": "feature", "subdirectory": "packages/api" },
+ { "source": "https://example.org/team/project.git", "revision": "main" }
+ ]
+ },
+ "acceptableOutputs": [{
+ "channel": "ahp-session:/repository-session",
+ "repositories": [
+ { "source": "https://example.org/team/project.git", "revision": "feature", "subdirectory": "packages/api" },
+ { "source": "https://example.org/team/project.git", "revision": "main" }
+ ]
+ }]
+}
diff --git a/types/test-cases/round-trips/055-repository-config-query-context.json b/types/test-cases/round-trips/055-repository-config-query-context.json
new file mode 100644
index 000000000..7340226d5
--- /dev/null
+++ b/types/test-cases/round-trips/055-repository-config-query-context.json
@@ -0,0 +1,18 @@
+{
+ "name": "repository-config-query-context",
+ "group": "A",
+ "description": "Repository lists and directory context coexist in configuration queries, outside generic config.",
+ "type": "ResolveSessionConfigParams",
+ "input": {
+ "channel": "ahp-root://",
+ "workingDirectory": "file:///work/context",
+ "repositories": [{ "source": "https://example.org/team/project.git", "revision": "main", "subdirectory": "packages/api" }],
+ "config": { "mode": "review" }
+ },
+ "acceptableOutputs": [{
+ "channel": "ahp-root://",
+ "workingDirectory": "file:///work/context",
+ "repositories": [{ "source": "https://example.org/team/project.git", "revision": "main", "subdirectory": "packages/api" }],
+ "config": { "mode": "review" }
+ }]
+}
diff --git a/types/test-cases/round-trips/056-repository-config-completions-context.json b/types/test-cases/round-trips/056-repository-config-completions-context.json
new file mode 100644
index 000000000..4e8708d99
--- /dev/null
+++ b/types/test-cases/round-trips/056-repository-config-completions-context.json
@@ -0,0 +1,22 @@
+{
+ "name": "repository-config-completions-context",
+ "group": "A",
+ "description": "Configuration completions preserve the repository list and optional existing directory context.",
+ "type": "SessionConfigCompletionsParams",
+ "input": {
+ "channel": "ahp-root://",
+ "workingDirectory": "file:///work/context",
+ "repositories": [{ "source": "https://example.org/team/project.git", "subdirectory": "packages/api" }],
+ "config": { "mode": "review" },
+ "property": "mode",
+ "query": "re"
+ },
+ "acceptableOutputs": [{
+ "channel": "ahp-root://",
+ "workingDirectory": "file:///work/context",
+ "repositories": [{ "source": "https://example.org/team/project.git", "subdirectory": "packages/api" }],
+ "config": { "mode": "review" },
+ "property": "mode",
+ "query": "re"
+ }]
+}
diff --git a/types/test-cases/round-trips/057-repository-session-summary.json b/types/test-cases/round-trips/057-repository-session-summary.json
new file mode 100644
index 000000000..3deab247d
--- /dev/null
+++ b/types/test-cases/round-trips/057-repository-session-summary.json
@@ -0,0 +1,34 @@
+{
+ "name": "repository-session-summary",
+ "group": "A",
+ "description": "Session summaries carry URI-only and rich directories with local, absent, and repository provenance.",
+ "type": "SessionSummary",
+ "input": {
+ "resource": "ahp-session:/repository-session",
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "createdAt": "2026-09-20T12:00:00.000Z",
+ "modifiedAt": "2026-09-20T12:00:00.000Z",
+ "workingDirectories": [
+ "file:///work/legacy",
+ { "uri": "file:///work/local", "origin": { "kind": "local" } },
+ { "uri": "file:///work/unspecified" },
+ { "uri": "file:///work/project", "repo": "https://example.org/team/project.git", "origin": { "kind": "repo" } }
+ ]
+ },
+ "acceptableOutputs": [{
+ "resource": "ahp-session:/repository-session",
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "createdAt": "2026-09-20T12:00:00.000Z",
+ "modifiedAt": "2026-09-20T12:00:00.000Z",
+ "workingDirectories": [
+ "file:///work/legacy",
+ { "uri": "file:///work/local", "origin": { "kind": "local" } },
+ { "uri": "file:///work/unspecified" },
+ { "uri": "file:///work/project", "repo": "https://example.org/team/project.git", "origin": { "kind": "repo" } }
+ ]
+ }]
+}
diff --git a/types/test-cases/round-trips/058-repository-session-multiple-ready.json b/types/test-cases/round-trips/058-repository-session-multiple-ready.json
new file mode 100644
index 000000000..94ab009dc
--- /dev/null
+++ b/types/test-cases/round-trips/058-repository-session-multiple-ready.json
@@ -0,0 +1,38 @@
+{
+ "name": "repository-session-multiple-ready",
+ "group": "A",
+ "description": "Ready state can associate different directories with the same source without deduplication.",
+ "type": "Snapshot",
+ "input": {
+ "resource": "ahp-session:/repository-session",
+ "state": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "ready",
+ "activeClients": [],
+ "chats": [],
+ "workingDirectories": [
+ { "uri": "file:///work/session-feature", "repo": "https://example.org/team/project.git", "origin": { "kind": "repo" } },
+ { "uri": "file:///work/session-main", "repo": "https://example.org/team/project.git", "origin": { "kind": "repo" } }
+ ]
+ },
+ "fromSeq": 2
+ },
+ "acceptableOutputs": [{
+ "resource": "ahp-session:/repository-session",
+ "state": {
+ "provider": "example",
+ "title": "Repository session",
+ "status": 1,
+ "lifecycle": "ready",
+ "activeClients": [],
+ "chats": [],
+ "workingDirectories": [
+ { "uri": "file:///work/session-feature", "repo": "https://example.org/team/project.git", "origin": { "kind": "repo" } },
+ { "uri": "file:///work/session-main", "repo": "https://example.org/team/project.git", "origin": { "kind": "repo" } }
+ ]
+ },
+ "fromSeq": 2
+ }]
+}
diff --git a/types/test-cases/round-trips/059-initialize-working-directory-info.json b/types/test-cases/round-trips/059-initialize-working-directory-info.json
new file mode 100644
index 000000000..a0e5c9a14
--- /dev/null
+++ b/types/test-cases/round-trips/059-initialize-working-directory-info.json
@@ -0,0 +1,14 @@
+{
+ "name": "initialize-working-directory-info",
+ "group": "A",
+ "description": "A client opts into rich directories with a presence-only capability.",
+ "type": "InitializeParams",
+ "input": {
+ "channel": "ahp-root://", "clientId": "rich-client", "protocolVersions": ["0.9.0"],
+ "capabilities": { "workingDirectoryInfo": {} }
+ },
+ "acceptableOutputs": [{
+ "channel": "ahp-root://", "clientId": "rich-client", "protocolVersions": ["0.9.0"],
+ "capabilities": { "workingDirectoryInfo": {} }
+ }]
+}
diff --git a/types/test-cases/round-trips/060-initialize-rich-directories.json b/types/test-cases/round-trips/060-initialize-rich-directories.json
new file mode 100644
index 000000000..f7cf61cc4
--- /dev/null
+++ b/types/test-cases/round-trips/060-initialize-rich-directories.json
@@ -0,0 +1,36 @@
+{
+ "name": "initialize-rich-directories",
+ "group": "A",
+ "description": "Initialize snapshots accept mixed URI-only and rich directory entries.",
+ "type": "InitializeResult",
+ "input": {
+ "protocolVersion": "0.9.0", "serverSeq": 2,
+ "snapshots": [{
+ "resource": "ahp-session:/directories", "fromSeq": 2,
+ "state": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": [
+ "file:///legacy",
+ { "uri": "file:///tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///main" } }
+ ]
+ }
+ }]
+ },
+ "acceptableOutputs": [{
+ "protocolVersion": "0.9.0", "serverSeq": 2,
+ "snapshots": [{
+ "resource": "ahp-session:/directories", "fromSeq": 2,
+ "state": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": [
+ "file:///legacy",
+ { "uri": "file:///tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///main" } }
+ ]
+ }
+ }]
+ }]
+}
diff --git a/types/test-cases/round-trips/061-subscribe-rich-directories.json b/types/test-cases/round-trips/061-subscribe-rich-directories.json
new file mode 100644
index 000000000..1ac5cddbd
--- /dev/null
+++ b/types/test-cases/round-trips/061-subscribe-rich-directories.json
@@ -0,0 +1,32 @@
+{
+ "name": "subscribe-rich-directories",
+ "group": "A",
+ "description": "Subscribe carries resolved directory metadata in its full session snapshot.",
+ "type": "SubscribeResult",
+ "input": {
+ "snapshot": {
+ "resource": "ahp-session:/directories", "fromSeq": 2,
+ "state": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": ["file:///legacy", {
+ "uri": "file:///tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///main" }
+ }]
+ }
+ }
+ },
+ "acceptableOutputs": [{
+ "snapshot": {
+ "resource": "ahp-session:/directories", "fromSeq": 2,
+ "state": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": ["file:///legacy", {
+ "uri": "file:///tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///main" }
+ }]
+ }
+ }
+ }]
+}
diff --git a/types/test-cases/round-trips/062-list-sessions-rich-directories.json b/types/test-cases/round-trips/062-list-sessions-rich-directories.json
new file mode 100644
index 000000000..6d1eaedbf
--- /dev/null
+++ b/types/test-cases/round-trips/062-list-sessions-rich-directories.json
@@ -0,0 +1,22 @@
+{
+ "name": "list-sessions-rich-directories",
+ "group": "A",
+ "description": "Session list summaries preserve mixed directories and their source associations.",
+ "type": "ListSessionsResult",
+ "input": { "items": [{
+ "resource": "ahp-session:/directories", "provider": "example", "title": "Directories", "status": 1,
+ "createdAt": "2026-09-21T00:00:00.000Z", "modifiedAt": "2026-09-21T00:00:00.000Z",
+ "workingDirectories": ["file:///legacy", {
+ "uri": "file:///tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///main" }
+ }]
+ }] },
+ "acceptableOutputs": [{ "items": [{
+ "resource": "ahp-session:/directories", "provider": "example", "title": "Directories", "status": 1,
+ "createdAt": "2026-09-21T00:00:00.000Z", "modifiedAt": "2026-09-21T00:00:00.000Z",
+ "workingDirectories": ["file:///legacy", {
+ "uri": "file:///tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///main" }
+ }]
+ }] }]
+}
diff --git a/types/test-cases/round-trips/063-session-added-rich-directories.json b/types/test-cases/round-trips/063-session-added-rich-directories.json
new file mode 100644
index 000000000..58067db23
--- /dev/null
+++ b/types/test-cases/round-trips/063-session-added-rich-directories.json
@@ -0,0 +1,28 @@
+{
+ "name": "session-added-rich-directories",
+ "group": "A",
+ "description": "Session-added notifications preserve rich directory metadata.",
+ "type": "SessionAddedParams",
+ "input": {
+ "channel": "ahp-root://",
+ "summary": {
+ "resource": "ahp-session:/directories", "provider": "example", "title": "Directories", "status": 1,
+ "createdAt": "2026-09-21T00:00:00.000Z", "modifiedAt": "2026-09-21T00:00:00.000Z",
+ "workingDirectories": ["file:///legacy", {
+ "uri": "file:///tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///main" }
+ }]
+ }
+ },
+ "acceptableOutputs": [{
+ "channel": "ahp-root://",
+ "summary": {
+ "resource": "ahp-session:/directories", "provider": "example", "title": "Directories", "status": 1,
+ "createdAt": "2026-09-21T00:00:00.000Z", "modifiedAt": "2026-09-21T00:00:00.000Z",
+ "workingDirectories": ["file:///legacy", {
+ "uri": "file:///tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///main" }
+ }]
+ }
+ }]
+}
diff --git a/types/test-cases/round-trips/064-session-summary-rich-directories.json b/types/test-cases/round-trips/064-session-summary-rich-directories.json
new file mode 100644
index 000000000..44c74ae81
--- /dev/null
+++ b/types/test-cases/round-trips/064-session-summary-rich-directories.json
@@ -0,0 +1,20 @@
+{
+ "name": "session-summary-rich-directories",
+ "group": "A",
+ "description": "Summary deltas replace the directory list with complete mixed-format entries.",
+ "type": "SessionSummaryChangedParams",
+ "input": {
+ "channel": "ahp-root://", "session": "ahp-session:/directories",
+ "changes": { "workingDirectories": ["file:///legacy", {
+ "uri": "file:///tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///main" }
+ }] }
+ },
+ "acceptableOutputs": [{
+ "channel": "ahp-root://", "session": "ahp-session:/directories",
+ "changes": { "workingDirectories": ["file:///legacy", {
+ "uri": "file:///tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///main" }
+ }] }
+ }]
+}
diff --git a/types/test-cases/round-trips/065-directory-set-rich.json b/types/test-cases/round-trips/065-directory-set-rich.json
new file mode 100644
index 000000000..deeccbbdc
--- /dev/null
+++ b/types/test-cases/round-trips/065-directory-set-rich.json
@@ -0,0 +1,20 @@
+{
+ "name": "directory-set-rich",
+ "group": "A",
+ "description": "Live set actions carry the full URI-keyed record rather than parallel metadata.",
+ "type": "ActionEnvelope",
+ "input": {
+ "channel": "ahp-session:/directories", "serverSeq": 3,
+ "action": { "type": "session/workingDirectorySet", "directory": {
+ "uri": "file:///tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///main" }
+ } }
+ },
+ "acceptableOutputs": [{
+ "channel": "ahp-session:/directories", "serverSeq": 3,
+ "action": { "type": "session/workingDirectorySet", "directory": {
+ "uri": "file:///tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///main" }
+ } }
+ }]
+}
diff --git a/types/test-cases/round-trips/066-directory-replacement-rich.json b/types/test-cases/round-trips/066-directory-replacement-rich.json
new file mode 100644
index 000000000..5cb2f7152
--- /dev/null
+++ b/types/test-cases/round-trips/066-directory-replacement-rich.json
@@ -0,0 +1,22 @@
+{
+ "name": "directory-replacement-rich",
+ "group": "A",
+ "description": "Live replacement actions target a URI and carry a complete new directory record.",
+ "type": "ActionEnvelope",
+ "input": {
+ "channel": "ahp-session:/directories", "serverSeq": 4,
+ "action": {
+ "type": "session/workingDirectoryReplaced", "directory": "file:///main/packages/api",
+ "replacement": { "uri": "file:///tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///main" } }
+ }
+ },
+ "acceptableOutputs": [{
+ "channel": "ahp-session:/directories", "serverSeq": 4,
+ "action": {
+ "type": "session/workingDirectoryReplaced", "directory": "file:///main/packages/api",
+ "replacement": { "uri": "file:///tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///main" } }
+ }
+ }]
+}
diff --git a/types/test-cases/round-trips/067-directory-unknown-origin.json b/types/test-cases/round-trips/067-directory-unknown-origin.json
new file mode 100644
index 000000000..2d08acb56
--- /dev/null
+++ b/types/test-cases/round-trips/067-directory-unknown-origin.json
@@ -0,0 +1,14 @@
+{
+ "name": "directory-unknown-origin",
+ "group": "A",
+ "description": "Future working-directory origin kinds survive typed decoders without losing their payload.",
+ "type": "StateAction",
+ "input": {
+ "type": "session/workingDirectorySet",
+ "directory": { "uri": "file:///future", "origin": { "kind": "future", "location": "host-defined" } }
+ },
+ "acceptableOutputs": [{
+ "type": "session/workingDirectorySet",
+ "directory": { "uri": "file:///future", "origin": { "kind": "future", "location": "host-defined" } }
+ }]
+}
diff --git a/types/test-cases/round-trips/068-reconnect-replay-rich-directories.json b/types/test-cases/round-trips/068-reconnect-replay-rich-directories.json
new file mode 100644
index 000000000..f8c1703d3
--- /dev/null
+++ b/types/test-cases/round-trips/068-reconnect-replay-rich-directories.json
@@ -0,0 +1,28 @@
+{
+ "name": "reconnect-replay-rich-directories",
+ "group": "A",
+ "description": "Reconnect replay carries both rich and URI-only actions under the retained capability.",
+ "type": "ReconnectResult",
+ "input": {
+ "type": "replay", "missing": [],
+ "actions": [
+ { "channel": "ahp-session:/directories", "serverSeq": 5,
+ "action": { "type": "session/workingDirectorySet", "directory": "file:///legacy" } },
+ { "channel": "ahp-session:/directories", "serverSeq": 6,
+ "action": { "type": "session/workingDirectoryReplaced", "directory": "file:///legacy",
+ "replacement": { "uri": "file:///tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///main" } } } }
+ ]
+ },
+ "acceptableOutputs": [{
+ "type": "replay", "missing": [],
+ "actions": [
+ { "channel": "ahp-session:/directories", "serverSeq": 5,
+ "action": { "type": "session/workingDirectorySet", "directory": "file:///legacy" } },
+ { "channel": "ahp-session:/directories", "serverSeq": 6,
+ "action": { "type": "session/workingDirectoryReplaced", "directory": "file:///legacy",
+ "replacement": { "uri": "file:///tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///main" } } } }
+ ]
+ }]
+}
diff --git a/types/test-cases/round-trips/069-reconnect-snapshot-rich-directories.json b/types/test-cases/round-trips/069-reconnect-snapshot-rich-directories.json
new file mode 100644
index 000000000..0d159a990
--- /dev/null
+++ b/types/test-cases/round-trips/069-reconnect-snapshot-rich-directories.json
@@ -0,0 +1,34 @@
+{
+ "name": "reconnect-snapshot-rich-directories",
+ "group": "A",
+ "description": "Full-snapshot reconnect recovery preserves actual directory provenance.",
+ "type": "ReconnectResult",
+ "input": {
+ "type": "snapshot",
+ "snapshots": [{
+ "resource": "ahp-session:/directories", "fromSeq": 9,
+ "state": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": ["file:///legacy", {
+ "uri": "file:///tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///main" }
+ }]
+ }
+ }]
+ },
+ "acceptableOutputs": [{
+ "type": "snapshot",
+ "snapshots": [{
+ "resource": "ahp-session:/directories", "fromSeq": 9,
+ "state": {
+ "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": ["file:///legacy", {
+ "uri": "file:///tree/packages/api", "repo": "https://example.org/project.git",
+ "origin": { "kind": "worktree", "mainWorktree": "file:///main" }
+ }]
+ }
+ }]
+ }]
+}
diff --git a/types/test-cases/round-trips/070-initialize-without-directory-info.json b/types/test-cases/round-trips/070-initialize-without-directory-info.json
new file mode 100644
index 000000000..6d37c7c38
--- /dev/null
+++ b/types/test-cases/round-trips/070-initialize-without-directory-info.json
@@ -0,0 +1,8 @@
+{
+ "name": "initialize-without-directory-info",
+ "group": "A",
+ "description": "Older clients can omit the rich-directory capability.",
+ "type": "InitializeParams",
+ "input": { "channel": "ahp-root://", "clientId": "legacy-client", "protocolVersions": ["0.9.0"] },
+ "acceptableOutputs": [{ "channel": "ahp-root://", "clientId": "legacy-client", "protocolVersions": ["0.9.0"] }]
+}
diff --git a/types/test-cases/round-trips/071-initialize-legacy-directories.json b/types/test-cases/round-trips/071-initialize-legacy-directories.json
new file mode 100644
index 000000000..18c3b6e58
--- /dev/null
+++ b/types/test-cases/round-trips/071-initialize-legacy-directories.json
@@ -0,0 +1,24 @@
+{
+ "name": "initialize-legacy-directories",
+ "group": "A",
+ "description": "The legacy initialize snapshot format remains decodable by new clients.",
+ "type": "InitializeResult",
+ "input": {
+ "protocolVersion": "0.9.0", "serverSeq": 2,
+ "snapshots": [{
+ "resource": "ahp-session:/directories", "fromSeq": 2,
+ "state": { "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": ["file:///legacy", "file:///tree/packages/api"] }
+ }]
+ },
+ "acceptableOutputs": [{
+ "protocolVersion": "0.9.0", "serverSeq": 2,
+ "snapshots": [{
+ "resource": "ahp-session:/directories", "fromSeq": 2,
+ "state": { "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": ["file:///legacy", "file:///tree/packages/api"] }
+ }]
+ }]
+}
diff --git a/types/test-cases/round-trips/072-subscribe-legacy-directories.json b/types/test-cases/round-trips/072-subscribe-legacy-directories.json
new file mode 100644
index 000000000..a2beadf4e
--- /dev/null
+++ b/types/test-cases/round-trips/072-subscribe-legacy-directories.json
@@ -0,0 +1,18 @@
+{
+ "name": "subscribe-legacy-directories",
+ "group": "A",
+ "description": "Subscribe snapshots from legacy hosts contain only directory URIs.",
+ "type": "SubscribeResult",
+ "input": {
+ "snapshot": { "resource": "ahp-session:/directories", "fromSeq": 2,
+ "state": { "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": ["file:///legacy", "file:///tree/packages/api"] } }
+ },
+ "acceptableOutputs": [{
+ "snapshot": { "resource": "ahp-session:/directories", "fromSeq": 2,
+ "state": { "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": ["file:///legacy", "file:///tree/packages/api"] } }
+ }]
+}
diff --git a/types/test-cases/round-trips/073-list-sessions-legacy-directories.json b/types/test-cases/round-trips/073-list-sessions-legacy-directories.json
new file mode 100644
index 000000000..06b427ae7
--- /dev/null
+++ b/types/test-cases/round-trips/073-list-sessions-legacy-directories.json
@@ -0,0 +1,16 @@
+{
+ "name": "list-sessions-legacy-directories",
+ "group": "A",
+ "description": "Legacy session-list summaries remain URI-only.",
+ "type": "ListSessionsResult",
+ "input": { "items": [{
+ "resource": "ahp-session:/directories", "provider": "example", "title": "Directories", "status": 1,
+ "createdAt": "2026-09-21T00:00:00.000Z", "modifiedAt": "2026-09-21T00:00:00.000Z",
+ "workingDirectories": ["file:///legacy", "file:///tree/packages/api"]
+ }] },
+ "acceptableOutputs": [{ "items": [{
+ "resource": "ahp-session:/directories", "provider": "example", "title": "Directories", "status": 1,
+ "createdAt": "2026-09-21T00:00:00.000Z", "modifiedAt": "2026-09-21T00:00:00.000Z",
+ "workingDirectories": ["file:///legacy", "file:///tree/packages/api"]
+ }] }]
+}
diff --git a/types/test-cases/round-trips/074-session-added-legacy-directories.json b/types/test-cases/round-trips/074-session-added-legacy-directories.json
new file mode 100644
index 000000000..0ffa4f6cc
--- /dev/null
+++ b/types/test-cases/round-trips/074-session-added-legacy-directories.json
@@ -0,0 +1,22 @@
+{
+ "name": "session-added-legacy-directories",
+ "group": "A",
+ "description": "Legacy session-added notifications contain URI-only directories.",
+ "type": "SessionAddedParams",
+ "input": {
+ "channel": "ahp-root://",
+ "summary": {
+ "resource": "ahp-session:/directories", "provider": "example", "title": "Directories", "status": 1,
+ "createdAt": "2026-09-21T00:00:00.000Z", "modifiedAt": "2026-09-21T00:00:00.000Z",
+ "workingDirectories": ["file:///legacy", "file:///tree/packages/api"]
+ }
+ },
+ "acceptableOutputs": [{
+ "channel": "ahp-root://",
+ "summary": {
+ "resource": "ahp-session:/directories", "provider": "example", "title": "Directories", "status": 1,
+ "createdAt": "2026-09-21T00:00:00.000Z", "modifiedAt": "2026-09-21T00:00:00.000Z",
+ "workingDirectories": ["file:///legacy", "file:///tree/packages/api"]
+ }
+ }]
+}
diff --git a/types/test-cases/round-trips/075-session-summary-legacy-directories.json b/types/test-cases/round-trips/075-session-summary-legacy-directories.json
new file mode 100644
index 000000000..b17c6ae87
--- /dev/null
+++ b/types/test-cases/round-trips/075-session-summary-legacy-directories.json
@@ -0,0 +1,14 @@
+{
+ "name": "session-summary-legacy-directories",
+ "group": "A",
+ "description": "Legacy summary deltas replace directories using only URI strings.",
+ "type": "SessionSummaryChangedParams",
+ "input": {
+ "channel": "ahp-root://", "session": "ahp-session:/directories",
+ "changes": { "workingDirectories": ["file:///legacy", "file:///tree/packages/api"] }
+ },
+ "acceptableOutputs": [{
+ "channel": "ahp-root://", "session": "ahp-session:/directories",
+ "changes": { "workingDirectories": ["file:///legacy", "file:///tree/packages/api"] }
+ }]
+}
diff --git a/types/test-cases/round-trips/076-reconnect-replay-legacy-directories.json b/types/test-cases/round-trips/076-reconnect-replay-legacy-directories.json
new file mode 100644
index 000000000..188256394
--- /dev/null
+++ b/types/test-cases/round-trips/076-reconnect-replay-legacy-directories.json
@@ -0,0 +1,24 @@
+{
+ "name": "reconnect-replay-legacy-directories",
+ "group": "A",
+ "description": "Legacy replay set and replace actions remain URI-only.",
+ "type": "ReconnectResult",
+ "input": {
+ "type": "replay", "missing": [],
+ "actions": [
+ { "channel": "ahp-session:/directories", "serverSeq": 5,
+ "action": { "type": "session/workingDirectorySet", "directory": "file:///legacy" } },
+ { "channel": "ahp-session:/directories", "serverSeq": 6,
+ "action": { "type": "session/workingDirectoryReplaced", "directory": "file:///legacy", "replacement": "file:///tree/packages/api" } }
+ ]
+ },
+ "acceptableOutputs": [{
+ "type": "replay", "missing": [],
+ "actions": [
+ { "channel": "ahp-session:/directories", "serverSeq": 5,
+ "action": { "type": "session/workingDirectorySet", "directory": "file:///legacy" } },
+ { "channel": "ahp-session:/directories", "serverSeq": 6,
+ "action": { "type": "session/workingDirectoryReplaced", "directory": "file:///legacy", "replacement": "file:///tree/packages/api" } }
+ ]
+ }]
+}
diff --git a/types/test-cases/round-trips/077-reconnect-snapshot-legacy-directories.json b/types/test-cases/round-trips/077-reconnect-snapshot-legacy-directories.json
new file mode 100644
index 000000000..162d355af
--- /dev/null
+++ b/types/test-cases/round-trips/077-reconnect-snapshot-legacy-directories.json
@@ -0,0 +1,24 @@
+{
+ "name": "reconnect-snapshot-legacy-directories",
+ "group": "A",
+ "description": "Legacy full-snapshot reconnect recovery remains URI-only.",
+ "type": "ReconnectResult",
+ "input": {
+ "type": "snapshot",
+ "snapshots": [{
+ "resource": "ahp-session:/directories", "fromSeq": 9,
+ "state": { "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": ["file:///legacy", "file:///tree/packages/api"] }
+ }]
+ },
+ "acceptableOutputs": [{
+ "type": "snapshot",
+ "snapshots": [{
+ "resource": "ahp-session:/directories", "fromSeq": 9,
+ "state": { "provider": "example", "title": "Directories", "status": 1,
+ "lifecycle": "ready", "activeClients": [], "chats": [],
+ "workingDirectories": ["file:///legacy", "file:///tree/packages/api"] }
+ }]
+ }]
+}