Goal
Move 13 contract-level types from packages/core/src/types.ts into the new packages/session-contract/src/types.ts. Add the isCodesparSession type guard in packages/session-contract/src/guards.ts. Update packages/core/src/types.ts to re-export from the contract package so all existing callers compile unchanged.
Context
The session contract types currently live inside packages/core. Any package that needs to reference Session, SessionBase, or StreamEvent must depend on @codespar/core even when it only needs the interface shapes — pulling in runtime code it doesn't need.
Extracting these types into a dedicated @codespar/session-contract package breaks that coupling. The contract package has no runtime dependencies: just TypeScript type definitions and a single type guard function. Packages that only need the interface shapes (SDK wrappers, adapters, test fixtures) can depend on the contract package directly without dragging in core internals.
This issue depends on the package scaffold being in place (ISSUE:1). The contract types established here are consumed by SDK wiring (ISSUE:3), the contract test suite (ISSUE:4), and the adapter layer (ISSUE:6).
Interface hierarchy
// packages/session-contract/src/types.ts
interface SessionBase {
readonly id: string;
readonly status: "active" | "closed" | "error";
execute(toolName: string, params: Record<string, unknown>): Promise<ToolResult>;
send(message: string): Promise<SendResult>;
sendStream(message: string): AsyncIterable<StreamEvent>;
connections(): Promise<BaseConnection[]>;
close(): Promise<void>;
}
interface Session extends SessionBase {
proxyExecute(request: ProxyRequest): Promise<ProxyResult>;
authorize(serverId: string, config: AuthConfig): Promise<AuthResult>;
mcp?: { url: string; headers: Record<string, string> };
}
type BaseConnection = { id: string; connected: boolean };
interface CreateSessionRequest {
servers: string[];
metadata?: Record<string, string>;
projectId?: string;
}
type StreamEvent =
| { type: "user_message"; content: string }
| { type: "assistant_text"; content: string; iteration: number }
| { type: "tool_use"; toolName: string; params: Record<string, unknown> }
| { type: "tool_result"; toolCall: ToolCallRecord }
| { type: "done" }
| { type: "error"; message: string; code?: string };
The remaining 8 types to move alongside the 5 above: ToolResult, SendResult, ServerConnection, ProxyRequest, ProxyResult, AuthConfig, AuthResult, ToolCallRecord.
Stability note — StreamEvent: SSE consumers in framework adapters and external callers read event.toolCall and event.iteration unconditionally. Do not rename, flatten, or reorder the union variants. ToolCallRecord mirrors the session_tool_calls database row shape — field renames here are a data-layer breaking change. Copy these types verbatim from packages/core/src/types.ts.
Type guard
// packages/session-contract/src/guards.ts
export function isCodesparSession(s: SessionBase): s is Session {
return "proxyExecute" in s && "authorize" in s;
}
Checks the two codespar-specific methods present on Session but absent from SessionBase. mcp is optional on Session and cannot serve as the discriminant — the two-method check is correct.
Main entrypoint
// packages/session-contract/src/index.ts
export * from "./types.js";
export * from "./guards.js";
Re-export in core
packages/core/src/types.ts must import the 13 types from @codespar/session-contract and re-export them so all existing import paths resolve unchanged:
export {
SessionBase,
Session,
BaseConnection,
CreateSessionRequest,
StreamEvent,
ToolResult,
SendResult,
ServerConnection,
ProxyRequest,
ProxyResult,
AuthConfig,
AuthResult,
ToolCallRecord,
} from "@codespar/session-contract";
Add "@codespar/session-contract": "*" to packages/core/package.json dependencies so this import resolves in the monorepo workspace. This workspace link is sufficient for this phase; the formal peer-dependency declaration is handled in ISSUE:3.
Acceptance Criteria
packages/session-contract/src/types.ts contains all 13 types listed above, copied verbatim from packages/core/src/types.ts.
packages/session-contract/src/guards.ts exports isCodesparSession exactly as specified.
packages/session-contract/src/index.ts re-exports from both files.
packages/core/src/types.ts no longer defines any of the 13 types locally — it imports and re-exports them all from @codespar/session-contract.
packages/core/package.json lists "@codespar/session-contract": "*" in dependencies.
pnpm --filter @codespar/session-contract build completes without errors.
pnpm --filter @codespar/core typecheck completes without errors.
pnpm --filter @codespar/sdk typecheck completes without errors (no import path changes required in the SDK).
isCodesparSession correctly narrows SessionBase to Session when both proxyExecute and authorize are present, and does not narrow when either is absent.
- No changes to any file outside
packages/session-contract/ and packages/core/src/types.ts and packages/core/package.json.
Validation
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"
cd "$REPO_ROOT"
echo "=== Build: @codespar/session-contract ==="
pnpm --filter @codespar/session-contract build
echo "=== Typecheck: @codespar/core ==="
pnpm --filter @codespar/core typecheck
echo "=== Typecheck: @codespar/sdk ==="
pnpm --filter @codespar/sdk typecheck
echo "=== Type guard narrowing check ==="
TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT
cat > "$TMPDIR/guard-check.ts" <<'EOF'
import { SessionBase, Session, isCodesparSession } from "@codespar/session-contract";
// Minimal object that satisfies Session structurally
const full = {
id: "s1",
status: "active" as const,
execute: async () => ({ success: true as const, output: "" }),
send: async () => ({ success: true as const }),
sendStream: async function* () {},
connections: async () => [],
close: async () => {},
proxyExecute: async () => ({ success: true as const, output: "" }),
authorize: async () => ({ success: true as const, token: "" }),
};
// Minimal object that satisfies only SessionBase
const base = {
id: "s2",
status: "active" as const,
execute: async () => ({ success: true as const, output: "" }),
send: async () => ({ success: true as const }),
sendStream: async function* () {},
connections: async () => [],
close: async () => {},
};
// isCodesparSession must narrow full to Session
function assertSession(s: Session): void {}
if (isCodesparSession(full as SessionBase)) {
assertSession(full);
}
// isCodesparSession must not narrow base (it lacks both methods)
if (isCodesparSession(base)) {
// This branch should not be reached, but if the type compiles it's a TS error
// @ts-expect-error base does not satisfy Session
assertSession(base);
}
export {};
EOF
cat > "$TMPDIR/tsconfig.json" <<'EOF'
{
"compilerOptions": {
"strict": true,
"moduleResolution": "bundler",
"module": "esnext",
"target": "esnext",
"noEmit": true,
"paths": {
"@codespar/session-contract": ["../packages/session-contract/src/index.ts"]
},
"baseUrl": "$REPO_ROOT"
},
"include": ["guard-check.ts"]
}
EOF
# Replace placeholder in tsconfig
sed -i "s|\$REPO_ROOT|$REPO_ROOT|g" "$TMPDIR/tsconfig.json"
pnpm exec tsc --project "$TMPDIR/tsconfig.json"
echo "=== Verify core types.ts no longer defines contract types locally ==="
for TYPE in SessionBase Session BaseConnection CreateSessionRequest StreamEvent \
ToolResult SendResult ServerConnection ProxyRequest ProxyResult \
AuthConfig AuthResult ToolCallRecord; do
if grep -Pn "^(export\s+)?(interface|type)\s+${TYPE}\b" \
"$REPO_ROOT/packages/core/src/types.ts" > /dev/null 2>&1; then
echo "FAIL: $TYPE is still defined locally in packages/core/src/types.ts"
exit 1
fi
done
echo "All 13 types confirmed absent from core/src/types.ts local definitions."
echo "=== All checks passed ==="
Dependencies
- ISSUE:1 — the
packages/session-contract package scaffold must exist (directory, package.json, tsconfig.json, build script) before types can be placed there.
Downstream Dependencies
- ISSUE:3 — SDK wiring imports
Session and SessionBase from @codespar/session-contract directly; the interface hierarchy established here defines the SDK surface.
- ISSUE:4 — contract test suite imports from
@codespar/session-contract to validate the type guard and interface shapes against live session objects.
- ISSUE:6 — adapter layer implements
SessionBase and uses isCodesparSession to determine whether to call proxyExecute; both must be stable before the adapter can be written.
Goal
Move 13 contract-level types from
packages/core/src/types.tsinto the newpackages/session-contract/src/types.ts. Add theisCodesparSessiontype guard inpackages/session-contract/src/guards.ts. Updatepackages/core/src/types.tsto re-export from the contract package so all existing callers compile unchanged.Context
The session contract types currently live inside
packages/core. Any package that needs to referenceSession,SessionBase, orStreamEventmust depend on@codespar/coreeven when it only needs the interface shapes — pulling in runtime code it doesn't need.Extracting these types into a dedicated
@codespar/session-contractpackage breaks that coupling. The contract package has no runtime dependencies: just TypeScript type definitions and a single type guard function. Packages that only need the interface shapes (SDK wrappers, adapters, test fixtures) can depend on the contract package directly without dragging in core internals.This issue depends on the package scaffold being in place (ISSUE:1). The contract types established here are consumed by SDK wiring (ISSUE:3), the contract test suite (ISSUE:4), and the adapter layer (ISSUE:6).
Interface hierarchy
The remaining 8 types to move alongside the 5 above:
ToolResult,SendResult,ServerConnection,ProxyRequest,ProxyResult,AuthConfig,AuthResult,ToolCallRecord.Stability note —
StreamEvent: SSE consumers in framework adapters and external callers readevent.toolCallandevent.iterationunconditionally. Do not rename, flatten, or reorder the union variants.ToolCallRecordmirrors thesession_tool_callsdatabase row shape — field renames here are a data-layer breaking change. Copy these types verbatim frompackages/core/src/types.ts.Type guard
Checks the two codespar-specific methods present on
Sessionbut absent fromSessionBase.mcpis optional onSessionand cannot serve as the discriminant — the two-method check is correct.Main entrypoint
Re-export in core
packages/core/src/types.tsmust import the 13 types from@codespar/session-contractand re-export them so all existing import paths resolve unchanged:Add
"@codespar/session-contract": "*"topackages/core/package.jsondependencies so this import resolves in the monorepo workspace. This workspace link is sufficient for this phase; the formal peer-dependency declaration is handled in ISSUE:3.Acceptance Criteria
packages/session-contract/src/types.tscontains all 13 types listed above, copied verbatim frompackages/core/src/types.ts.packages/session-contract/src/guards.tsexportsisCodesparSessionexactly as specified.packages/session-contract/src/index.tsre-exports from both files.packages/core/src/types.tsno longer defines any of the 13 types locally — it imports and re-exports them all from@codespar/session-contract.packages/core/package.jsonlists"@codespar/session-contract": "*"in dependencies.pnpm --filter @codespar/session-contract buildcompletes without errors.pnpm --filter @codespar/core typecheckcompletes without errors.pnpm --filter @codespar/sdk typecheckcompletes without errors (no import path changes required in the SDK).isCodesparSessioncorrectly narrowsSessionBasetoSessionwhen bothproxyExecuteandauthorizeare present, and does not narrow when either is absent.packages/session-contract/andpackages/core/src/types.tsandpackages/core/package.json.Validation
Dependencies
packages/session-contractpackage scaffold must exist (directory,package.json,tsconfig.json, build script) before types can be placed there.Downstream Dependencies
SessionandSessionBasefrom@codespar/session-contractdirectly; the interface hierarchy established here defines the SDK surface.@codespar/session-contractto validate the type guard and interface shapes against live session objects.SessionBaseand usesisCodesparSessionto determine whether to callproxyExecute; both must be stable before the adapter can be written.