Skip to content

SessionFs structured error contract + workspace E2E tests#1102

Merged
SteveSandersonMS merged 26 commits intomainfrom
stevesa/workspace-manager-on-sessionfs
Apr 21, 2026
Merged

SessionFs structured error contract + workspace E2E tests#1102
SteveSandersonMS merged 26 commits intomainfrom
stevesa/workspace-manager-on-sessionfs

Conversation

@SteveSandersonMS
Copy link
Copy Markdown
Contributor

@SteveSandersonMS SteveSandersonMS commented Apr 17, 2026

Summary

  • Updates SDKs to support the new sessionFs structured error contract from the runtime
  • Simplifies per-language codegen to react to the new runtime JSON schema convention of emitting a named $ref type for every type in the hierarchy
    • This is a huge simplification/consistency win for C#/Python/Go, eliminating ~500 lines of complex codegen logic that tried to account for types maybe or maybe not being $ref and maybe or maybe not having explicit names
    • It's a simplification for TypeScript output too, but at the cost that we no longer retain the anonymous nested types (e.g., for discriminated unions) and instead have explicitly-named types for everything. On balance this is much more maintainable even if it doesn't take as much advantage of TypeScript's type system as we tried to before.
    • Also we now try to emit all types/members in alphabetical order so that later codegen changes will lead to more intelligible diffs

…e tests

- Regenerate rpc.ts from updated api.schema.json with structured error type
  (error?: { code: 'ENOENT' | 'UNKNOWN', message?: string }) on all sessionFs results
- Update createTestSessionFsHandler to catch fs errors and return error objects
  instead of throwing (matches the new RPC contract)
- Add E2E tests for workspace metadata and plan.md written via sessionFs
- Add test snapshots for new workspace tests

Depends on: github/copilot-agent-runtime#6479

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@SteveSandersonMS SteveSandersonMS force-pushed the stevesa/workspace-manager-on-sessionfs branch from ae9d745 to b39f0de Compare April 20, 2026 13:09
SteveSandersonMS and others added 8 commits April 20, 2026 14:27
Two issues caused duplicate type generation in json-schema-to-typescript:

1. $
ef objects with sibling keywords (e.g. description) were treated as new
   inline types instead of reusing the referenced definition. Fix: strip all
   sibling keywords from $
ef objects in normalizeSchemaForTypeScript.

2. withSharedDefinitions copies definitions into $defs when $defs is empty,
   then normalizeSchemaForTypeScript aliased collisions as $defs_X. Since
   definitions entries go through the full pipeline (title-stripping, ref-reuse)
   but $defs copies don't, they diverge. Fix: when a key exists in both,
   prefer the definitions entry and drop the $defs duplicate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The runtime schema uses anyOf [{not:{}}, {$ref: ...}] for optional
results (e.g., writeFile returns SessionFsError or undefined).
Previously all 4 codegen scripts only recognized {type: 'null'} as
null-like, producing incorrect wrapper types.

Changes:
- utils.ts: Add getNullableInner() to detect {not:{}} null patterns
- typescript.ts: Emit 'T | undefined' for nullable results
- csharp.ts: Emit 'Task<T?>' for nullable results
- python.ts: Emit 'T | None' for nullable results
- go.ts: Emit '*T' (pointer) for nullable results
- Regenerated all 4 languages
- Fixed C# test handler to use Task<SessionFsError?>
- Fixed Go session.go Vision type rename

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduce a SessionFsProvider base class/interface in each language that
wraps the generated SessionFsHandler RPC contract with idiomatic patterns:

- C#: Abstract class with virtual methods that throw on error
- Go: Interface with (value, error) returns; unexported adapter
- TypeScript: Interface + createSessionFsAdapter() wrapper function
- Python: ABC + create_session_fs_adapter() wrapper function

Each adapter catches exceptions/errors and converts them to SessionFsError
results (ENOENT/UNKNOWN). Public APIs now use SessionFsProvider exclusively.

Also fixes C# serialization crash (CommonErrorData not registered in
source-generated JSON context) and adds shared Warning-level logging
to all C# E2E tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread python/copilot/session.py Fixed
Comment thread dotnet/src/SessionFsProvider.cs Fixed
Comment thread dotnet/src/SessionFsProvider.cs Fixed
Comment thread dotnet/src/SessionFsProvider.cs Fixed
Comment thread dotnet/src/SessionFsProvider.cs Fixed
Comment thread dotnet/src/SessionFsProvider.cs Fixed
Comment thread dotnet/src/SessionFsProvider.cs Fixed
Comment thread dotnet/src/SessionFsProvider.cs Fixed
Comment thread dotnet/src/SessionFsProvider.cs Fixed
Comment thread dotnet/src/SessionFsProvider.cs Fixed
Comment thread dotnet/src/SessionFsProvider.cs Fixed
The _dispatch_request method was converting None handler results to {}
(empty dict) before sending JSON-RPC responses. The runtime expects null
for void operations (mkdir, writeFile, appendFile). Receiving {} caused
the runtime to fall back to local filesystem for subsequent writes.

Also removes debug tracing from session_fs_provider.py adapter.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@SteveSandersonMS SteveSandersonMS marked this pull request as ready for review April 20, 2026 19:10
@SteveSandersonMS SteveSandersonMS requested a review from a team as a code owner April 20, 2026 19:10
Copilot AI review requested due to automatic review settings April 20, 2026 19:10
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the SDKs/codegen to support the new structured sessionFs error contract (returning classified errors as part of results instead of relying on exception serialization), and adds E2E coverage asserting workspace state is persisted through sessionFs.

Changes:

  • Regenerates RPC types and updates codegen to better handle nullable anyOf result wrappers and definition deduplication.
  • Introduces idiomatic SessionFsProvider abstractions + adapters (Node/Python/Go/.NET) that translate thrown/native errors into the structured SessionFsError contract.
  • Adds new E2E tests + snapshots verifying workspace.yaml and plan.md are written via sessionFs.
Show a summary per file
File Description
test/snapshots/session_fs/should_write_workspace_metadata_via_sessionfs.yaml New snapshot for workspace metadata write scenario.
test/snapshots/session_fs/should_persist_plan_md_via_sessionfs.yaml New snapshot for plan persistence scenario.
scripts/codegen/utils.ts Adds getNullableInner helper and minor clone logic tweak.
scripts/codegen/typescript.ts Updates TS codegen to handle nullable results and $ref normalization.
scripts/codegen/python.ts Updates Python codegen for nullable results and reduces quicktype duplicate-type noise.
scripts/codegen/go.ts Updates Go codegen for nullable results/anyOf handling and quicktype duplicate-type noise.
scripts/codegen/csharp.ts Updates C# codegen for nullable anyOf and discriminated unions.
python/e2e/test_session_fs.py Adds E2E tests for workspace.yaml + plan.md; refactors test FS handler to provider style.
python/copilot/session_fs_provider.py Adds provider abstraction + adapter that maps exceptions to SessionFSError.
python/copilot/session.py Updates generated type names and session-fs handler typing to use provider abstraction.
python/copilot/generated/session_events.py Regenerated session-events types (new fields and renames).
python/copilot/client.py Wraps user provider with adapter when sessionFs is enabled.
python/copilot/_jsonrpc.py Allows request handlers to return None (JSON null) for nullable result contract.
python/copilot/init.py Exposes provider/adapter types in public Python package surface.
nodejs/test/e2e/session_fs.test.ts Adds E2E tests for workspace.yaml + plan.md; updates test FS handler to provider style.
nodejs/src/types.ts Switches public session fs handler type to provider; updates permission kind union.
nodejs/src/sessionFsProvider.ts Adds provider abstraction + adapter that maps Node fs errors to SessionFsError.
nodejs/src/index.ts Re-exports provider/adapter types from public entrypoint.
nodejs/src/generated/rpc.ts Regenerated RPC types reflecting structured sessionFs errors + other schema updates.
nodejs/src/client.ts Wraps user provider with adapter when sessionFs is enabled.
go/types.go Public API now uses SessionFsProvider; fixes model capabilities vision alias.
go/session_fs_provider.go Adds provider interface + adapter mapping Go errors to SessionFSError.
go/session.go Updates enums/types for elicitation/permissions; removes unnecessary model-capabilities conversion.
go/internal/e2e/session_fs_test.go Adds E2E coverage for workspace.yaml + plan.md; updates test provider impl.
go/generated_session_events.go Regenerated session-events types (new fields and renames).
go/client.go Wraps user provider with adapter when sessionFs is enabled.
dotnet/test/SessionFsTests.cs Adds E2E-style tests for workspace.yaml + plan.md; updates test handler base type.
dotnet/test/SessionEventSerializationTests.cs Updates shutdown model metrics test to typed objects.
dotnet/test/Harness/E2ETestContext.cs Adds optional logger plumbing into created clients.
dotnet/test/Harness/E2ETestBase.cs Adds xUnit logger forwarding warnings+ into test output.
dotnet/src/Types.cs Session config now uses SessionFsProvider instead of ISessionFsHandler.
dotnet/src/SessionFsProvider.cs Adds base class implementing structured error mapping for sessionFs.
dotnet/src/Session.cs Updates permission handling to new generated RPC contract types.
dotnet/src/Generated/SessionEvents.cs Regenerated session-events types and adds new nested metric/usage types.
dotnet/src/Generated/Rpc.cs Regenerated RPC types including SessionFsError and polymorphic PermissionDecision.
dotnet/src/Client.cs Updates sessionFs handler plumbing + JSON serializer context additions.

Copilot's findings

  • Files reviewed: 32/39 changed files
  • Comments generated: 5

Comment thread nodejs/src/types.ts
Comment thread nodejs/test/e2e/session_fs.test.ts
Comment thread python/e2e/test_session_fs.py
Comment thread python/copilot/session_fs_provider.py Outdated
Comment thread dotnet/src/Session.cs
SteveSandersonMS and others added 5 commits April 21, 2026 13:13
- Alphabetical property ordering in all languages
- Alphabetical type/struct/class emission ordering
- Required-first param sort for both C# method emission sites

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread python/copilot/session.py Fixed
Comment thread python/copilot/session.py Dismissed
SteveSandersonMS and others added 3 commits April 21, 2026 14:50
datetime.UTC alias is not recognized by ty type checker. Use
timezone.utc with noqa: UP017 suppression instead.

Also remove unused SessionFsProvider import from session.py (moved to
TYPE_CHECKING block).

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

The grep tool now returns relative paths instead of absolute paths.
Update the snapshot to match.

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

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by SDK Consistency Review Agent for issue #1102 · ● 1.2M

Comment thread dotnet/src/SessionFsProvider.cs Outdated
/// <param name="path">SessionFs-relative path.</param>
/// <param name="recursive">Whether to create parent directories.</param>
/// <param name="cancellationToken">Cancellation token.</param>
protected abstract Task MkdirAsync(string path, bool recursive, CancellationToken cancellationToken);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-SDK consistency note: Same as Go — the mode parameter from the RPC SessionFsMkdirRequest is silently dropped (see line ~145 in the ISessionFsHandler.MkdirAsync implementation).

Node.js and Python both expose it in their SessionFsProvider abstractions. Consider adding long? mode = null to MkdirAsync for parity:

protected abstract Task MkdirAsync(string path, bool recursive, long? mode = null, CancellationToken cancellationToken = default);

The adapter would then pass request.Mode through. Even if most .NET implementors ignore it, having it in the signature keeps the public API aligned across all four SDKs.

Comment thread go/session_fs_provider.go Outdated
// Return os.ErrNotExist if the path does not exist.
Stat(path string) (*SessionFsFileInfo, error)
// Mkdir creates a directory. If recursive is true, create parent directories as needed.
Mkdir(path string, recursive bool) error
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-SDK consistency note: The Mkdir interface definition omits the mode parameter, but the underlying RPC type (SessionFSMkdirRequest.Mode) does include it, and the Node.js and Python SessionFsProvider equivalents both expose it:

  • Node.js: mkdir(path: string, recursive: boolean, mode?: number): Promise<void>
  • Python: async def mkdir(self, path: str, recursive: bool, mode: int | None = None) -> None

In the adapter (line ~111), request.Mode is silently dropped when calling a.provider.Mkdir(request.Path, recursive).

For consistency, consider adding mode as an optional parameter here (e.g. Mkdir(path string, recursive bool, mode *int64) error), so Go implementors can honour it if needed. The adapter would then pass request.Mode through.

SteveSandersonMS and others added 2 commits April 21, 2026 15:12
The compaction snapshot was stale after @github/copilot@1.0.35-0 update.
Regenerated to match new runtime compaction behavior. Unskip
the Node.js compaction test since it now passes reliably with
the refreshed snapshot.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a property uses $
ef to reference a definition that has a default
value, the default was not being picked up because we only checked
propSchema.default (which is undefined for a bare $
ef). Now we resolve
through $
ef and fall back to the referenced schema's default.

This fixes PermissionRequest.action defaulting to None instead of 'store'
for memory permission requests, and SessionTaskCompleteData.summary
defaulting to None instead of ''.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@SteveSandersonMS
Copy link
Copy Markdown
Contributor Author

Thanks for the cross-SDK consistency review! The \mkdir\ \mode\ note is acknowledged — Go and .NET don't have a direct equivalent of POSIX file mode, so the adapters intentionally omit it. The runtime doesn't currently send \mode\ values that require special handling, so this is safe for now. Can be added later if the runtime starts using non-default modes.

@SteveSandersonMS
Copy link
Copy Markdown
Contributor Author

Correction on my previous reply: Go and .NET do support file modes (\os.FileMode\ and \UnixFileMode\ respectively). The \mode\ parameter was omitted from the Go/C# provider interfaces for simplicity since the runtime currently uses default modes. Worth adding in a follow-up if needed — filed as a note for future work.

Node.js and Python already had the mode parameter on their
SessionFsProvider mkdir methods. This adds it to Go and .NET
for consistency, forwarding the POSIX-style permission mode
from the RPC request through to the provider implementation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread dotnet/src/SessionFsProvider.cs Fixed
Comment thread dotnet/src/SessionFsProvider.cs Fixed
Comment thread dotnet/src/SessionFsProvider.cs Fixed
Comment thread dotnet/src/SessionFsProvider.cs Fixed
Comment thread dotnet/src/SessionFsProvider.cs Fixed
Comment thread dotnet/src/SessionFsProvider.cs Fixed
Comment thread dotnet/src/SessionFsProvider.cs Fixed
Comment thread dotnet/src/SessionFsProvider.cs Fixed
Comment thread dotnet/src/SessionFsProvider.cs Fixed
Comment thread dotnet/src/SessionFsProvider.cs Fixed
The RPC types for writeFile and appendFile include an optional mode
field, but all 4 SDK languages were silently dropping it. This adds
the mode parameter to the provider interfaces and adapters for
Node.js, Python, Go, and .NET, consistent with how mkdir already
forwards mode.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment on lines +87 to +90
catch (Exception ex)
{
return new SessionFsReadFileResult { Error = ToSessionFsError(ex) };
}
Comment on lines +100 to +103
catch (Exception ex)
{
return ToSessionFsError(ex);
}
Comment on lines +113 to +116
catch (Exception ex)
{
return ToSessionFsError(ex);
}
Comment on lines +126 to +129
catch
{
return new SessionFsExistsResult { Exists = false };
}
Comment on lines +138 to +141
catch (Exception ex)
{
return new SessionFsStatResult { Error = ToSessionFsError(ex) };
}
Comment on lines +151 to +154
catch (Exception ex)
{
return ToSessionFsError(ex);
}
Comment on lines +164 to +167
catch (Exception ex)
{
return new SessionFsReaddirResult { Error = ToSessionFsError(ex) };
}
Comment on lines +177 to +180
catch (Exception ex)
{
return new SessionFsReaddirWithTypesResult { Error = ToSessionFsError(ex) };
}
Comment on lines +190 to +193
catch (Exception ex)
{
return ToSessionFsError(ex);
}
Comment on lines +203 to +206
catch (Exception ex)
{
return ToSessionFsError(ex);
}
…guages

Restore main's compaction snapshot and skip the compaction tests
across all 4 SDKs (Node.js, Python, Go, .NET) until they are
stabilized.

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

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by SDK Consistency Review Agent for issue #1102 · ● 1.4M

Comment thread nodejs/src/index.ts Outdated
SteveSandersonMS and others added 2 commits April 21, 2026 15:45
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
createSessionFsAdapter is a runtime function, not a type. It was
incorrectly placed inside the 'export type { ... }' block which
would erase it during compilation. Move it to the value export
block alongside defineTool, approveAll, etc.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@SteveSandersonMS SteveSandersonMS merged commit a3e273c into main Apr 21, 2026
36 of 37 checks passed
@SteveSandersonMS SteveSandersonMS deleted the stevesa/workspace-manager-on-sessionfs branch April 21, 2026 14:57
@github-actions
Copy link
Copy Markdown
Contributor

SDK Consistency Review ✅

This PR maintains strong cross-SDK consistency. Here's a summary of findings:

✅ SessionFsProvider pattern — consistent across all 4 SDKs

The new SessionFsProvider abstraction (throw on errors → adapter converts to structured { code, message } results) is now present in all four SDKs:

SDK Status
Node.js ✅ Added in this PR (sessionFsProvider.ts)
Go ✅ Added in this PR (session_fs_provider.go)
.NET ✅ Added in this PR (SessionFsProvider.cs)
Python ✅ Already present (session_fs_provider.py) — no change needed

Python's generated rpc.py already has the error field on all SessionFs*Result types and the SessionFsProvider adapter was already correct, so the Python SDK was correctly left untouched.

✅ Generated types — consistent

All SDKs have the error?: { code: 'ENOENT' | 'UNKNOWN'; message?: string } field on sessionFs result types. Schema-derived changes (PermissionDecision discriminated union, PermissionRequestKind with memory/hook, etc.) are reflected consistently across all SDKs.

💡 Minor observation: .NET StatAsync return type

In Node.js, Python, and Go, the provider's stat method returns a dedicated "no-error" info type (SessionFsFileInfo) that deliberately omits the error field — enforcing the "throw for errors" contract at compile time:

// Node.js: SessionFsFileInfo = Omit<SessionFsStatResult, "error">
stat(path: string): Promise<SessionFsFileInfo>;
# Python: dataclass with is_file, is_directory, size, mtime, birthtime
async def stat(self, path: str) -> SessionFsFileInfo: ...
// Go: struct without Error field
Stat(path string) (*SessionFsFileInfo, error)

In .NET, StatAsync returns SessionFsStatResult directly, which includes the Error property:

protected abstract Task<SessionFsStatResult> StatAsync(string path, CancellationToken cancellationToken);

This isn't wrong (throwing is still the recommended pattern and the adapter handles it), but it means .NET providers could also signal errors by setting Error on the returned result rather than throwing — a path not available in other SDKs. Adding a dedicated SessionFsStatInfo return type (mirroring the other SDKs) would enforce the contract more strictly, though this is a minor point and may not be worth the extra type.

Generated by SDK Consistency Review Agent for issue #1102 · ● 2.3M ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants